Skip to content

Underutilization of TimerQueue when application runs with CPU affinity #83890

Description

@ezsilmar

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?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions