diff --git a/src/benchmark/Akka.Benchmarks/Actor/ActorPathBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Actor/ActorPathBenchmarks.cs
index 02c72881c98..b9885c1290c 100644
--- a/src/benchmark/Akka.Benchmarks/Actor/ActorPathBenchmarks.cs
+++ b/src/benchmark/Akka.Benchmarks/Actor/ActorPathBenchmarks.cs
@@ -16,12 +16,16 @@ public class ActorPathBenchmarks
{
private ActorPath x;
private ActorPath y;
+ private ActorPath _childPath;
+ private Address _sysAdr = new Address("akka.tcp", "system", "127.0.0.1", 1337);
+ private Address _otherAdr = new Address("akka.tcp", "system", "127.0.0.1", 1338);
[GlobalSetup]
public void Setup()
{
- x = new RootActorPath(new Address("akka.tcp", "system", "127.0.0.1", 1337), "user");
- y = new RootActorPath(new Address("akka.tcp", "system", "127.0.0.1", 1337), "system");
+ x = new RootActorPath(_sysAdr, "user");
+ y = new RootActorPath(_sysAdr, "system");
+ _childPath = x / "parent" / "child";
}
[Benchmark]
@@ -45,7 +49,19 @@ public bool ActorPath_Equals()
[Benchmark]
public string ActorPath_ToString()
{
- return x.ToString();
+ return _childPath.ToString();
+ }
+
+ [Benchmark]
+ public string ActorPath_ToSerializationFormat()
+ {
+ return _childPath.ToSerializationFormat();
+ }
+
+ [Benchmark]
+ public string ActorPath_ToSerializationFormatWithAddress()
+ {
+ return _childPath.ToSerializationFormatWithAddress(_otherAdr);
}
}
}
diff --git a/src/benchmark/Akka.Benchmarks/Actor/NameAndUidBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Actor/NameAndUidBenchmarks.cs
new file mode 100644
index 00000000000..c616f5527b4
--- /dev/null
+++ b/src/benchmark/Akka.Benchmarks/Actor/NameAndUidBenchmarks.cs
@@ -0,0 +1,34 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (C) 2009-2021 Lightbend Inc.
+// Copyright (C) 2013-2021 .NET Foundation
+//
+//-----------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Akka.Actor;
+using Akka.Benchmarks.Configurations;
+using BenchmarkDotNet.Attributes;
+
+namespace Akka.Benchmarks.Actor
+{
+ [Config(typeof(MicroBenchmarkConfig))]
+ public class NameAndUidBenchmarks
+ {
+ public const string ActorPath = "foo#11241311";
+
+ [Benchmark]
+ public NameAndUid ActorCell_SplitNameAndUid()
+ {
+ return ActorCell.SplitNameAndUid(ActorPath);
+ }
+
+ [Benchmark]
+ public (string name, int uid) ActorCell_GetNameAndUid()
+ {
+ return ActorCell.GetNameAndUid(ActorPath);
+ }
+ }
+}
diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt
index 8aee29b1c71..cde266ae92e 100644
--- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt
+++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt
@@ -115,6 +115,7 @@ namespace Akka.Actor
protected bool SetChildrenTerminationReason(Akka.Actor.Internal.SuspendReason reason) { }
public void SetReceiveTimeout(System.Nullable timeout = null) { }
protected void SetTerminated() { }
+ [System.ObsoleteAttribute("Not used. Will be removed in Akka.NET v1.5.")]
public static Akka.Actor.NameAndUid SplitNameAndUid(string name) { }
public virtual void Start() { }
protected void Stash(Akka.Dispatch.SysMsg.SystemMessage msg) { }
@@ -1325,6 +1326,7 @@ namespace Akka.Actor
public override void Suspend() { }
protected override void TellInternal(object message, Akka.Actor.IActorRef sender) { }
}
+ [System.ObsoleteAttribute("Not used. Will be removed in Akka.NET v1.5.")]
public class NameAndUid
{
public NameAndUid(string name, int uid) { }
diff --git a/src/core/Akka.Remote/RemoteSystemDaemon.cs b/src/core/Akka.Remote/RemoteSystemDaemon.cs
index 7393d0aa5b6..1a820b62fb3 100644
--- a/src/core/Akka.Remote/RemoteSystemDaemon.cs
+++ b/src/core/Akka.Remote/RemoteSystemDaemon.cs
@@ -308,10 +308,10 @@ public override IActorRef GetChild(IEnumerable name)
var n = 0;
while (true)
{
- var nameAndUid = ActorCell.SplitNameAndUid(path);
- if (TryGetChild(nameAndUid.Name, out var child))
+ var (s, uid) = ActorCell.GetNameAndUid(path);
+ if (TryGetChild(s, out var child))
{
- if (nameAndUid.Uid != ActorCell.UndefinedUid && nameAndUid.Uid != child.Path.Uid)
+ if (uid != ActorCell.UndefinedUid && uid != child.Path.Uid)
return Nobody.Instance;
return n == 0 ? child : child.GetChild(name.TakeRight(n));
}
diff --git a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs
index f5b0e9b5111..e2a99fde2f3 100644
--- a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs
+++ b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs
@@ -25,7 +25,7 @@ internal static class FastHash
/// A 32-bit pseudo-random hash value.
public static int OfString(string s)
{
- var chars = s.ToCharArray();
+ var chars = s.AsSpan();
var s0 = 391408L; // seed value 1, DON'T CHANGE
var s1 = 601258L; // seed value 2, DON'T CHANGE
unchecked
diff --git a/src/core/Akka.Tests/Actor/ActorSelectionSpec.cs b/src/core/Akka.Tests/Actor/ActorSelectionSpec.cs
index 89a863597a3..49f6152c646 100644
--- a/src/core/Akka.Tests/Actor/ActorSelectionSpec.cs
+++ b/src/core/Akka.Tests/Actor/ActorSelectionSpec.cs
@@ -76,12 +76,10 @@ private IActorRef AskNode(IActorRef node, IQuery query)
{
var result = node.Ask(query).Result;
- var actorRef = result as IActorRef;
- if (actorRef != null)
+ if (result is IActorRef actorRef)
return actorRef;
- var selection = result as ActorSelection;
- return selection != null ? Identify(selection) : null;
+ return result is ActorSelection selection ? Identify(selection) : null;
}
[Fact]
diff --git a/src/core/Akka/Actor/ActorCell.Children.cs b/src/core/Akka/Actor/ActorCell.Children.cs
index cc38364269d..9c69a74df31 100644
--- a/src/core/Akka/Actor/ActorCell.Children.cs
+++ b/src/core/Akka/Actor/ActorCell.Children.cs
@@ -387,17 +387,16 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child)
}
else
{
- var nameAndUid = SplitNameAndUid(name);
- if (TryGetChildRestartStatsByName(nameAndUid.Name, out var stats))
+ var (s, uid) = GetNameAndUid(name);
+ if (TryGetChildRestartStatsByName(s, out var stats))
{
- var uid = nameAndUid.Uid;
if (uid == ActorCell.UndefinedUid || uid == stats.Uid)
{
child = stats.Child;
return true;
}
}
- else if (TryGetFunctionRef(nameAndUid.Name, nameAndUid.Uid, out var functionRef))
+ else if (TryGetFunctionRef(s, uid, out var functionRef))
{
child = functionRef;
return true;
diff --git a/src/core/Akka/Actor/ActorCell.cs b/src/core/Akka/Actor/ActorCell.cs
index 13b5a00e6b7..0894b4707e5 100644
--- a/src/core/Akka/Actor/ActorCell.cs
+++ b/src/core/Akka/Actor/ActorCell.cs
@@ -479,10 +479,11 @@ protected void SetActorFields(ActorBase actor)
actor?.Unclear();
}
///
- /// TBD
+ /// INTERNAL API
///
/// TBD
/// TBD
+ [Obsolete("Not used. Will be removed in Akka.NET v1.5.")]
public static NameAndUid SplitNameAndUid(string name)
{
var i = name.IndexOf('#');
@@ -491,6 +492,19 @@ public static NameAndUid SplitNameAndUid(string name)
: new NameAndUid(name.Substring(0, i), Int32.Parse(name.Substring(i + 1)));
}
+ ///
+ /// INTERNAL API
+ ///
+ /// The full name of the actor, including the UID if known
+ /// A new (string name, int uid) instance.
+ internal static (string name, int uid) GetNameAndUid(string name)
+ {
+ var i = name.IndexOf('#');
+ return i < 0
+ ? (name, UndefinedUid)
+ : (name.Substring(0, i), SpanHacks.Parse(name.AsSpan(i + 1)));
+ }
+
///
/// TBD
///
diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs
index db2a0488988..8491dd1e886 100644
--- a/src/core/Akka/Actor/ActorPath.cs
+++ b/src/core/Akka/Actor/ActorPath.cs
@@ -276,8 +276,8 @@ public bool Equals(ActorPath other)
/// A newly created
public static ActorPath operator /(ActorPath path, string name)
{
- var nameAndUid = ActorCell.SplitNameAndUid(name);
- return new ChildActorPath(path, nameAndUid.Name, nameAndUid.Uid);
+ var (s, uid) = ActorCell.GetNameAndUid(name);
+ return new ChildActorPath(path, s, uid);
}
///
@@ -327,74 +327,173 @@ public static bool TryParse(string path, out ActorPath actorPath)
{
actorPath = null;
+ if (!TryParseAddress(path, out var address, out var absoluteUri)) return false;
+ var spanified = absoluteUri;
- Address address;
- Uri uri;
- if (!TryParseAddress(path, out address, out uri)) return false;
- var pathElements = uri.AbsolutePath.Split('/');
- actorPath = new RootActorPath(address) / pathElements.Skip(1);
- if (uri.Fragment.StartsWith("#"))
+ // check for Uri fragment here
+ var nextSlash = 0;
+
+ actorPath = new RootActorPath(address);
+
+ do
{
- var uid = int.Parse(uri.Fragment.Substring(1));
- actorPath = actorPath.WithUid(uid);
- }
+ nextSlash = spanified.IndexOf('/');
+ if (nextSlash > 0)
+ {
+ actorPath /= spanified.Slice(0, nextSlash).ToString();
+ }
+ else if (nextSlash < 0 && spanified.Length > 0) // final segment
+ {
+ var fragLoc = spanified.IndexOf('#');
+ if (fragLoc > -1)
+ {
+ var fragment = spanified.Slice(fragLoc+1);
+ var fragValue = SpanHacks.Parse(fragment);
+ spanified = spanified.Slice(0, fragLoc);
+ actorPath = new ChildActorPath(actorPath, spanified.ToString(), fragValue);
+ }
+ else
+ {
+ actorPath /= spanified.ToString();
+ }
+
+ }
+
+ spanified = spanified.Slice(nextSlash + 1);
+ } while (nextSlash >= 0);
+
return true;
}
///
- /// TBD
+ /// Attempts to parse an from a stringified .
///
- /// TBD
- /// TBD
- /// TBD
+ /// The string representation of the .
+ /// If true, the parsed . Otherwise null.
+ /// true if the could be parsed, false otherwise.
public static bool TryParseAddress(string path, out Address address)
{
- Uri uri;
- return TryParseAddress(path, out address, out uri);
+ return TryParseAddress(path, out address, out var _);
}
- private static bool TryParseAddress(string path, out Address address, out Uri uri)
+ ///
+ /// Attempts to parse an from a stringified .
+ ///
+ /// The string representation of the .
+ /// If true, the parsed . Otherwise null.
+ /// A containing the path following the address.
+ /// true if the could be parsed, false otherwise.
+ private static bool TryParseAddress(string path, out Address address, out ReadOnlySpan absoluteUri)
{
- //This code corresponds to AddressFromURIString.unapply
address = null;
- if (!Uri.TryCreate(path, UriKind.Absolute, out uri))
+
+ var spanified = path.AsSpan();
+ absoluteUri = spanified;
+
+ var firstColonPos = spanified.IndexOf(':');
+
+ if (firstColonPos == -1) // not an absolute Uri
return false;
- var protocol = uri.Scheme; //Typically "akka"
- if (!protocol.StartsWith("akka", StringComparison.OrdinalIgnoreCase))
- {
- // Protocol must start with 'akka.*
+
+ var fullScheme = SpanHacks.ToLowerInvariant(spanified.Slice(0, firstColonPos));
+ if (!fullScheme.StartsWith("akka"))
+ return false;
+
+ spanified = spanified.Slice(firstColonPos + 1);
+ if (spanified.Length < 2 || !(spanified[0] == '/' && spanified[1] == '/'))
return false;
+
+ spanified = spanified.Slice(2); // move past the double //
+ var firstAtPos = spanified.IndexOf('@');
+ var sysName = string.Empty;
+
+ if (firstAtPos == -1)
+ { // dealing with an absolute local Uri
+ var nextSlash = spanified.IndexOf('/');
+
+ if (nextSlash == -1)
+ {
+ sysName = spanified.ToString();
+ absoluteUri = "/".AsSpan(); // RELY ON THE JIT
+ }
+ else
+ {
+ sysName = spanified.Slice(0, nextSlash).ToString();
+ absoluteUri = spanified.Slice(nextSlash);
+ }
+
+ address = new Address(fullScheme, sysName);
+ return true;
}
+ // dealing with a remote Uri
+ sysName = spanified.Slice(0, firstAtPos).ToString();
+ spanified = spanified.Slice(firstAtPos + 1);
+
+ /*
+ * Need to check for:
+ * - IPV4 / hostnames
+ * - IPV6 (must be surrounded by '[]') according to spec.
+ */
+ var host = string.Empty;
+
+ // check for IPV6 first
+ var openBracket = spanified.IndexOf('[');
+ var closeBracket = spanified.IndexOf(']');
+ if (openBracket > -1 && closeBracket > openBracket)
+ {
+ // found an IPV6 address
+ host = spanified.Slice(openBracket, closeBracket - openBracket + 1).ToString();
+ spanified = spanified.Slice(closeBracket + 1); // advance past the address
- string systemName;
- string host = null;
- int? port = null;
- if (IsNullOrEmpty(uri.UserInfo))
+ // need to check for trailing colon
+ var secondColonPos = spanified.IndexOf(':');
+ if (secondColonPos == -1)
+ return false;
+
+ spanified = spanified.Slice(secondColonPos + 1);
+ }
+ else
{
- // protocol://SystemName/Path1/Path2
- if (uri.Port > 0)
- {
- //port may not be specified for these types of paths
+ var secondColonPos = spanified.IndexOf(':');
+ if (secondColonPos == -1)
return false;
- }
- //System name is in the "host" position. According to rfc3986 host is case
- //insensitive, but should be produced as lowercase, so if we use uri.Host
- //we'll get it in lower case.
- //So we'll extract it ourselves using the original path.
- //We skip the protocol and "://"
- var systemNameLength = uri.Host.Length;
- systemName = path.Substring(protocol.Length + 3, systemNameLength);
+
+ host = spanified.Slice(0, secondColonPos).ToString();
+
+ // move past the host
+ spanified = spanified.Slice(secondColonPos + 1);
+ }
+
+ var actorPathSlash = spanified.IndexOf('/');
+ ReadOnlySpan strPort;
+ if (actorPathSlash == -1)
+ {
+ strPort = spanified;
}
else
{
- // protocol://SystemName@Host:port/Path1/Path2
- systemName = uri.UserInfo;
- host = uri.Host;
- port = uri.Port;
+ strPort = spanified.Slice(0, actorPathSlash);
}
- address = new Address(protocol, systemName, host, port);
- return true;
+
+ if (SpanHacks.TryParse(strPort, out var port))
+ {
+ address = new Address(fullScheme, sysName, host, port);
+
+ // need to compute the absolute path after the Address
+ if (actorPathSlash == -1)
+ {
+ absoluteUri = "/".AsSpan();
+ }
+ else
+ {
+ absoluteUri = spanified.Slice(actorPathSlash);
+ }
+
+ return true;
+ }
+
+ return false;
}
@@ -408,8 +507,8 @@ private string Join()
return "/";
// Resolve length of final string
- int totalLength = 0;
- ActorPath p = this;
+ var totalLength = 0;
+ var p = this;
while (!(p is RootActorPath))
{
totalLength += p.Name.Length + 1;
@@ -424,7 +523,9 @@ private string Join()
{
offset -= p.Name.Length + 1;
buffer[offset] = '/';
+
p.Name.CopyTo(0, buffer, offset + 1, p.Name.Length);
+
p = p.Parent;
}
diff --git a/src/core/Akka/Actor/NameAndUid.cs b/src/core/Akka/Actor/NameAndUid.cs
index 104a45d4a77..fdef5823b4f 100644
--- a/src/core/Akka/Actor/NameAndUid.cs
+++ b/src/core/Akka/Actor/NameAndUid.cs
@@ -5,11 +5,14 @@
//
//-----------------------------------------------------------------------
+using System;
+
namespace Akka.Actor
{
///
- /// TBD
+ /// INTERNAL API
///
+ [Obsolete("Not used. Will be removed in Akka.NET v1.5.")]
public class NameAndUid
{
private readonly string _name;
diff --git a/src/core/Akka/Actor/RepointableActorRef.cs b/src/core/Akka/Actor/RepointableActorRef.cs
index c0e3773a8d9..6933f23da21 100644
--- a/src/core/Akka/Actor/RepointableActorRef.cs
+++ b/src/core/Akka/Actor/RepointableActorRef.cs
@@ -292,12 +292,10 @@ public override IActorRef GetChild(IEnumerable name)
case "":
return ActorRefs.Nobody;
default:
- var nameAndUid = ActorCell.SplitNameAndUid(next);
- if (Lookup.TryGetChildStatsByName(nameAndUid.Name, out var stats))
+ var (s, uid) = ActorCell.GetNameAndUid(next);
+ if (Lookup.TryGetChildStatsByName(s, out var stats))
{
- var crs = stats as ChildRestartStats;
- var uid = nameAndUid.Uid;
- if (crs != null && (uid == ActorCell.UndefinedUid || uid == crs.Uid))
+ if (stats is ChildRestartStats crs && (uid == ActorCell.UndefinedUid || uid == crs.Uid))
{
if (name.Skip(1).Any())
return crs.Child.GetChild(name.Skip(1));
@@ -305,7 +303,7 @@ public override IActorRef GetChild(IEnumerable name)
return crs.Child;
}
}
- else if (Lookup is ActorCell cell && cell.TryGetFunctionRef(nameAndUid.Name, nameAndUid.Uid, out var functionRef))
+ else if (Lookup is ActorCell cell && cell.TryGetFunctionRef(s, uid, out var functionRef))
{
return functionRef;
}
diff --git a/src/core/Akka/Util/SpanHacks.cs b/src/core/Akka/Util/SpanHacks.cs
new file mode 100644
index 00000000000..08464610126
--- /dev/null
+++ b/src/core/Akka/Util/SpanHacks.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Akka.Util
+{
+ ///
+ /// INTERNAL API.
+ ///
+ /// polyfills that should be deleted once we drop .NET Standard 2.0 support.
+ ///
+ internal static class SpanHacks
+ {
+ public static bool IsNumeric(char x)
+ {
+ return (x >= '0' && x <= '9');
+ }
+
+ ///
+ /// Parses an integer from a string.
+ ///
+ ///
+ /// PERFORMS NO INPUT VALIDATION.
+ ///
+ /// The span of input characters.
+ /// An .
+ public static int Parse(ReadOnlySpan str)
+ {
+ if (TryParse(str, out var i))
+ return i;
+ throw new FormatException($"[{str.ToString()}] is now a valid numeric format");
+ }
+
+ ///
+ /// Parses an integer from a string.
+ ///
+ ///
+ /// PERFORMS NO INPUT VALIDATION.
+ ///
+ /// The span of input characters.
+ /// The parsed integer, if any.
+ /// An .
+ public static bool TryParse(ReadOnlySpan str, out int returnValue)
+ {
+ var pos = 0;
+ returnValue = 0;
+ var sign = 1;
+ if (str[0] == '-')
+ {
+ sign = -1;
+ pos++;
+ }
+
+ for (; pos < str.Length; pos++)
+ {
+ if (!IsNumeric(str[pos]))
+ return false;
+ returnValue = returnValue * 10 + str[pos] - '0';
+ }
+
+ returnValue = sign * returnValue;
+
+ return true;
+ }
+
+ ///
+ /// Performs without having to
+ /// allocate a new first.
+ ///
+ /// The set of characters to be lower-cased
+ /// A new string.
+ public static string ToLowerInvariant(ReadOnlySpan input)
+ {
+ Span output = stackalloc char[input.Length];
+ for (var i = 0; i < input.Length; i++)
+ {
+ output[i] = char.ToLowerInvariant(input[i]);
+ }
+ return output.ToString();
+ }
+ }
+}