user1691896
user1691896

Reputation: 107

My Akka.Net Demo is incredibly slow

I am trying to get a proof of concept running with akka.net. I am sure that I am doing something terribly wrong, but I can't figure out what it is.

I want my actors to form a graph of nodes. Later, this will be a complex graph of business objekts, but for now I want to try a simple linear structure like this:

enter image description here

I want to ask a node for a neighbour that is 9 steps away. I am trying to implement this in a recursive manner. I ask node #9 for a neighbour that is 9 steps away, then I ask node #8 for a neighbour that is 8 steps away and so on. Finally, this should return node #0 as an answer.

Well, my code works, but it takes more than 4 seconds to execute. Why is that?

This is my full code listing:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Akka;
using Akka.Actor;

namespace AkkaTest
{
    class Program
    {
        public static Stopwatch stopwatch = new Stopwatch();
        static void Main(string[] args)
        {
            var system = ActorSystem.Create("MySystem");

            IActorRef[] current = new IActorRef[0];

            Console.WriteLine("Initializing actors...");

            for (int i = 0; i < 10; i++)
            {
                var current1 = current;
                var props = Props.Create<Obj>(() => new Obj(current1, Guid.NewGuid()));
                var actorRef = system.ActorOf(props, i.ToString());
                current = new[] { actorRef };
            }
            Console.WriteLine("actors initialized.");

            FindNeighboursRequest r = new FindNeighboursRequest(9);

            stopwatch.Start();

            var response = current[0].Ask(r);
            FindNeighboursResponse result = (FindNeighboursResponse)response.Result;
            stopwatch.Stop();
            foreach (var d in result.FoundNeighbours)
            {
                Console.WriteLine(d);
            }

            Console.WriteLine("Search took " + stopwatch.ElapsedMilliseconds + "ms.");
            Console.ReadLine();
        }
    }
    public class FindNeighboursRequest
    {
        public FindNeighboursRequest(int distance)
        {
            this.Distance = distance;
        }
        public int Distance { get; private set; }
    }

    public class FindNeighboursResponse
    {
        private IActorRef[] foundNeighbours;

        public FindNeighboursResponse(IEnumerable<IActorRef> descendants)
        {
            this.foundNeighbours = descendants.ToArray();
        }

        public IActorRef[] FoundNeighbours
        {
            get { return this.foundNeighbours; }
        }
    }


    public class Obj : ReceiveActor
    {
        private Guid objGuid;
        readonly List<IActorRef> neighbours = new List<IActorRef>();
        public Obj(IEnumerable<IActorRef> otherObjs, Guid objGuid)
        {
            this.neighbours.AddRange(otherObjs);
            this.objGuid = objGuid;
            Receive<FindNeighboursRequest>(r => handleFindNeighbourRequest(r));
        }

        public Obj()
        {
        }

        private async void handleFindNeighbourRequest (FindNeighboursRequest r)
        {
            if (r.Distance == 0)
            {
                FindNeighboursResponse response = new FindNeighboursResponse(new IActorRef[] { Self });
                Sender.Tell(response, Self);
                return;
            }

            List<FindNeighboursResponse> responses = new List<FindNeighboursResponse>();

            foreach (var actorRef in neighbours)
            {
                FindNeighboursRequest req = new FindNeighboursRequest(r.Distance - 1);
                var response2 = actorRef.Ask(req);
                responses.Add((FindNeighboursResponse)response2.Result);
            }

            FindNeighboursResponse response3 = new FindNeighboursResponse(responses.SelectMany(rx => rx.FoundNeighbours));
            Sender.Tell(response3, Self);
        }
    }
}

Upvotes: 2

Views: 581

Answers (1)

Bartosz Sypytkowski
Bartosz Sypytkowski

Reputation: 7542

The reason of such slow behavior is the way you use Ask (an that you use it, but I'll cover this later). In your example, you're asking each neighbor in a loop, and then immediately executing response2.Result which is actively blocking current actor (and thread it resides on). So you're essentially making synchronous flow with blocking.

The easiest thing to fix that, is to collect all tasks returned from Ask and use Task.WhenAll to collect them all, without waiting for each one in a loop. Taking this example:

public class Obj : ReceiveActor
{
    private readonly IActorRef[] _neighbours;
    private readonly Guid _id;

    public Obj(IActorRef[] neighbours, Guid id)
    {
        _neighbours = neighbours;
        _id = id;
        Receive<FindNeighboursRequest>(async r =>
        {
            if (r.Distance == 0) Sender.Tell(new FindNeighboursResponse(new[] {Self}));
            else
            {
                var request = new FindNeighboursRequest(r.Distance - 1);
                var replies = _neighbours.Select(neighbour => neighbour.Ask<FindNeighboursResponse>(request));
                var ready = await Task.WhenAll(replies);
                var responses = ready.SelectMany(x => x.FoundNeighbours);
                Sender.Tell(new FindNeighboursResponse(responses.ToArray()));
            }
        });
    }
}

This one is much faster.

NOTE: In general you shouldn't use Ask inside of an actor:

  1. Each ask is allocating a listener inside current actor, so in general using Ask is A LOT heavier than passing messages with Tell.
  2. When sending messages through chain of actors, cost of ask is additionally transporting message twice (one for request and one for reply) through each actor. One of the popular patterns is that, when you are sending request from A⇒B⇒C⇒D and respond from D back to A, you can reply directly D⇒A, without need of passing the message through whole chain back. Usually combination of Forward/Tell works better.
  3. In general don't use async version of Receive if it's not necessary - at the moment, it's slower for an actor when compared to sync version.

Upvotes: 5

Related Questions