2929import java .util .Map ;
3030import java .util .Objects ;
3131import java .util .concurrent .ConcurrentHashMap ;
32+ import java .util .concurrent .ConcurrentMap ;
3233import net .bytebuddy .ByteBuddy ;
3334import net .bytebuddy .description .field .FieldDescription ;
3435import net .bytebuddy .description .method .MethodDescription ;
113114import org .apache .beam .sdk .values .TypeDescriptor ;
114115import org .apache .beam .sdk .values .TypeDescriptors ;
115116import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .base .MoreObjects ;
117+ import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .collect .MapMaker ;
116118import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .collect .Maps ;
117119import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .primitives .Primitives ;
118120import org .checkerframework .checker .nullness .qual .MonotonicNonNull ;
@@ -242,6 +244,22 @@ public String toString() {
242244 private final Map <InvokerCacheKey , Constructor <?>> byteBuddyInvokerConstructorCache =
243245 new ConcurrentHashMap <>();
244246
247+ /**
248+ * A weak, identity-keyed cache of the constructor selected for each {@link DoFn} instance.
249+ *
250+ * <p>Selecting a constructor resolves the {@link DoFn}'s input and output type descriptors. Some
251+ * runners create a new invoker for the same {@link DoFn} at every bundle boundary, so doing that
252+ * work on every invocation can be expensive. A deserialized {@link DoFn} is a new instance, so
253+ * its constructor is selected again; later invoker creation for that instance reuses the cached
254+ * selection.
255+ *
256+ * <p>The constructor values do not reference their {@link DoFn} keys, and weak keys allow unused
257+ * instances to be collected. {@link MapMaker#weakKeys} also uses identity equality, which is
258+ * required because separate instances of the same class may have different generic types.
259+ */
260+ private final ConcurrentMap <DoFn <?, ?>, Constructor <?>> byteBuddyInvokerConstructorInstanceCache =
261+ new MapMaker ().weakKeys ().makeMap ();
262+
245263 private ByteBuddyDoFnInvokerFactory () {}
246264
247265 /** Returns the {@link DoFnInvoker} for the given {@link DoFn}. */
@@ -330,39 +348,15 @@ public <InputT, OutputT> DoFnInvoker<InputT, OutputT> newByteBuddyInvoker(
330348 signature .fnClass (),
331349 fn .getClass ());
332350
333- // Extract input and output type descriptors to distinguish generic instantiations.
334- // Fall back to Object.class if unavailable. When type info is lost, different generic
335- // instantiations share an invoker, which is acceptable since the DoFn class in the cache
336- // key prevents collisions between different DoFn classes.
337- TypeDescriptor <InputT > inputType ;
338- try {
339- inputType = fn .getInputTypeDescriptor ();
340- } catch (Exception e ) {
341- // Some DoFns (like MapElements) throw IllegalStateException if queried after
342- // serialization.
343- // In this case, we fall back to the raw class behavior (Object).
344- inputType = null ;
345- }
346- if (inputType == null ) {
347- inputType = (TypeDescriptor <InputT >) TypeDescriptor .of (Object .class );
348- }
349-
350- TypeDescriptor <OutputT > outputType ;
351- try {
352- outputType = fn .getOutputTypeDescriptor ();
353- } catch (Exception e ) {
354- // Same as above: fall back to Object if type info is unavailable.
355- outputType = null ;
356- }
357- if (outputType == null ) {
358- outputType = (TypeDescriptor <OutputT >) TypeDescriptor .of (Object .class );
359- }
351+ Constructor <?> invokerConstructor =
352+ byteBuddyInvokerConstructorInstanceCache .computeIfAbsent (
353+ fn , unused -> getByteBuddyInvokerConstructor (signature , fn ));
360354
361355 try {
362356 @ SuppressWarnings ("unchecked" )
363357 DoFnInvokerBase <InputT , OutputT , DoFn <InputT , OutputT >> invoker =
364358 (DoFnInvokerBase <InputT , OutputT , DoFn <InputT , OutputT >>)
365- getByteBuddyInvokerConstructor ( signature , inputType , outputType ) .newInstance (fn );
359+ invokerConstructor .newInstance (fn );
366360
367361 if (signature .onTimerMethods () != null ) {
368362 for (OnTimerMethod onTimerMethod : signature .onTimerMethods ().values ()) {
@@ -389,6 +383,39 @@ public <InputT, OutputT> DoFnInvoker<InputT, OutputT> newByteBuddyInvoker(
389383 }
390384 }
391385
386+ private <InputT , OutputT > Constructor <?> getByteBuddyInvokerConstructor (
387+ DoFnSignature signature , DoFn <InputT , OutputT > fn ) {
388+ // Extract input and output type descriptors to distinguish generic instantiations.
389+ // Fall back to Object.class if unavailable. When type info is lost, different generic
390+ // instantiations share an invoker, which is acceptable since the DoFn class in the cache
391+ // key prevents collisions between different DoFn classes.
392+ TypeDescriptor <InputT > inputType ;
393+ try {
394+ inputType = fn .getInputTypeDescriptor ();
395+ } catch (Exception e ) {
396+ // Some DoFns (like MapElements) throw IllegalStateException if queried after
397+ // serialization.
398+ // In this case, we fall back to the raw class behavior (Object).
399+ inputType = null ;
400+ }
401+ if (inputType == null ) {
402+ inputType = (TypeDescriptor <InputT >) TypeDescriptor .of (Object .class );
403+ }
404+
405+ TypeDescriptor <OutputT > outputType ;
406+ try {
407+ outputType = fn .getOutputTypeDescriptor ();
408+ } catch (Exception e ) {
409+ // Same as above: fall back to Object if type info is unavailable.
410+ outputType = null ;
411+ }
412+ if (outputType == null ) {
413+ outputType = (TypeDescriptor <OutputT >) TypeDescriptor .of (Object .class );
414+ }
415+
416+ return getByteBuddyInvokerConstructor (signature , inputType , outputType );
417+ }
418+
392419 /**
393420 * Returns a generated constructor for a {@link DoFnInvoker} for the given {@link DoFnSignature}
394421 * and specific generic types.
0 commit comments