Skip to content

Commit 3f232d8

Browse files
Perf optimize ActorSelection (#4962)
* added memory metrics to `ActorSelection` benchmarks * added ActorSelection benchmark * ramped up the iteration counts * validate that double wildcard can't be used outside of leaf node * improve allocations on create * minor cleanup * create emptyRef only when needed via local function * made `Iterator` into `struct` * approved public API changes
1 parent 8d03165 commit 3f232d8

File tree

6 files changed

+103
-15
lines changed

6 files changed

+103
-15
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net.NetworkInformation;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Akka.Actor;
7+
using Akka.Benchmarks.Configurations;
8+
using BenchmarkDotNet.Attributes;
9+
using BenchmarkDotNet.Engines;
10+
11+
namespace Akka.Benchmarks.Actor
12+
{
13+
[Config(typeof(MicroBenchmarkConfig))] // need memory diagnosis
14+
public class ActorSelectionBenchmark
15+
{
16+
[Params(10000)]
17+
public int Iterations { get; set; }
18+
private TimeSpan _timeout;
19+
private ActorSystem _system;
20+
private IActorRef _echo;
21+
22+
// cached selection for measuring .Tell / .Ask performance
23+
private ActorSelection _actorSelection;
24+
25+
[GlobalSetup]
26+
public void Setup()
27+
{
28+
_timeout = TimeSpan.FromMinutes(1);
29+
_system = ActorSystem.Create("system");
30+
_echo = _system.ActorOf(Props.Create(() => new EchoActor()), "echo");
31+
_actorSelection = _system.ActorSelection("/user/echo");
32+
}
33+
34+
[Benchmark]
35+
public async Task RequestResponseActorSelection()
36+
{
37+
for(var i = 0; i < Iterations; i++)
38+
await _actorSelection.Ask("foo", _timeout);
39+
}
40+
41+
[Benchmark]
42+
public void CreateActorSelection()
43+
{
44+
for (var i = 0; i < Iterations; i++)
45+
_system.ActorSelection("/user/echo");
46+
}
47+
48+
[GlobalCleanup]
49+
public void Cleanup()
50+
{
51+
_system.Terminate().Wait();
52+
}
53+
54+
public class EchoActor : UntypedActor
55+
{
56+
protected override void OnReceive(object message)
57+
{
58+
Sender.Tell(message);
59+
}
60+
}
61+
}
62+
}

src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,13 +1621,15 @@ namespace Akka.Actor
16211621
}
16221622
public class SelectChildRecursive : Akka.Actor.SelectionPathElement
16231623
{
1624+
public static readonly Akka.Actor.SelectChildRecursive Instance;
16241625
public SelectChildRecursive() { }
16251626
public override bool Equals(object obj) { }
16261627
public override int GetHashCode() { }
16271628
public override string ToString() { }
16281629
}
16291630
public class SelectParent : Akka.Actor.SelectionPathElement
16301631
{
1632+
public static readonly Akka.Actor.SelectParent Instance;
16311633
public SelectParent() { }
16321634
public override bool Equals(object obj) { }
16331635
public override int GetHashCode() { }

src/core/Akka.Tests.Performance/Actor/ActorSelectionSpecs.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public void Setup(BenchmarkContext context)
6666
[PerfBenchmark(Description = "Tests the message delivery throughput of NEW ActorSelections to NEW actors",
6767
NumberOfIterations = 13, RunMode = RunMode.Throughput, RunTimeMilliseconds = 1000, TestMode = TestMode.Measurement)]
6868
[CounterMeasurement(ActorSelectionCounterName)]
69+
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
6970
public void New_ActorSelection_on_new_actor_throughput(BenchmarkContext context)
7071
{
7172
var actorRef = System.ActorOf(_oneMessageBenchmarkProps); // create a new actor every time
@@ -77,6 +78,7 @@ public void New_ActorSelection_on_new_actor_throughput(BenchmarkContext context)
7778
[PerfBenchmark(Description = "Tests the message delivery throughput of REUSABLE ActorSelections to PRE-EXISTING actors",
7879
NumberOfIterations = 13, RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
7980
[CounterMeasurement(ActorSelectionCounterName)]
81+
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
8082
public void Reused_ActorSelection_on_pre_existing_actor_throughput(BenchmarkContext context)
8183
{
8284
var actorSelection = System.ActorSelection(_receiverActorPath);
@@ -91,6 +93,7 @@ public void Reused_ActorSelection_on_pre_existing_actor_throughput(BenchmarkCont
9193
[PerfBenchmark(Description = "Tests the message delivery throughput of NEW ActorSelections to PRE-EXISTING actors. This is really a stress test.",
9294
NumberOfIterations = 13, RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
9395
[CounterMeasurement(ActorSelectionCounterName)]
96+
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
9497
public void New_ActorSelection_on_pre_existing_actor_throughput(BenchmarkContext context)
9598
{
9699
for (var i = 0; i < NumberOfMessages;)
@@ -104,6 +107,7 @@ public void New_ActorSelection_on_pre_existing_actor_throughput(BenchmarkContext
104107
[PerfBenchmark(Description = "Tests the throughput of resolving an ActorSelection on a pre-existing actor via ResolveOne",
105108
NumberOfIterations = 13, RunMode = RunMode.Throughput, RunTimeMilliseconds = 1000, TestMode = TestMode.Measurement)]
106109
[CounterMeasurement(ActorSelectionCounterName)]
110+
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
107111
public void ActorSelection_ResolveOne_throughput(BenchmarkContext context)
108112
{
109113
var actorRef= System.ActorSelection(_receiverActorPath).ResolveOne(TimeSpan.FromSeconds(2)).Result; // send that actor a message via selection
@@ -113,6 +117,7 @@ public void ActorSelection_ResolveOne_throughput(BenchmarkContext context)
113117
[PerfBenchmark(Description = "Continuously creates actors and attempts to resolve them immediately. Used to surface race conditions.",
114118
NumberOfIterations = 13, RunMode = RunMode.Throughput, RunTimeMilliseconds = 1000, TestMode = TestMode.Measurement)]
115119
[CounterMeasurement(ActorSelectionCounterName)]
120+
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
116121
public void ActorSelection_ResolveOne_stress_test(BenchmarkContext context)
117122
{
118123
var actorRef = System.ActorOf(_oneMessageBenchmarkProps); // create a new actor every time

src/core/Akka.Tests/Actor/ActorSelectionSpec.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,9 @@ public void An_ActorSelection_must_identify_actors_with_double_wildcard_selectio
486486
// nothing under /user/a/b2/c1/d
487487
Sys.ActorSelection("/user/a/b2/c1/d/**").Tell(new Identify(3), probe.Ref);
488488
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
489+
490+
Action illegalDoubleWildCard = () => Sys.ActorSelection("/user/a/**/d").Tell(new Identify(4), probe.Ref);
491+
illegalDoubleWildCard.Should().Throw<IllegalActorNameException>();
489492
}
490493

491494
[Fact]

src/core/Akka/Actor/ActorSelection.cs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,31 +77,34 @@ public ActorSelection(IActorRef anchor, IEnumerable<string> elements)
7777
Anchor = anchor;
7878

7979
var list = new List<SelectionPathElement>();
80-
var iter = elements.Iterator();
81-
while (!iter.IsEmpty())
80+
var count = elements.Count(); // shouldn't have a multiple enumeration issue\
81+
var i = 0;
82+
foreach (var s in elements)
8283
{
83-
var s = iter.Next();
8484
switch (s)
8585
{
8686
case null:
8787
case "":
8888
break;
8989
case "**":
90-
if (!iter.IsEmpty())
90+
if (i < count-1)
9191
throw new IllegalActorNameException("Double wildcard can only appear at the last path entry");
92-
list.Add(new SelectChildRecursive());
92+
list.Add(SelectChildRecursive.Instance);
9393
break;
9494
case string e when e.Contains("?") || e.Contains("*"):
9595
list.Add(new SelectChildPattern(e));
9696
break;
9797
case string e when e == "..":
98-
list.Add(new SelectParent());
98+
list.Add(SelectParent.Instance);
9999
break;
100100
default:
101101
list.Add(new SelectChildName(s));
102102
break;
103103
}
104+
105+
i++;
104106
}
107+
105108
Path = list.ToArray();
106109
}
107110

@@ -194,10 +197,12 @@ void Rec(IInternalActorRef actorRef)
194197
{
195198
if (actorRef is ActorRefWithCell refWithCell)
196199
{
197-
var emptyRef = new EmptyLocalActorRef(
198-
provider: refWithCell.Provider,
199-
path: anchor.Path / sel.Elements.Select(el => el.ToString()),
200-
eventStream: refWithCell.Underlying.System.EventStream);
200+
EmptyLocalActorRef EmptyRef(){
201+
return new EmptyLocalActorRef(
202+
provider: refWithCell.Provider,
203+
path: anchor.Path / sel.Elements.Select(el => el.ToString()),
204+
eventStream: refWithCell.Underlying.System.EventStream);
205+
}
201206

202207
switch (iter.Next())
203208
{
@@ -217,7 +222,7 @@ void Rec(IInternalActorRef actorRef)
217222
{
218223
// don't send to emptyRef after wildcard fan-out
219224
if (!sel.WildCardFanOut)
220-
emptyRef.Tell(sel, sender);
225+
EmptyRef().Tell(sel, sender);
221226
}
222227
else if (iter.IsEmpty())
223228
{
@@ -234,7 +239,7 @@ void Rec(IInternalActorRef actorRef)
234239
if (allChildren.Count == 0)
235240
return;
236241

237-
var msg = new ActorSelectionMessage(sel.Message, new[] { new SelectChildRecursive() }, true);
242+
var msg = new ActorSelectionMessage(sel.Message, new SelectionPathElement[] { SelectChildRecursive.Instance }, true);
238243
foreach (var c in allChildren)
239244
{
240245
c.Tell(sel.Message, sender);
@@ -250,7 +255,7 @@ void Rec(IInternalActorRef actorRef)
250255
if (iter.IsEmpty())
251256
{
252257
if (matchingChildren.Count == 0 && !sel.WildCardFanOut)
253-
emptyRef.Tell(sel, sender);
258+
EmptyRef().Tell(sel, sender);
254259
else
255260
{
256261
for (var i = 0; i < matchingChildren.Count; i++)
@@ -261,7 +266,7 @@ void Rec(IInternalActorRef actorRef)
261266
{
262267
// don't send to emptyRef after wildcard fan-out
263268
if (matchingChildren.Count == 0 && !sel.WildCardFanOut)
264-
emptyRef.Tell(sel, sender);
269+
EmptyRef().Tell(sel, sender);
265270
else
266271
{
267272
var message = new ActorSelectionMessage(
@@ -474,6 +479,11 @@ public override bool Equals(object obj)
474479
return true;
475480
}
476481

482+
/// <summary>
483+
/// Use this instead of calling the default constructor
484+
/// </summary>
485+
public static readonly SelectChildRecursive Instance = new SelectChildRecursive();
486+
477487
/// <inheritdoc/>
478488
public override int GetHashCode() => "**".GetHashCode();
479489

@@ -487,6 +497,11 @@ public override bool Equals(object obj)
487497
/// </summary>
488498
public class SelectParent : SelectionPathElement
489499
{
500+
/// <summary>
501+
/// Use this instead of calling the default constructor
502+
/// </summary>
503+
public static readonly SelectParent Instance = new SelectParent();
504+
490505
/// <inheritdoc/>
491506
public override bool Equals(object obj) => !ReferenceEquals(obj, null) && obj is SelectParent;
492507

src/core/Akka/Util/Internal/Collections/Iterator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010

1111
namespace Akka.Util.Internal.Collections
1212
{
13-
internal sealed class Iterator<T>
13+
internal struct Iterator<T>
1414
{
1515
private readonly IList<T> _enumerator;
1616
private int _index;
1717

1818
public Iterator(IEnumerable<T> enumerator)
1919
{
20+
_index = 0;
2021
_enumerator = enumerator.ToList();
2122
}
2223

0 commit comments

Comments
 (0)