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]