Location>code7788 >text

AOT Rambling Topic (Part 3): How to Get CPU Utilization of C# Programs

Popularity:366 ℃/2024-10-14 10:36:06

I: Background

1. Storytelling

The previous post talked about how to do lightweight APM monitoring of AOT programs, a friend asked me how to get the CPU utilization of the AOT program, originally I thought it was a pretty simple problem, but a study is not such a thing, this post we have a simple chat.

II: How to get CPU utilization

1. Recognizing the cpuUtilization field

Those familiar with the underlying .NET should know that the .NET thread pool has acpuUtilizationThe field then records the CPU utilization of the current machine, so the next idea is how to dig this field out, before digging this field also know that .NET6 for the boundaries of the two thread pools have appeared.

1)

This is the .NET thread pool that was used until .NET 6, which was created by clr's1) realized, the reference code is as follows:


SVAL_IMPL(LONG,ThreadpoolMgr,cpuUtilization);

For better cross-platform and high-level unification, the .NET team refactored the original thread pool in C#, so naturally this field fell into C# as well, as referenced below:


internal sealed class PortableThreadPool
{
    private int _cpuUtilization;
}

I thought thread pooling had been split evenly between these two implementations, but it seems I was too young to know when another thread pooling implementation had been shoehorned inIt's a C# wrapper for the simple WindowsThreadPool, shedding a lot of the original method implementations, such as:


internal static class WindowsThreadPool
{
    public static bool SetMinThreads(int workerThreads, int completionPortThreads)
    {
        return false;
    }
    public static bool SetMaxThreads(int workerThreads, int completionPortThreads)
    {
        return false;
    }

    internal static void NotifyThreadUnblocked()
    {
    }

    internal unsafe static void RequestWorkerThread()
    {
        //todo...
        //Submit to windowsthread pool
        Interop.(s_work);
    }
}

And this is also the default implementation of the Windows version of the AOT, because the Windows thread pool is implemented by the operating system, there is no source code public, observed the open source implementation of reactos, and did not find a similarcpuUtilizationfield, which is rather awkward, the common response is as follows:

  1. Because there are no existing fields in dump or program, you can only get them using code in the program.
  2. Modify aot default thread pool on windows.

2. if the default thread pool for AOT is modified

In Microsoft's official documentation:/zh-cn/dotnet/core/runtime-config/threading The screenshot below documents some overview of the Windows thread pool and how to switch thread pools:

The MSBuild method of configuration is chosen here.


<Project Sdk="">

	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net8.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
		<PublishAot>true</PublishAot>
		<UseWindowsThreadPool>false</UseWindowsThreadPool>
		<InvariantGlobalization>true</InvariantGlobalization>
	</PropertyGroup>
</Project>

Next write a simple piece of C# code that intentionally makes a thread dead loop.


    internal class Program
    {
        static void Main(string[] args)
        {
            (() =>
            {
                Test();
            }).Wait();
        }

        static void Test()
        {
            var flag = true;
            while (true)
            {
                flag = !flag;
            }
        }
    }

One thing to note here is that a program published as AOT cannot be set up as a normal C# program with metadata. After all, the former has no metadata, so what to do? This is to test your understanding of the AOT dependency tree. Those who are familiar with AOT know that the construction of the dependency tree is ultimately stored as a directed graph in the _dependencyGraph field, and each node is carried by the base class NodeFactory, the reference code is as follows:


public abstract class Compilation : ICompilation
{
    protected readonly DependencyAnalyzerBase<NodeFactory> _dependencyGraph;
}

public abstract partial class NodeFactory
{
    public virtual void AttachToDependencyGraph(DependencyAnalyzerBase<NodeFactory> graph)
    {
        ReadyToRunHeader = new ReadyToRunHeaderNode();

        (ReadyToRunHeader, "ReadyToRunHeader is always generated");
        (new ModulesSectionNode(), "ModulesSection is always generated");

        (GCStaticsRegion, "GC StaticsRegion is always generated");
        (ThreadStaticsRegion, "ThreadStaticsRegion is always generated");
        (EagerCctorTable, "EagerCctorTable is always generated");
        (TypeManagerIndirection, "TypeManagerIndirection is always generated");
        (FrozenSegmentRegion, "FrozenSegmentRegion is always generated");
        (InterfaceDispatchCellSection, "Interface dispatch cell section is always generated");
        (ModuleInitializerList, "Module initializer list is always generated");

        if (_inlinedThreadStatics.IsComputed())
        {
            (_inlinedThreadStatiscNode, "Inlined threadstatics are used if present");
            (TlsRoot, "Inlined threadstatics are used if present");
        }

        (, GCStaticsRegion);
        (, ThreadStaticsRegion);
        (, EagerCctorTable);
        (, TypeManagerIndirection);
        (, FrozenSegmentRegion);
        (, ModuleInitializerList);

        var commonFixupsTableNode = new ExternalReferencesTableNode("CommonFixupsTable", this);
        (ReadyToRunHeader, this, commonFixupsTableNode);
        (ReadyToRunHeader, this, commonFixupsTableNode);
        (graph);
        ((), commonFixupsTableNode);
    }
}

Combined with the code above, our PortableThreadPool static class is recorded in the GCStaticsRegion of the root region, and with that knowledge, the next step is to dig in.

3. Mining with windbg

Start the generated aot program with windbg, then use theExample_21_8!S_P_CoreLib_System_Threading_PortableThreadPool::__GCSTATICS Find the static fields in the class.


0:007> dp Example_21_8!S_P_CoreLib_System_Threading_PortableThreadPool::__GCSTATICS L1
00007ff6`e4b7c5d0  000002a5`a4000468
0:007> dp 000002a5`a4000468+0x8 L1
000002a5`a4000470  000002a5`a6809ca0
0:007> dd 000002a5`a6809ca0+0x50 L1
000002a5`a6809cf0  0000000a
0:007> ? a
Evaluate expression: 10 = 00000000`0000000a

It is clear from the trigrams above that the currentCPU=16%. Here's a little explanation000002a5a4000468+0x8 is used to skip the vtable and thus fetch the class instance, followed by the000002a5a6809ca0+0x50 is used to get the PortableThreadPool._cpuUtilization field, the layout is referenced below:


0:012> !dumpobj /d 27bc100b288
Name:        
MethodTable: 00007ffc6c1aa6f8
EEClass:     00007ffc6c186b38
Tracked Type: false
Size:        512(0x200) bytes
File:        C:\Program Files\dotnet\shared\\8.0.8\
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffc6c031188  4000d42       50         System.Int32  1 instance                10 _cpuUtilization
00007ffc6c0548b0  4000d43       5c         System.Int16  1 instance               12 _minThreads
00007ffc6c0548b0  4000d44       5e         System.Int16  1 instance            32767 _maxThreads

III: Summary

In summary, if your AOT uses the default WindowsThreadPool, it's basically impossible to get the cpu utilization, of course, if anyone knows, they can tell you, if you cut to the default WindowsThreadPool, you can get the cpu utilization.NET Thread PoolIt's still a good idea to reverse search based on the contents of _minThreads and _maxThreads even without the pdb symbol.
图片名称