Membaca penghitung kinerja .NET tanpa PerfCounter API
Catatan: Kode awalnya ditulis sebagai bagian dari pekerjaan saya di Datadog dan dipublikasikan di repositori dd-trace-dotnet di bawah lisensi Apache 2.0.
Oleh karena itu, kutipan kode dalam artikel ini dilisensikan dengan ketentuan yang sama.
Sebelum menggunakannya, mengutipnya, atau mendistribusikannya kembali, pastikan untuk memeriksa persyaratan lisensi dan pemberitahuannya .
Pelacak Datadog .NET memiliki fitur bernama " runtime metrics ". Ini mengumpulkan metrik teknis tentang perilaku aplikasi Anda dan memublikasikannya ke dasbor khusus, yang dapat Anda gunakan untuk mendiagnosis masalah kinerja Anda:
Banyak dari metrik tersebut dikumpulkan dari CLR, menggunakan penghitung kinerja di .NET Framework dan penghitung peristiwa di .NET Core.
Membaca penghitung kinerja di .NET cukup mudah, dan kodenya terlihat seperti:
var counter = new PerformanceCounter(".NET CLR Memory", "Gen 0 heap size", "ConsoleApp1");
Console.WriteLine(counter.NextValue());
Penghitung kinerja bisa berbahaya
Karena Datadog memiliki basis pelanggan yang luas, pustaka kami diterapkan di berbagai lingkungan, dan terkadang kami menerima laporan pesan kesalahan aneh di log peristiwa saat mengaktifkan metrik runtime di .NET Framework. Itu tidak ideal tetapi dapat diterima. Namun pada titik tertentu, satu pelanggan melaporkan kepada kami bahwa aplikasi mereka mogok dengan ekstensi AccessViolationException.
Untuk memahami bagaimana itu mungkin, kita perlu membahas cara kerja penghitung kinerja. Setiap penghitung kinerja memiliki penyedia yang terkait dengannya, yang bertanggung jawab untuk menerbitkan nilainya.
Ketahuilah bahwa ada dua jenis penyedia: v1 dan v2. Saya tidak akan membahas penghitung v2 karena saya hampir tidak memiliki pengetahuan tentang subjek ini, dan penyedia penghitung kinerja .NET menggunakan v1.
Dengan v1, setiap penyedia penghitung kinerja terdaftar di registri. Antara lain, informasi pendaftaran berisi jalur ke penyedia DLL. Jadi untuk membaca penghitung kinerja, Anda perlu:
- Temukan informasi penyedia penghitung kinerja di registri
- Muat DLL yang ditentukan
- Sebut saja untuk mendapatkan nilainya
Jadi dapatkah kita menyimpulkan bahwa crash yang dialami oleh satu pelanggan tersebut disebabkan oleh DLL yang terkait dengan penyedia penghitung kinerja .NET?
Yah itu semakin buruk.
Saya menjelaskan langkah-langkah untuk membaca penghitung kinerja, tetapi Anda sebenarnya tidak diharapkan untuk mengikutinya sendiri. Ada beberapa API yang tersedia untuk mengabstraksi semua ini, seperti PDH , PerfLib , atau… registry . Kunci HKEY_PERFORMANCE_DATAdalam registri itu khusus, dan ketika mencoba membacanya, Windows akan secara otomatis mengembalikan nilai penghitung kinerja yang terdaftar di mesin Anda.
Manakah dari API tersebut yang digunakan oleh .NET? Saat menggunakan PerformanceCounterkelas untuk membaca penghitung kinerja, .NET bergantung pada registri untuk mendapatkan nilai penghitung kinerja yang Anda minta.
Dan inilah masalah sebenarnya. Saya tidak tahu apakah itu kekhasan dari API registri, atau efek samping dari cara .NET menggunakannya, tetapi membaca penghitung kinerja menggunakan PerformanceCounterkelas .NET memicu pencacahan semua penghitung kinerja yang terdaftar di mesin. Dengan kata lain, saat pertama kali Anda menggunakan PerformanceCounter, .NET akan memuat setiap penyedia kinerja yang terdaftar di mesin Anda, meminta metadata darinya, lalu membongkarnya.
Artinya, kami tidak hanya memuat penyedia penghitung kinerja yang kami minati, tetapi juga banyak DLL lain yang tidak kami ketahui. Dalam kasus pelanggan yang malang, itu berarti salah satu penyedia penghitung kinerja yang diinstal pada mesin mereka adalah sampah, dan merusak memori proses saat memuatnya.
Dari sudut pandang teknis murni, Datadog tidak bertanggung jawab atas kerusakan ini. Kami menggunakan .NET API untuk menanyakan penyedia penghitung kinerja terkenal, dan karena cara penerapannya di .NET menyebabkan penyedia palsu lainnya dimuat. Kami mungkin dapat meminta pelanggan untuk membersihkan mesin mereka dan menutup kotaknya. Namun di sisi lain, penyedia pihak ketiga itu mungkin tidak akan dimuat jika kami tidak mencoba membaca penghitung kinerja, jadi kami pasti berperan dalam crash tersebut. Untuk mencegah kejadian di masa mendatang, saya memutuskan untuk mencari cara yang lebih aman untuk membaca penghitung tersebut.
Mencari alternatif
Seperti yang saya sebutkan sebelumnya, ada tiga cara untuk menanyakan penghitung kinerja: registri, PDH, dan PerfLib. PerfLib hanya dapat digunakan dengan penyedia v2, jadi saya memutuskan untuk bereksperimen dengan PDH. Saya menulis penyedia penghitung kinerja palsu saya sendiri dan mendaftarkannya di mesin saya, kemudian saya mulai menulis prototipe menggunakan PDH untuk mengonfirmasi bahwa DLL saya tidak dimuat sebagai efek samping.
Dan saat itulah saya tersadar.
Saya sebutkan sebelumnya bahwa penghitung kinerja memiliki instance yang terkait dengannya. Untuk penghitung kinerja yang disediakan oleh .NET, instance tersebut adalah nama proses yang penghitungnya ingin Anda baca.
Namun seperti yang telah saya jelaskan, untuk membaca nilai penghitung kinerja, Anda memuat penyedia (yang hanya berupa DLL) ke dalam proses Anda dan memanggilnya. Jadi jika saya membuat aplikasi konsol, katakanlah ConsoleApp1.exe, dan saya menanyakan penghitung kinerja dari proses lain, katakanlah devenv.exe, karena penyedia dimuat ke dalamnya ConsoleApp1.exe berarti semacam mekanisme komunikasi antar proses (IPC) digunakan untuk menanyakan nilai kinerja dari devenv.exeproses . Dan jika saya entah bagaimana bisa terhubung ke mekanisme IPC itu maka saya bisa melewati API penghitung kinerja sama sekali.
Kode sumber .NET Framework tidak bersifat publik, tetapi seperti yang telah saya sebutkan di banyak artikel saya yang lain, ada triknya: karena .NET Core dicabangkan dari .NET framework, Anda dapat membuka repositori coreclr di github dan checkout komit pertama , untuk mendapatkan gambaran umum yang baik tentang kode sumber .NET Framework sebelum menyimpang terlalu banyak.
Saya menghabiskan beberapa waktu mencari tetapi saya tidak dapat menemukan apa yang saya cari. Saya memang menemukan beberapa struktur yang digunakan untuk menyimpan nilai kinerja , dan kode yang memperbaruinya , tetapi gagal memahami bagaimana mereka diekspos ke proses lain.
Setelah beberapa saat, saya mundur selangkah, menjalankan prototipe PDH saya, dan memprofilkannya dengan profiler Superluminal yang luar biasa untuk mendapatkan petunjuk tentang apa yang terjadi di balik terpal saat memuat penyedia penghitung kinerja .NET.
Kami dapat melihat beberapa hal menarik dalam pelacakan tumpukan itu:
- Pertama, kami melihat panggilan ke
RegQueryValueExW, jadi sepertinya bahkan PDH bergantung pada registri di bawah tenda - Kemudian kami melihat panggilan ke
OpenCtrsinmscoreei.dll, ini adalah fungsi yang diekspos .NET untuk penyedia penghitung kinerjanya mscoreei.dlllalu panggilCORPerfMonExt.dll(DLL lain yang disertakan dengan .NET Framework)CORPerfMonExt.dllmenggunakan kelas bernamaIPCReaderInterface, yang namanya sangat menggembirakan mengingat kami sedang mencari beberapa mekanisme IPC
Sejak saat itu, itu hanya masalah membaca kode sumber IPCReaderInterface(dan pasangannya IPCWriterInterface) untuk memahami cara kerjanya.
Antarmuka .NET Framework IPC
Mekanisme IPC yang digunakan oleh .NET Framework untuk berbagi penghitung kinerja bergantung pada file yang dipetakan memori dengan izin khusus. Nama berkas memori adalah Cor_SxSPublic_IPCBlock, dan dapat diakses melalui ruang nama pribadi Cor_CLR_READER . Jika Anda ingin mengakses file dari proses saat ini, itu sudah dipetakan melalui namespace Cor_CLR_WRITER.
File yang dipetakan memori berisi daftar 32 "blok kontrol IPC", masing-masing dengan ukuran tetap 2KB. Sejauh yang saya tahu, ini adalah bagian dari infrastruktur yang diperkenalkan di .NET Framework 4 untuk memungkinkan eksekusi berdampingan dengan versi kerangka kerja yang akan datang. Setiap runtime yang dimuat dalam proses tertentu seharusnya memiliki salah satu blok, dan mengisi headernya dengan tepat. Mengingat tidak ada .NET Framework 5 yang pernah dirilis, saya yakin kemampuan ini tidak pernah dan tidak akan pernah digunakan, yang sedikit menyedihkan.
Header blok kontrol IPC berisi informasi tentang runtime yang mengambil kepemilikannya, dan jumlah entri di dalam blok. Setiap entri dapat memiliki ukuran yang sewenang-wenang, dan sejauh yang saya tahu hanya satu entri yang disimpan: entri yang berisi nilai penghitung kinerja.
Bagaimana cara mengakses semua ini di C#?
Untuk membuka file yang dipetakan memori dari dalam proses, cukup mudah menggunakan namespace System.IO.MemoryMappedFiles:
var file = MemoryMappedFile.OpenExisting(@"Cor_CLR_WRITER\Cor_SxSPublic_IPCBlock");
var view = file.CreateViewAccessor();
Kemudian secara teori kita harus menelusuri setiap blok 2KB untuk menemukan yang tepat, tetapi karena kemampuan berdampingan itu tidak pernah digunakan, saya yakin aman untuk menganggap itu akan selalu menjadi yang pertama.
Struktur header blok kontrol IPC yang diterjemahkan dalam C# adalah: (komentar disalin-tempel dari kode C++ asli)
[StructLayout(LayoutKind.Sequential)]
public readonly struct IPCHeader
{
[Flags]
public enum IpcHeaderFlags : ushort
{
IPC_FLAG_USES_FLAGS = 0x1,
IPC_FLAG_INITIALIZED = 0x2,
IPC_FLAG_X86 = 0x4
}
/// <summary>
/// Value of 0 is special; means that this block has never been touched before by a writer
/// </summary>
public readonly int Counter;
/// <summary>
/// Value of 0 is special; means that chunk is currently free (runtime ids are always greater than 0)
/// </summary>
public readonly int RuntimeId;
public readonly int Reserved1;
public readonly int Reserved2;
/// <summary>
/// Version of the IPC Block
/// </summary>
public readonly ushort Version;
/// <summary>
/// Flags field
/// </summary>
public readonly IpcHeaderFlags Flags;
/// <summary>
/// Size of the entire shared memory block
/// </summary>
public readonly int BlockSize;
/// <summary>
/// Stamp for year built
/// </summary>
public readonly ushort BuildYear;
/// <summary>
/// Stamp for Month/Day built
/// </summary>
public readonly ushort BuildNumber;
/// <summary>
/// Number of entries in the table
/// </summary>
public readonly int NumEntries;
/// <summary>
/// Entry describing each client's block
/// </summary>
public readonly IPCEntry Table;
}
Masing-masing IPCEntrymenunjukkan posisi dan ukuran entri di dalam blok:
[StructLayout(LayoutKind.Sequential)]
public readonly struct IPCEntry
{
/// <summary>
/// Offset of the IPC Block from the end of the Full IPC Header
/// </summary>
public readonly int Offset;
/// <summary>
/// Size (in bytes) of the block
/// </summary>
public readonly int Size;
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct IpcControlBlock
{
public readonly IPCHeader Header;
public readonly PerfCounterIpcControlBlock Perf;
}
Terakhir, the PerfCounterIPCControlBlockberisi nilai penghitung kinerja:
[StructLayout(LayoutKind.Sequential)]
public readonly struct PerfCounterIpcControlBlock
{
// Versioning info
/// <summary>
/// Size of this entire block
/// </summary>
public readonly short Size;
/// <summary>
/// Attributes for this block
/// </summary>
public readonly short Attributes;
// Counter Sections
public readonly PerfGC GC;
public readonly PerfContexts Context;
public readonly PerfInterop Interop;
public readonly PerfLoading Loading;
public readonly PerfExceptions Exceptions;
public readonly PerfLocksAndThreads LocksAndThreads;
public readonly PerfJit Jit;
public readonly PerfSecurity Security;
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct PerfJit
{
/// <summary>
/// Number of methods jitted
/// </summary>
public readonly int MethodsJitted;
/// <summary>
/// IL jitted stats
/// </summary>
public readonly TRICOUNT ILJitted;
/// <summary>
/// # of standard Jit failures
/// </summary>
public readonly int JitFailures;
/// <summary>
/// Time in JIT since last sample
/// </summary>
public readonly int TimeInJit;
/// <summary>
/// Time in JIT base counter
/// </summary>
public readonly int TimeInJitBase;
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct PerfSecurity
{
public readonly int TotalRuntimeChecks;
/// <summary>
/// % time authenticating
/// </summary>
public readonly long TimeAuthorize;
/// <summary>
/// Link time checks
/// </summary>
public readonly int LinkChecks;
/// <summary>
/// % time in Runtime checks
/// </summary>
public readonly int TimeRTchecks;
/// <summary>
/// % time in Runtime checks base counter
/// </summary>
public readonly int TimeRTchecksBase;
/// <summary>
/// Depth of stack for security checks
/// </summary>
public readonly int StackWalkDepth;
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct PerfLocksAndThreads
{
/// <summary>
/// # of times in AwareLock::EnterEpilogue()
/// </summary>
public readonly int Contention;
public readonly TRICOUNT QueueLength;
/// <summary>
/// Number (created - destroyed) of logical threads
/// </summary>
public readonly int CurrentThreadsLogical;
/// <summary>
/// Number (created - destroyed) of OS threads
/// </summary>
public readonly int CurrentThreadsPhysical;
/// <summary>
/// # of Threads execute in runtime's control
/// </summary>
public readonly TRICOUNT RecognizedThreads;
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct PerfExceptions
{
/// <summary>
/// Number of Exceptions thrown
/// </summary>
public readonly int Thrown;
/// <summary>
/// Number of Filters executed
/// </summary>
public readonly int FiltersExecuted;
/// <summary>
/// Number of Finallys executed
/// </summary>
public readonly int FinallysExecuted;
/// <summary>
/// Delta from throw to catch site on stack
/// </summary>
public readonly int ThrowToCatchStackDepth;
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct PerfLoading
{
public readonly TRICOUNT ClassesLoaded;
/// <summary>
/// Current # of AppDomains
/// </summary>
public readonly TRICOUNT AppDomains;
/// <summary>
/// Current # of Assemblies
/// </summary>
public readonly TRICOUNT Assemblies;
/// <summary>
/// % time loading
/// </summary>
public readonly long TimeLoading;
/// <summary>
/// Avg search length for assemblies
/// </summary>
public readonly int AsmSearchLength;
/// <summary>
/// Classes Failed to load
/// </summary>
public readonly int LoadFailures;
/// <summary>
/// Total size of heap used by the loader
/// </summary>
public readonly nint LoaderHeapSize;
/// <summary>
/// Rate at which app domains are unloaded
/// </summary>
public readonly int AppDomainsUnloaded;
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct PerfInterop
{
/// <summary>
/// Number of CCWs
/// </summary>
public readonly int CCW;
/// <summary>
/// Number of stubs
/// </summary>
public readonly int Stubs;
/// <summary>
/// # of time marshalling args and return values
/// </summary>
public readonly int Marshalling;
/// <summary>
/// Number of tlbs we import
/// </summary>
public readonly int TLBImports;
/// <summary>
/// Number of tlbs we export
/// </summary>
public readonly int TLBExports;
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct PerfContexts
{
/// <summary>
/// # of remote calls
/// </summary>
public readonly int RemoteCalls;
/// <summary>
/// Number of current channels
/// </summary>
public readonly int Channels;
/// <summary>
/// Number of context proxies
/// </summary>
public readonly int Proxies;
/// <summary>
/// # of Context-bound classes
/// </summary>
public readonly int Classes;
/// <summary>
/// # of context bound objects allocated
/// </summary>
public readonly int ObjAlloc;
/// <summary>
/// The current number of contexts
/// </summary>
public readonly int Contexts;
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct PerfGC
{
/// <summary>
/// Count of collects gen 0
/// </summary>
public readonly nint GenCollections0;
/// <summary>
/// Count of collects gen 1
/// </summary>
public readonly nint GenCollections1;
/// <summary>
/// Count of collects gen 2
/// </summary>
public readonly nint GenCollections2;
/// <summary>
/// Count of promoted memory from gen 0
/// </summary>
public readonly nint PromotedMem0;
/// <summary>
/// Count of promoted memory from gen 1
/// </summary>
public readonly nint PromotedMem1;
/// <summary>
/// Count of memory promoted due to finalization
/// </summary>
public readonly nint PromotedFinalizationMem;
/// <summary>
/// Process ID
/// </summary>
public readonly nint ProcessID;
/// <summary>
/// Size of heaps gen 0
/// </summary>
public readonly nint GenHeapSize0;
/// <summary>
/// Size of heaps gen 1
/// </summary>
public readonly nint GenHeapSize1;
/// <summary>
/// Size of heaps gen 2
/// </summary>
public readonly nint GenHeapSize2;
/// <summary>
/// Total number of committed bytes
/// </summary>
public readonly nint TotalCommittedBytes;
/// <summary>
/// Bytes reserved via VirtualAlloc
/// </summary>
public readonly nint TotalReservedBytes;
/// <summary>
/// Size of Large Object Heap
/// </summary>
public readonly nint LargeObjSize;
/// <summary>
/// Count of instances surviving from finalizing
/// </summary>
public readonly nint SurviveFinalize;
/// <summary>
/// Count of GC handles
/// </summary>
public readonly nint Handles;
/// <summary>
/// Bytes allocated
/// </summary>
public readonly nint Alloc;
/// <summary>
/// Bytes allocated for Large Objects
/// </summary>
public readonly nint LargeAlloc;
/// <summary>
/// Number of explicit GCs
/// </summary>
public readonly nint InducedGCs;
/// <summary>
/// Time in GC
/// </summary>
public readonly int TimeInGC;
/// <summary>
/// Must follow time in GC counter
/// </summary>
public readonly int TimeInGCBase;
/// <summary>
/// # of Pinned Objects
/// </summary>
public readonly nint PinnedObj;
/// <summary>
/// # of sink blocks
/// </summary>
public readonly nint SinkBlocks;
}
Sekarang kita hanya perlu membaca struktur dari file yang dipetakan memori dan selesai. Sama seperti pemeriksaan kewarasan, saya membaca nilai penghitung "id proses" dan membandingkannya dengan nilai yang diharapkan:
internal class Program
{
static void Main(string[] args)
{
var file = MemoryMappedFile.OpenExisting(@"Cor_CLR_WRITER\Cor_SxSPublic_IPCBlock");
var view = file.CreateViewAccessor();
if (GC.CollectionCount(0) == 0)
{
// We must make sure that the GC has run at least once,
// otherwise the GC structure that we use for the sanity check will be empty.
GC.Collect(0, GCCollectionMode.Forced, blocking: true);
}
view.Read<IpcControlBlock>(0, out var controlBlock);
Console.WriteLine($"Process id from perf counters: {controlBlock.Perf.GC.ProcessID}, expected: {Process.GetCurrentProcess().Id}");
}
}
Membungkus
Untuk membaca nilai penghitung kinerja dalam proses saat ini, pendekatan ini memiliki banyak keuntungan:
- Itu cepat. Ini benar-benar hanya membaca nilai dalam memori. Bahkan mungkin untuk menghindari salinan struct dengan menggunakan beberapa kode yang tidak aman (ada beberapa proposal API yang tertunda untuk membuatnya aman).
- Ini dapat diandalkan. Rasanya aneh mengatakan bahwa mengingat itu bergantung pada perilaku tidak berdokumen yang direkayasa ulang, tetapi jelas lebih aman daripada memuat dan memanggil penyedia penghitung kinerja yang sewenang-wenang.
- Ini memiliki kompatibilitas yang lebih baik. Anda memerlukan izin tambahan untuk membaca penghitung kinerja saat dijalankan di IIS, yang bisa sangat merepotkan saat dijalankan di lingkungan kotak pasir seperti Layanan Aplikasi Azure. Di sini kami tidak memiliki masalah ini karena proses tertentu selalu memiliki izin untuk membaca file yang dipetakan memorinya sendiri.
- Anda tidak perlu bersusah payah untuk menemukan instance yang tepat ketika banyak proses menggunakan nama yang sama.

![Apa itu Linked List? [Bagian 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































