CharlesIrvine

CharlesIrvine

I have just started performance testing for mozart (BPM platform) and I am seeing something I don’t understand and am hoping to get some advice.

I have a GenServer module named ProcessEngine. Instances of this module are spawned via a DynamicSupervisor.

Each ProcessEngine instance spawned is initialized with a data structure representing a defined business process. To clarify, a “business process” is not an Elixir process.

The ProcessEngine instances runs until the “business process” has ran out of work to do, that is, it has finished it’s intended function.

So, here is the issue that I am trying to understand.

If I spawn 1,000 GenServers, they finish execution in about 300,000 microseconds:

iex [09:24 :: 6] > :timer.tc(fn -> run_process_n_times(%{}, :process_with_single_service_task, 1000) end)
{287086, :ok}

If I spawn 10 times that number, i.e. 10,000, they finish execution in 26,602,559, or about 100 times longer than the execution of a 1000 instances.

iex [09:24 :: 8] > :timer.tc(fn -> run_process_n_times(%{}, :process_with_single_service_task, 10000) end)
{26602559, :ok}

So, executing 10 times more GenServer instances takes 100 times the time to complete. I had assumed that execution time would increase linearly with the number of GenServer instances.

If I run the observer, I do see scheduler utilization go to 100% for 2 out of 12 schedulers. It’s always scheduler 1 & 2 that to to 100%. Couple of questions:

Why don’t I see more schedulers become active?
Is the 100% for two schedulers indicative of a problem?

Finally, is there any advice on how to analyze this?

Showing Posts 1 to 10

dimitarvp

dimitarvp

Are your GenServers CPU-bound?

CharlesIrvine

CharlesIrvine OP

Good question, but sorry. How do I determine this? I don’t see this info available in the observer app.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Can you show your “run n times” function?

CharlesIrvine

CharlesIrvine OP

Sure, this file:

https://github.com/CharlesIrvineKC/mozart/blob/main/lib/mozart/performance/demo.ex

the last function in the file.

If anyone cares to run these tests themselves, it wouldn’t require too much work:

Paste to iex:

:observer.start()
import Mozart.Performance.Demo
clear_and_load()
spawn(fn -> :timer.tc(fn -> run_process_n_times(%{}, :process_with_single_service_task, 10000) end) end)

Thanks!

CharlesIrvine

CharlesIrvine OP

One thing that doesn’t look right in the observer System panel is the “Run Queue”. It stays consistently at 2. I would think it would go higher.

CharlesIrvine

CharlesIrvine OP

I discovered that each GenServer was getting shutdown after about one half of a millisecond. I increased the alive time of the GenServer to several seconds. When I did this, all 12 of the schedulers became utilized. I don’t understand this completely, but it kind of makes sense. So, that was one issue.

The second issue is that processing time doesn’t increase linearly with the number of processes serviced. I am still trying to figure that out.

al2o3cr

al2o3cr

One thing that’s not helping concurrency: doing work in init means that start_link takes longer to return. Consider moving the code from ProcessEngine.init to a handle_continue callback.

Another thing that isn’t helping concurrency: init and execute both need to make calls to singleton processes (ProcessModelService and ProcessService)

With a nearly 100x increase based on 10x more input, I’d start by carefully looking at where data’s being collected in the code; all it takes is one List.append that’s called per-ProcessEngine to make things quadratic.

Some other random thoughts:

  • terminate doesn’t do any formatting on the reason argument, so if you call Process.exit(some_pid, :shutdown) (eg) you’ll get :shutdown as an argument - and the ProcessEngine target will crash!

  • harping on the same point: terminate is not guaranteed, there are lots of (admittedly uncommon) scenarios where it will not run before the process disappears. Consider checkpointing the state during intermediate steps, if “resuming” is important.

  • consider extracting type-specific code like this to a per-step (or per-type) “callback module”

CharlesIrvine

CharlesIrvine OP

@al2o3cr

Oh Wow. That is very helpful. I appreciate your effort. I’ll get back to you. Thanks

CharlesIrvine

CharlesIrvine OP

One thing that’s not helping concurrency: doing work in init means that start_link takes longer to return. Consider moving the code from ProcessEngine.init to a handle_continue callback.

I did that.

Another thing that isn’t helping concurrency: init and execute both need to make calls to singleton processes (ProcessModelService and ProcessService)

What you say is true, but why is making calls to those servers an issue? Perhaps you are alluding to it below, but I’m not sure.

With a nearly 100x increase based on 10x more input, I’d start by carefully looking at where data’s being collected in the code; all it takes is one List.append that’s called per-ProcessEngine to make things quadratic.

Some other random thoughts:

I had read that a terminate call isn’t guaranteed. Pity, I thought. Wonder why that is? I’ll need to research what might cause the call to be skipped.

If I did checkpointing it would probably be done frequently and it might be a big hit to performance. Maybe an occasional missed terminate call might be worthwhile.

  • consider extracting type-specific code like this to a per-step (or per-type) “callback module”

That sounds like a sensible thing to to and a good exercise also.

Really appreciate your feedback.

dimitarvp

dimitarvp

Because every OTP process processes messages one by one. If you have 100+ processes each wanting something from that “central” process then that’s an obvious and very major bottleneck.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews