This is an automated email from the ASF dual-hosted git repository.

fanningpj pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new f193fddf93 Optimize PropsAdapter + add test coverage (#2743)
f193fddf93 is described below

commit f193fddf936e0456b814d95340033aeacdf7c367
Author: PJ Fanning <[email protected]>
AuthorDate: Tue Mar 17 13:48:03 2026 +0100

    Optimize PropsAdapter + add test coverage (#2743)
    
    * Initial plan
    
    * Optimize PropsAdapter to avoid unnecessary object creation (port 
akka-core#31695)
    
    Co-authored-by: pjfanning <[email protected]>
    
    * Add PropsAdapterSpec test coverage from original akka-core#31695
    
    Co-authored-by: pjfanning <[email protected]>
    
    * Port remaining file changes from akka-core#31695
    
    Co-authored-by: pjfanning <[email protected]>
    
    * scalafmt
    
    ---------
    
    Co-authored-by: copilot-swe-agent[bot] 
<[email protected]>
    Co-authored-by: pjfanning <[email protected]>
---
 .../typed/internal/adpater/PropsAdapterSpec.scala  | 33 +++++++++++-
 .../org/apache/pekko/actor/typed/Behavior.scala    |  8 +++
 .../pekko/actor/typed/internal/CachedProps.scala   | 26 +++++++++
 .../typed/internal/adapter/ActorAdapter.scala      |  2 +-
 .../internal/adapter/ActorContextAdapter.scala     | 12 ++++-
 .../internal/adapter/ActorRefFactoryAdapter.scala  | 24 +++++++--
 .../typed/internal/adapter/PropsAdapter.scala      | 62 +++++++++++++---------
 7 files changed, 135 insertions(+), 32 deletions(-)

diff --git 
a/actor-typed-tests/src/test/scala/org/apache/pekko/actor/typed/internal/adpater/PropsAdapterSpec.scala
 
b/actor-typed-tests/src/test/scala/org/apache/pekko/actor/typed/internal/adpater/PropsAdapterSpec.scala
index 659fe51e1c..bd5cccfef0 100644
--- 
a/actor-typed-tests/src/test/scala/org/apache/pekko/actor/typed/internal/adpater/PropsAdapterSpec.scala
+++ 
b/actor-typed-tests/src/test/scala/org/apache/pekko/actor/typed/internal/adpater/PropsAdapterSpec.scala
@@ -15,6 +15,8 @@ package org.apache.pekko.actor.typed.internal.adpater
 
 import org.apache.pekko
 import pekko.actor
+import pekko.actor.typed.ActorTags
+import pekko.actor.typed.MailboxSelector
 import pekko.actor.typed.Props
 import pekko.actor.typed.internal.adapter.PropsAdapter
 import pekko.actor.typed.scaladsl.Behaviors
@@ -28,7 +30,36 @@ class PropsAdapterSpec extends AnyWordSpec with Matchers {
     "default to org.apache.pekko.dispatch.SingleConsumerOnlyUnboundedMailbox" 
in {
       val props: Props = Props.empty
       val pa: actor.Props = PropsAdapter(() => Behaviors.empty, props, 
rethrowTypedFailure = false)
-      pa.mailbox shouldEqual "pekko.actor.typed.default-mailbox"
+      pa.mailbox should ===("pekko.actor.typed.default-mailbox")
+
+      val props2: Props = MailboxSelector.defaultMailbox()
+      val pa2: actor.Props = PropsAdapter(() => Behaviors.empty, props2, 
rethrowTypedFailure = false)
+      pa2.mailbox should ===("pekko.actor.typed.default-mailbox")
+    }
+    "adapt dispatcher from config" in {
+      val props: Props = Props.empty.withDispatcherFromConfig("some.path")
+      val pa: actor.Props = PropsAdapter(() => Behaviors.empty, props, 
rethrowTypedFailure = false)
+      pa.dispatcher should ===("some.path")
+    }
+    "adapt dispatcher same as parent" in {
+      val props: Props = Props.empty.withDispatcherSameAsParent
+      val pa: actor.Props = PropsAdapter(() => Behaviors.empty, props, 
rethrowTypedFailure = false)
+      pa.dispatcher should ===("..")
+    }
+    "adapt mailbox from config" in {
+      val props: Props = MailboxSelector.fromConfig("some.path")
+      val pa: actor.Props = PropsAdapter(() => Behaviors.empty, props, 
rethrowTypedFailure = false)
+      pa.mailbox should ===("some.path")
+    }
+    "adapt bounded mailbox" in {
+      val props: Props = MailboxSelector.bounded(24)
+      val pa: actor.Props = PropsAdapter(() => Behaviors.empty, props, 
rethrowTypedFailure = false)
+      pa.mailbox should ===("bounded-capacity:24")
+    }
+    "adapt tags" in {
+      val props: Props = ActorTags.create("my-tag")
+      val pa: actor.Props = PropsAdapter(() => Behaviors.empty, props, 
rethrowTypedFailure = false)
+      pa.deploy.tags should ===(Set("my-tag"))
     }
   }
 }
diff --git 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/Behavior.scala 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/Behavior.scala
index c9661cd6fa..1c9cbcad67 100644
--- a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/Behavior.scala
+++ b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/Behavior.scala
@@ -22,8 +22,10 @@ import pekko.actor.InvalidMessageException
 import pekko.actor.typed.internal.{ BehaviorImpl, BehaviorTags, 
InterceptorImpl, Supervisor }
 import pekko.actor.typed.internal.BehaviorImpl.DeferredBehavior
 import pekko.actor.typed.internal.BehaviorImpl.StoppedBehavior
+import pekko.actor.typed.internal.CachedProps
 import pekko.annotation.DoNotInherit
 import pekko.annotation.InternalApi
+import pekko.util.OptionVal
 
 /**
  * The behavior of an actor defines how it reacts to the messages that it
@@ -48,6 +50,12 @@ import pekko.annotation.InternalApi
 @DoNotInherit
 abstract class Behavior[T](private[pekko] val _tag: Int) { behavior =>
 
+  /**
+   * INTERNAL API
+   */
+  @InternalApi
+  @volatile private[pekko] var _internalClassicPropsCache: 
OptionVal[CachedProps] = OptionVal.None
+
   /**
    * Narrow the type of this Behavior, which is always a safe operation. This
    * method is necessary to implement the contravariant nature of Behavior
diff --git 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/CachedProps.scala
 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/CachedProps.scala
new file mode 100644
index 0000000000..478f4c77f3
--- /dev/null
+++ 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/CachedProps.scala
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package org.apache.pekko.actor.typed.internal
+
+import org.apache.pekko.actor.typed.Props
+import org.apache.pekko.annotation.InternalApi
+
+/**
+ * INTERNAL API
+ */
+@InternalApi
+final private[pekko] case class CachedProps(
+    typedProps: Props,
+    adaptedProps: org.apache.pekko.actor.Props,
+    rethrowTypedFailure: Boolean)
diff --git 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorAdapter.scala
 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorAdapter.scala
index 3800116a76..da1604cd3b 100644
--- 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorAdapter.scala
+++ 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorAdapter.scala
@@ -229,7 +229,7 @@ import pekko.util.OptionVal
       super.unhandled(other)
   }
 
-  override val supervisorStrategy = classic.OneForOneStrategy(loggingEnabled = 
false) {
+  final override def supervisorStrategy = 
classic.OneForOneStrategy(loggingEnabled = false) {
     case ex =>
       ctx.setCurrentActorThread()
       try ex match {
diff --git 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorContextAdapter.scala
 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorContextAdapter.scala
index bb438c7bb3..018d5c1e72 100644
--- 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorContextAdapter.scala
+++ 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorContextAdapter.scala
@@ -21,6 +21,7 @@ import scala.concurrent.duration._
 import org.apache.pekko
 import pekko.{ actor => classic }
 import pekko.annotation.InternalApi
+import pekko.util.OptionVal
 
 @InternalApi
 private[pekko] object ActorContextAdapter {
@@ -53,7 +54,16 @@ private[pekko] object ActorContextAdapter {
 
   private[pekko] override def currentBehavior: Behavior[T] = 
adapter.currentBehavior
 
-  final override val self = ActorRefAdapter(classicContext.self)
+  // optimization to avoid adapter allocation unless used
+  // self documented as thread safe, on purpose not volatile since
+  // lazily created because creating adapter is idempotent
+  private var _self: OptionVal[ActorRef[T]] = OptionVal.None
+  override def self: ActorRef[T] = {
+    if (_self.isEmpty) {
+      _self = OptionVal.Some(ActorRefAdapter(classicContext.self))
+    }
+    _self.get
+  }
   final override val system = ActorSystemAdapter(classicContext.system)
   private[pekko] def classicActorContext = classicContext
   override def children: Iterable[ActorRef[Nothing]] = {
diff --git 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorRefFactoryAdapter.scala
 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorRefFactoryAdapter.scala
index 1d47ef10f8..4611fd9243 100644
--- 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorRefFactoryAdapter.scala
+++ 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/ActorRefFactoryAdapter.scala
@@ -16,8 +16,10 @@ package org.apache.pekko.actor.typed.internal.adapter
 import org.apache.pekko
 import pekko.ConfigurationException
 import pekko.actor.typed._
+import pekko.actor.typed.internal.CachedProps
 import pekko.annotation.InternalApi
 import pekko.util.ErrorMessages
+import pekko.util.OptionVal
 
 /**
  * INTERNAL API
@@ -25,13 +27,28 @@ import pekko.util.ErrorMessages
 @InternalApi private[typed] object ActorRefFactoryAdapter {
 
   private val remoteDeploymentNotAllowed = "Remote deployment not allowed for 
typed actors"
+
+  private def classicPropsFor[T](behavior: Behavior[T], props: Props, 
rethrowTypedFailure: Boolean): pekko.actor.Props =
+    behavior._internalClassicPropsCache match {
+      case OptionVal.Some(cachedProps)
+          if (cachedProps.typedProps eq props) && 
cachedProps.rethrowTypedFailure == rethrowTypedFailure =>
+        cachedProps.adaptedProps
+      case _ =>
+        val adapted =
+          internal.adapter.PropsAdapter(() => 
Behavior.validateAsInitial(behavior), props, rethrowTypedFailure)
+        // we only optimistically cache the last seen typed props instance, 
since for most scenarios
+        // with large numbers of actors, they will be of the same type and the 
same props
+        behavior._internalClassicPropsCache = 
OptionVal.Some(CachedProps(props, adapted, rethrowTypedFailure))
+        adapted
+    }
+
   def spawnAnonymous[T](
       context: pekko.actor.ActorRefFactory,
       behavior: Behavior[T],
       props: Props,
       rethrowTypedFailure: Boolean): ActorRef[T] = {
     try {
-      ActorRefAdapter(context.actorOf(internal.adapter.PropsAdapter(() => 
behavior, props, rethrowTypedFailure)))
+      ActorRefAdapter(context.actorOf(classicPropsFor(behavior, props, 
rethrowTypedFailure)))
     } catch {
       case ex: ConfigurationException if 
ex.getMessage.startsWith(ErrorMessages.RemoteDeploymentConfigErrorPrefix) =>
         throw new ConfigurationException(remoteDeploymentNotAllowed, ex)
@@ -45,10 +62,7 @@ import pekko.util.ErrorMessages
       props: Props,
       rethrowTypedFailure: Boolean): ActorRef[T] = {
     try {
-      ActorRefAdapter(
-        actorRefFactory.actorOf(
-          internal.adapter.PropsAdapter(() => 
Behavior.validateAsInitial(behavior), props, rethrowTypedFailure),
-          name))
+      ActorRefAdapter(actorRefFactory.actorOf(classicPropsFor(behavior, props, 
rethrowTypedFailure), name))
     } catch {
       case ex: ConfigurationException if 
ex.getMessage.startsWith(ErrorMessages.RemoteDeploymentConfigErrorPrefix) =>
         throw new ConfigurationException(remoteDeploymentNotAllowed, ex)
diff --git 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/PropsAdapter.scala
 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/PropsAdapter.scala
index 4d8a814bbb..40e44ef921 100644
--- 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/PropsAdapter.scala
+++ 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/adapter/PropsAdapter.scala
@@ -15,6 +15,8 @@ package org.apache.pekko.actor.typed.internal.adapter
 
 import org.apache.pekko
 import pekko.actor.Deploy
+import pekko.actor.LocalScope
+import pekko.actor.TypedCreatorFunctionConsumer
 import pekko.actor.typed.ActorTags
 import pekko.actor.typed.Behavior
 import pekko.actor.typed.DispatcherSelector
@@ -28,31 +30,43 @@ import pekko.dispatch.Mailboxes
  * INTERNAL API
  */
 @InternalApi private[pekko] object PropsAdapter {
+  private final val TypedCreatorFunctionConsumerClazz = 
classOf[TypedCreatorFunctionConsumer]
+  private final val DefaultTypedDeploy = Deploy.local.copy(mailbox = 
"pekko.actor.typed.default-mailbox")
+
   def apply[T](behavior: () => Behavior[T], props: Props, rethrowTypedFailure: 
Boolean): pekko.actor.Props = {
-    val classicProps = pekko.actor.Props(new ActorAdapter(behavior(), 
rethrowTypedFailure))
-
-    val dispatcherProps = 
(props.firstOrElse[DispatcherSelector](DispatcherDefault.empty) match {
-      case _: DispatcherDefault          => classicProps
-      case DispatcherFromConfig(name, _) => classicProps.withDispatcher(name)
-      case _: DispatcherSameAsParent     => 
classicProps.withDispatcher(Deploy.DispatcherSameAsParent)
-      case unknown                       => throw new 
RuntimeException(s"Unsupported dispatcher selector: $unknown")
-    }).withDeploy(Deploy.local) // disallow remote deployment for typed actors
-
-    val mailboxProps = 
props.firstOrElse[MailboxSelector](MailboxSelector.default()) match {
-      case _: DefaultMailboxSelector           => dispatcherProps
-      case BoundedMailboxSelector(capacity, _) =>
-        // specific support in classic Mailboxes
-        
dispatcherProps.withMailbox(s"${Mailboxes.BoundedCapacityPrefix}$capacity")
-      case MailboxFromConfigSelector(path, _) =>
-        dispatcherProps.withMailbox(path)
-      case unknown => throw new RuntimeException(s"Unsupported mailbox 
selector: $unknown")
-    }
-
-    val localDeploy = mailboxProps.withDeploy(Deploy.local) // disallow remote 
deployment for typed actors
-
-    val tags = props.firstOrElse[ActorTags](ActorTagsImpl.empty).tags
-    if (tags.isEmpty) localDeploy
-    else localDeploy.withActorTags(tags)
+    val deploy =
+      if (props eq Props.empty) DefaultTypedDeploy // optimized case with no 
props specified
+      else {
+        val deployWithMailbox = 
props.firstOrElse[MailboxSelector](MailboxSelector.default()) match {
+          case _: DefaultMailboxSelector           => DefaultTypedDeploy
+          case BoundedMailboxSelector(capacity, _) =>
+            // specific support in classic Mailboxes
+            DefaultTypedDeploy.copy(mailbox = 
s"${Mailboxes.BoundedCapacityPrefix}$capacity")
+          case MailboxFromConfigSelector(path, _) => 
DefaultTypedDeploy.copy(mailbox = path)
+          case unknown                            => throw new 
RuntimeException(s"Unsupported mailbox selector: $unknown")
+        }
+
+        val deployWithDispatcher = 
props.firstOrElse[DispatcherSelector](DispatcherDefault.empty) match {
+          case _: DispatcherDefault          => deployWithMailbox
+          case DispatcherFromConfig(name, _) => 
deployWithMailbox.copy(dispatcher = name)
+          case _: DispatcherSameAsParent     => 
deployWithMailbox.copy(dispatcher = Deploy.DispatcherSameAsParent)
+          case unknown                       => throw new 
RuntimeException(s"Unsupported dispatcher selector: $unknown")
+        }
+
+        val tags = props.firstOrElse[ActorTags](ActorTagsImpl.empty).tags
+        val deployWithTags =
+          if (tags.isEmpty) deployWithDispatcher else 
deployWithDispatcher.withTags(tags)
+
+        if (deployWithTags.scope != LocalScope) // only replace if changed, 
withDeploy is expensive
+          deployWithTags.copy(scope = Deploy.local.scope) // disallow remote 
deployment for typed actors
+        else deployWithTags
+      }
+
+    // avoid the apply methods and also avoid copying props, for performance 
reasons
+    new pekko.actor.Props(
+      deploy,
+      TypedCreatorFunctionConsumerClazz,
+      classOf[ActorAdapter[_]] :: (() => new ActorAdapter(behavior(), 
rethrowTypedFailure)) :: Nil)
   }
 
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to