Description
There's a sharding mechanism in TimerQueue that selects the queue based on the current CPU id
_associatedTimerQueue = TimerQueue.Instances[Thread.GetCurrentProcessorId() % TimerQueue.Instances.Length];
This doesn't work as expected when the application is bound to specific cores.
For instance, with 32-core server you may have 4 numa nodes each containing 8 logical cores, where every pair of logical cores is backed by a single physical core. In this case, the cpuset of first numa node may look like this: [0, 1, 2, 3, 16, 17, 18, 19] as Linux enumerates first the physical cores, then their HT counterpart.
This leads to underutilization of the timer queues as we'd only use the first 4 due to modulo.
Reproduction Steps
for (var i = 0; i < 100; i++)
{
Task.Run(() => Task.Delay(10*60*1000 + i));
}
Console.ReadKey();
Run it with
taskset -c 0,1,4,5 dotnet ./TimerRepro.dll
Then take a memory dump of the process and find the timer queues:
dotnet-dump ps
dotnet-dump collect -p <pid>
dotnet-dump analyze <dumpfile>
dumpheap -stat
do -mt <timerqueue_mt>
do <timerqueue>
dumparray -details <timerqueue_instances>
You'll observe that queues 0 and 1 are utilized while queues 2 and 3 are empty. Sample output (only interesting part):
MT Field Offset Type VT Attr Value Name
00007f40f009b3b8 4000b2b 30 System.Int64 1 instance 86 <ActiveCount>k__BackingField
00007f40f009b3b8 4000b2b 30 System.Int64 1 instance 14 <ActiveCount>k__BackingField
00007f40f009b3b8 4000b2b 30 System.Int64 1 instance 0 <ActiveCount>k__BackingField
00007f40f009b3b8 4000b2b 30 System.Int64 1 instance 0 <ActiveCount>k__BackingField
Expected behavior
All timer queues are utilized
Actual behavior
Depending on CPU ids only some queues are utilized
Regression?
No response
Known Workarounds
Some options that comes to mind:
- Introduce more complex logic similar to what GC does with affinity ranges: querying the number of CPUs on the machine, then querying the affinity mask, filtering CPUs to available ones and building a dictionary cpuid => TimerQueue
- Always create timerqueues equal to the number of total cores on the machine, even if we won't use all of them
- Not use the CPU id but a thread id or any other sharding key
Configuration
Observed on dotnet 6.0.12 WSL2 Ubuntu and dotnet 5.0.x CentOS 7 but the code is the same in newer versions.
Other information
Are there other places in the runtime where the same sharding is utilized?
Description
There's a sharding mechanism in TimerQueue that selects the queue based on the current CPU id
This doesn't work as expected when the application is bound to specific cores.
For instance, with 32-core server you may have 4 numa nodes each containing 8 logical cores, where every pair of logical cores is backed by a single physical core. In this case, the cpuset of first numa node may look like this:
[0, 1, 2, 3, 16, 17, 18, 19]as Linux enumerates first the physical cores, then their HT counterpart.This leads to underutilization of the timer queues as we'd only use the first 4 due to modulo.
Reproduction Steps
Run it with
Then take a memory dump of the process and find the timer queues:
You'll observe that queues 0 and 1 are utilized while queues 2 and 3 are empty. Sample output (only interesting part):
Expected behavior
All timer queues are utilized
Actual behavior
Depending on CPU ids only some queues are utilized
Regression?
No response
Known Workarounds
Some options that comes to mind:
Configuration
Observed on dotnet 6.0.12 WSL2 Ubuntu and dotnet 5.0.x CentOS 7 but the code is the same in newer versions.
Other information
Are there other places in the runtime where the same sharding is utilized?