Why recon reports 2 schedulers even though BEAM was supposedly started with 1?

I’ve explicitly set number of schedulers to 1 on IEX startup:

iex --erl "+S 1" -S mix
Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1] [jit:ns]

Interactive Elixir (1.16.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

However, when I try checking scheduler usage via recon library, it reports two schedulers:

iex(1)> :recon.scheduler_usage(1000)
[{1, 7.620530725717925e-4}, {2, 0.0}]

Why is that? Where does this second scheduler come from?

It’s reporting on the number of total schedulers that were active during the 1000ms timespan, including dirty_cpu_schedulers.

:recon.scheduler_usage/1 executes :erlang.statistics(:scheduler_wall_time), whose docs explain:

Only information about schedulers that are expected to handle CPU bound work is included in the return values from this function. […]

Normal schedulers will have scheduler identifiers in the range 1 =< SchedulerId =< erlang:system_info(schedulers). Dirty CPU schedulers will have scheduler identifiers in the range erlang:system_info(schedulers) < SchedulerId =< erlang:system_info(schedulers) + erlang:system_info(dirty_cpu_schedulers).

The +S 1 flag is setting the number of “normal” scheduler threads used by the emulator.

Scheduler threads online schedules Erlang processes and Erlang ports, and execute Erlang code and Erlang linked-in driver code

There will always be at least 1 each of

  • dirty cpu schedulers

    execute CPU-bound native functions, such as NIFs, linked-in driver code, and BIFs that cannot be managed cleanly by the normal emulator schedulers

  • dirty io schedulers

    execute I/O-bound native functions, such as NIFs and linked-in driver code, which cannot be managed cleanly by the normal emulator schedulers

Here’s how my 16-core system boots with the same scheduler flag.

$ iex --erl "+S 1"
Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1] [jit:ns]

Interactive Elixir (1.16.2) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> :erlang.system_info(:schedulers)
1
iex(2)> :erlang.system_info(:dirty_cpu_schedulers)
1
iex(3)> :erlang.system_info(:dirty_io_schedulers)
10
iex(4)> :erlang.statistics(:microstate_accounting) |> Enum.map(& {&1.type, &1.id}) |> Enum.sort()
[
  async: 0,
  aux: 1,
  dirty_cpu_scheduler: 1,
  dirty_io_scheduler: 1,
  dirty_io_scheduler: 2,
  dirty_io_scheduler: 3,
  dirty_io_scheduler: 4,
  dirty_io_scheduler: 5,
  dirty_io_scheduler: 6,
  dirty_io_scheduler: 7,
  dirty_io_scheduler: 8,
  dirty_io_scheduler: 9,
  dirty_io_scheduler: 10,
  poll: 0,
  scheduler: 1
]

By the way, that’s the info the BEAM prints out at the start [smp:1:1] [ds:1:1:10], equivalent to the flags iex --erl "+S 1:1 +SDcpu 1:1 +SDio 10"

4 Likes