From 12dd1892b3d8971794cc1c70fadfa685d8cd40c6 Mon Sep 17 00:00:00 2001 From: Ruslan Dautkhanov Date: Fri, 22 May 2026 10:16:47 -0600 Subject: [PATCH] perf: background-warm GatewayConnection class during startSocket() (issue #557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cold-start work, C2 of the issue #557 series. GatewayConnection's static initializer references 14 command classes (ArrayCommand, CallCommand, ConstructorCommand, ..., StreamCommand) via class literals — so the *first* time GatewayConnection is touched at runtime, the JVM class-loads all 14 in sequence. Today that first touch happens inside the accept() loop when the first client connects, on the same thread that needs to immediately handshake back. The class-loading cost lands on the cold-call critical path. Kick off a one-shot daemon thread inside startSocket() that calls ``Class.forName("py4j.GatewayConnection", true, classLoader)`` — its ```` runs in parallel with the ``bind()`` on the main thread. By the time a client connects and we instantiate the first GatewayConnection in the accept loop, the class data is already in the JVM's class cache. Pure work re-ordering — no API change, no behavior change for any existing caller. Best-effort: errors in the warm-up thread are swallowed (the accept loop will still trigger the same class-load on first connection, just without the parallelism benefit). Thread safety: the JVM's per-class ```` lock is the synchronization primitive — if a client connects and triggers ``new GatewayConnection(...)`` before the warmup thread finishes ````, the main thread blocks on the same ```` lock just as it would have without warmup. Concurrent attempts cannot double-init or see partial state. Expected win is modest — ~10-100 ms on first connect depending on filesystem cache state. Subsequent connects to the same JVM are unchanged (classes already loaded). Targets XD (full cold start) and the first round of XB (gateway reconnect against running JVM); X1 / X2 / X4 / X6 won't move since they hold long-running connections once. The companion idea of "cache 14 command instance prototypes globally" was explored and dropped — Command instances hold per- connection state via init(gateway, connection), so they cannot be shared without an interface change. The class-loading win above is the safe subset of that idea. Co-authored-by: Isaac --- .../src/main/java/py4j/GatewayServer.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/py4j-java/src/main/java/py4j/GatewayServer.java b/py4j-java/src/main/java/py4j/GatewayServer.java index 0434c2e5..2c9a9a9d 100644 --- a/py4j-java/src/main/java/py4j/GatewayServer.java +++ b/py4j-java/src/main/java/py4j/GatewayServer.java @@ -879,6 +879,15 @@ public void start(boolean fork) { * If the port is busy. */ protected void startSocket() throws Py4JNetworkException { + // Kick off class-loading of GatewayConnection (and transitively + // the 14 command classes referenced in its static initializer) + // on a daemon thread in parallel with the bind. By the time the + // first client connects and we instantiate a GatewayConnection + // in the accept loop, those classes are already in the JVM's + // class cache instead of cold-loading on the first accept() — + // shaving tens to ~hundred ms off first-connect latency on + // cold filesystem-cache systems. issue #557. + warmupConnectionClasses(); try { sSocket = sSocketFactory.createServerSocket(); sSocket.setSoTimeout(connectTimeout); @@ -889,6 +898,37 @@ protected void startSocket() throws Py4JNetworkException { } } + /** + *

+ * Best-effort daemon-thread pre-load of GatewayConnection so its + * static initializer (which class-loads 14 command classes via + * class literals — see {@code GatewayConnection.}) runs in + * parallel with the {@code bind()} on the main thread. Pure work + * re-ordering — no behavior change. + *

+ * + *

+ * Errors are swallowed because this is purely a startup-time + * optimization: if the warm-up thread can't load the class, the + * main accept loop will still trigger the same class-load on the + * first connection (just without the parallelism benefit). + *

+ */ + private static void warmupConnectionClasses() { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + Class.forName("py4j.GatewayConnection", true, GatewayServer.class.getClassLoader()); + } catch (Throwable ignored) { + // Best-effort warm-up; see Javadoc. + } + } + }, "py4j-warmup"); + t.setDaemon(true); + t.start(); + } + /** *

* Gets a reference to the entry point on the Python side. This is often