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(); + } + } +}