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 acpuUtilization
The 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 similarcpuUtilization
field, which is rather awkward, the common response is as follows:
- Because there are no existing fields in dump or program, you can only get them using code in the program.
- 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 Pool
It's still a good idea to reverse search based on the contents of _minThreads and _maxThreads even without the pdb symbol.