Trong bài viết này, tôi sẽ trải nghiệm qua thư viện Silhouette của Kevin Gosse, để xem việc xây dựng một .NET CLR profiler cơ bản dễ dàng như thế nào. Và khi tôi nói cơ bản, ý tôi là rất cơ bản — trong bài viết này chúng ta chỉ đơn giản là ghi log khi một assembly được tải và thế là xong. Tôi chủ yếu muốn xem mức độ dễ dàng để bắt đầu và chạy được ngay.
Kevin đã có loạt bài gồm 5 phần về viết .NET profiler bằng C#, trong đó anh ấy mô tả thư viện Silhouette của mình, cũng như các bài viết tiếp theo sử dụng thư viện này để thực hiện các công việc thực tế, như đo lường khả năng phản hồi UI trong Resharper và sử dụng function hooks trong profiler. Bài viết này sẽ đơn giản hơn nhiều so với những bài đó, chỉ là một minh họa về cách thư viện hoạt động.
Tôi cũng sẽ không đi sâu vào chi tiết về bản thân các profiling APIs. Kevin đã đề cập một số điều này trong các bài viết trên, hoặc bạn cũng có thể xem loạt bài của Christophe Nasarre về các profiling APIs. Trong bài viết này, chúng ta thực sự không đi sâu như vậy, chúng ta chỉ thử nghiệm nhẹ nhàng và xem những gì có thể làm được!
Vậy trước khi bắt đầu, chúng ta nên ôn lại: Các .NET profiling APIs là gì?
Mục lục
.NET Profiling APIs là gì?
Đối với 99% mọi người, làm việc với .NET có nghĩa là ở trong một môi trường managed runtime tuyệt vời và không bao giờ phải lo lắng về native code, ngoại trừ thỉnh thoảng sử dụng P/Invoke. Tuy nhiên, đằng sau hậu trường, các thư viện lớp cơ sở .NET thường tương tác với các thư viện native, và bản thân .NET runtime là một ứng dụng native. Hơn nữa, cả .NET Core và .NET Framework đều cung cấp một loạt các unmanaged APIs mà bạn có thể gọi từ native code.
.NET Core ghi nhận ba loại unmanaged APIs chính:
- Debugging APIs — để debug code chạy trong môi trường common language runtime (CLR).
- Metadata APIs — để đọc hoặc tạo thông tin chi tiết về các module và type mà không cần tải chúng trong CLR.
- Profiling APIs — để giám sát việc thực thi chương trình bởi CLR.
Trong bài viết này, chúng ta chủ yếu xem xét loại cuối cùng, profiling APIs, và sẽ sử dụng metadata APIs như một vai trò hỗ trợ.
Khi bạn nghĩ về “profiling”, có lẽ bạn nghĩ đến các công cụ profiling tích hợp trong Visual Studio, dotTrace của JetBrain, hoặc xem các dấu vết dotnet-trace trong PerfView. Các profiler “truyền thống” này có thể sử dụng profiling APIs cho chức năng của chúng (mặc dù cũng có các cách tiếp cận khác), nhưng profiling APIs còn tổng quát hơn nhiều. Ví dụ, chúng tôi sử dụng chúng trong thư viện .NET client của Datadog để viết lại các phương thức nhằm thêm instrumentation.
Profiling APIs rất mạnh mẽ, nhưng khó khăn thực sự là chúng là các unmanaged APIs, điều đó thường có nghĩa là phải viết code C/C++ để sử dụng chúng. Kinh thật. Vâng, đó là lúc Silhouette xuất hiện.
Ai cần C khi bạn có NativeAOT?
Microsoft đã làm việc trên NativeAOT qua nhiều phiên bản, và với mỗi phiên bản .NET mới, nó ngày càng tốt hơn. Với NativeAOT, bạn có thể biên dịch ứng dụng .NET của mình thành một tệp nhị phân native, độc lập. Và một tệp nhị phân native độc lập là tất cả những gì bạn cần để viết một .NET profiler!
Điểm mấu chốt với một tệp nhị phân NativeAOT là nó hoàn toàn tự chứa (self-contained). Điều đó có nghĩa là khi tệp nhị phân profiling của bạn được tải, nó đang chạy một runtime .NET hoàn toàn riêng biệt với ứng dụng đang được profile. Đúng vậy, về mặt kỹ thuật, điều đó có nghĩa là có hai .NET runtime được tải trong cùng một tiến trình!
Tất nhiên, chỉ biên dịch .NET dưới dạng tệp nhị phân native không phải là yêu cầu duy nhất. Bạn cũng cần đảm bảo tệp nhị phân native của bạn hiển thị tất cả các entrypoint và interface chính xác để .NET runtime có thể tải thư viện của bạn như thể nó được xây dựng bằng C++. Trích dẫn từ bài viết của Kevin về Silhouette:
Nói tóm lại, chúng ta cần hiển thị một phương thức
DllGetClassObjectsẽ trả về một thể hiện củaIClassFactory. .NET runtime sẽ gọi phương thứcCreateInstancetrên class factory, phương thức này sẽ trả về một thể hiện củaICorProfilerCallback(hoặcICorProfilerCallback2,ICorProfilerCallback3, …, tùy thuộc vào phiên bản profiling API mà chúng ta muốn hỗ trợ). Cuối cùng nhưng không kém phần quan trọng, runtime sẽ gọi phương thứcInitializetrên thể hiện đó với tham sốIUnknownmà chúng ta có thể sử dụng để lấy một thể hiện củaICorProfilerInfo(hoặcICorProfilerInfo2,ICorProfilerInfo3, …) mà chúng ta cần để truy vấn profiling API.
Nếu những điều đó làm bạn choáng ngợp, đó là chuyện bình thường😅 Đây là những API mà rất ít kỹ sư .NET từng phải làm việc cùng, và vì chúng là các API C++, điều đó còn tệ hơn! Nhưng đó chính là vấn đề; với Native AOT và thư viện Silhouette, bạn không cần phải hiểu tất cả các tương tác này. Thư viện Silhouette xử lý công việc lộn xộn của việc thiết lập entrypoint của bạn và hiển thị các .NET types dưới dạng các C++ interfaces.
Tất nhiên, Silhouette không giúp bạn hoàn toàn thoát khỏi trách nhiệm — bạn vẫn cần biết các unmanaged APIs dùng để làm gì, cách sử dụng chúng và cách kết nối chúng lại với nhau. Nhưng Silhouette giúp việc bắt đầu dễ dàng hơn và cho phép bạn viết logic của mình bằng C#, ngôn ngữ bạn biết rõ nhất, thay vì phải vật lộn với C++.
Viết .NET profiler bằng C#
Ví dụ về mức độ dễ dàng mà Silhouette mang lại, trong phần còn lại của bài viết, chúng ta sẽ viết một profiler đơn giản bằng .NET chỉ in ra các assembly được tải ra console.
Tạo profiler và các dự án kiểm thử
Chúng ta sẽ bắt đầu bằng cách tạo một solution đơn giản. Solution này sẽ bao gồm hai dự án: một thư viện lớp (class library) là profiler của chúng ta và một ứng dụng kiểm thử “hello world” mà chúng ta sẽ profile:
# Tạo hai dự án
dotnet new classlib -o SilhouetteProf
dotnet new console -o TestApp
# Thêm các dự án vào file sln
dotnet new sln
dotnet sln add .\SilhouetteProf\
dotnet sln add .\TestApp\
Điều này tạo ra cấu trúc dự án cơ bản. Bây giờ chúng ta sẽ thêm thư viện Silhouette vào dự án profiler:
dotnet add package Silhouette --project SilhouetteProf
Tiếp theo, chúng ta cần đảm bảo xuất bản ứng dụng của mình bằng NativeAOT và cho phép code unsafe. Chúng ta sẽ không thực sự viết bất kỳ code unsafe nào trong bài kiểm tra này, nhưng Silhouette bao gồm một source generator mà có sử dụng unsafe.
Mở tệp SilhoutteProj.csproj và thêm hai thuộc tính:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>SilhouetteProf</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- 👇 Thêm hai dòng này -->
<PublishAot>true</PublishAot>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Silhouette" Version="3.2.0" />
</ItemGroup>
</Project>
Bây giờ chúng ta đã có các điều kiện tiên quyết, chúng ta có thể bắt đầu tạo profiler.
Tạo profiler cơ bản
Để tạo một .NET profiler với Silhouette, bạn tạo một lớp kế thừa từ CorProfilerCallbackBase (hoặc CorProfilerCallback2Base, CorProfilerCallback3Base, v.v. tùy thuộc vào chức năng bạn cần). Sau đó, bạn trang trí lớp này bằng thuộc tính [Profiler] và cung cấp một Guid duy nhất:
using Silhouette;
namespace SilhouetteProf;
// 👇 Sử dụng một Guid ngẫu nhiên mới, đừng dùng cái này!
[Profiler("9FD62131-BF21-47C1-A4D4-3AEF5D7C75C6")]
internal partial class MyCorProfilerCallback : CorProfilerCallback5Base
{
}
Trong ví dụ trên, tôi đã chọn một Guid ngẫu nhiên cho profiler của mình và kế thừa từ CorProfilerCallback5Base. Không có gì trong bài viết này thực sự cần “5” từ ICorProfilerInfo5, tôi chỉ đưa nó vào để minh họa mẫu thiết kế.
Thuộc tính [Profiler] ở trên điều khiển một source generator được bao gồm trong Silhouette, source generator này tạo ra mã boilerplate cần thiết cho .NET runtime để tạo IClassFactory. Bạn không bắt buộc phải sử dụng mã được tạo này (ví dụ: nếu bạn cần logic bổ sung trong phương thức DllGetClassObject); nếu bạn không muốn mã này, chỉ cần bỏ qua thuộc tính [Profiler]. Mã được tạo ra trông như thế này:
namespace Silhouette._Generated
{
using System;
using System.Runtime.InteropServices;
file static class DllMain
{
[UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")]
public static unsafe HResult DllGetClassObject(Guid* rclsid, Guid* riid, nint* ppv)
{
if (*rclsid != new Guid("9fd62131-bf21-47c1-a4d4-3aef5d7c75c6"))
{
return HResult.CORPROF_E_PROFILER_CANCEL_ACTIVATION;
}
*ppv = ClassFactory.For(new global::SilhouetteProf.MyCorProfilerCallback());
return HResult.S_OK;
}
}
}
Chúng ta đã có bộ khung của profiler, nhưng trước khi có thể biên dịch, chúng ta cần triển khai phương thức Initialize:
using Silhouette;
namespace SilhouetteProf;
[Profiler("9FD62131-BF21-47C1-A4D4-3AEF5D7C75C6")]
internal partial class MyCorProfilerCallback : CorProfilerCallback5Base
{
protected override HResult Initialize(int iCorProfilerInfoVersion)
{
Console.WriteLine("[SilhouetteProf] Initialize");
if (iCorProfilerInfoVersion < 5)
{
// chúng ta cần ít nhất ICorProfilerInfo5 và nhận được < 5
return HResult.E_FAIL;
}
// Gọi SetEventMask để báo cho .NET runtime biết chúng ta quan tâm đến sự kiện nào
return ICorProfilerInfo5.SetEventMask(COR_PRF_MONITOR.COR_PRF_MONITOR_ALL);
}
}
Đoạn code trên là phiên bản đơn giản nhất của phương thức Initialize mà chúng ta có thể viết. Silhouette xử lý việc xác định phiên bản nào của ICorProfilerInfo có sẵn và truyền nó dưới dạng int cho phương thức. Trong code trên, chúng ta đang đảm bảo rằng chúng ta có ít nhất ICorProfilerInfo5, để có thể gọi bất kỳ phương thức nào được hiển thị bởi ICorProfilerInfo5, ICorProfilerInfo4, ICorProfilerInfo3, v.v.
Bạn sẽ nhận thấy rất nhiều giá trị
HResultđược sử dụng làm giá trị trả về. Trả về mã lỗi là cách xử lý lỗi chủ yếu với các native APIs, vì vậy bạn sẽ phải xử lý chúng rất nhiều. May mắn thay, Silhouette cung cấp điều này dưới dạng một enum tiện lợi để bạn sử dụng.
Sau khi xác nhận rằng .NET runtime hiện tại hỗ trợ các tính năng chúng ta cần, chúng ta cần báo cho runtime biết chúng ta quan tâm đến sự kiện nào bằng enum COR_PRF_MONITOR và các phương thức SetEventMask() hoặc SetEventMask2(). Để đơn giản, tôi đã sử dụng ICorProfilerInfo5.SetEventMask() và chỉ cần bật tất cả các tính năng.
Trường
ICorProfilerInfo5được khởi tạo trước khiInitializeđược gọi, dựa trên phiên bản interface có sẵn. Ví dụ: nếuiCorProfilerInfoVersionlà7, thì tất cả các trườngICorProfilerInfo*cho đếnICorProfilerInfo7sẽ được khởi tạo. Điều rất quan trọng là bạn chỉ gọi các phiên bản interface đã được khởi tạo. Vì vậy, nếuiCorProfilerInfoVersionlà7, đừng gọiICorProfilerInfo8hoặc cao hơn!
Tại thời điểm này, chúng ta có thể kiểm tra profiler, nhưng tôi sẽ tiếp tục triển khai thêm một chút trước khi đến phần kiểm thử.
Thêm chức năng cho profiler
Phản hồi các sự kiện với Silhouette profiler dễ dàng như ghi đè một phương thức trong lớp cơ sở. Ví dụ, chúng ta có thể ghi đè phương thức Shutdown, được gọi khi runtime tắt:
protected override HResult Shutdown()
{
Console.WriteLine("[SilhouetteProf] Shutdown");
return HResult.S_OK;
}
Để thêm chút thú vị, chúng ta sẽ ghi đè phương thức AssemblyLoadFinished, được gọi khi một assembly đã tải xong:
protected override HResult AssemblyLoadFinished(AssemblyId assemblyId, HResult hrStatus)
{
// ...
}
Phương thức AssemblyLoadFinished cung cấp một AssemblyId mà chúng ta có thể sử dụng để lấy tên của assembly bằng cách gọi một phương thức khác trên ICorProfilerInfo5, GetAssemblyInfo(AssemblyId).
Kiểu
AssemblyIdlà một wrapper rất mỏng xung quanhIntPtr, hoạt động như các wrapper có kiểu mạnh (strongly-typed) xung quanh cácIntPtrphổ biến được sử dụng trong profiling APIs. Tôi là người rất hâm mộ cách tiếp cận này vì nó loại bỏ toàn bộ một lớp lỗi mà bạn có thể mắc phải khi truyền một “Class ID”IntPtrcho một phương thức mong đợi một “Assembly ID”IntPtr(ví dụ).
Chúng ta có thể gọi GetAssemblyInfo() khá dễ dàng bằng cách sử dụng trường ICorProfilerInfo5, nhưng đây là cơ hội tốt để xem xét một mẫu phổ biến trong thư viện Silhouette, việc sử dụng HResult<T>:
protected override HResult AssemblyLoadFinished(AssemblyId assemblyId, HResult hrStatus)
{
HResult<AssemblyInfoWithName> assemblyInfo = ICorProfilerInfo5.GetAssemblyInfo(assemblyId)
// ...
}
HResult<T> thực chất là một result pattern đơn giản dưới dạng discriminated union. Nó chứa cả HResult và, nếu HResult đại diện cho thành công, một đối tượng T. Cách tiếp cận này là một cách để tránh mẫu phổ biến trong profiling APIs có nhiều tham số “out” và một HRESULT để chỉ ra liệu có hợp lệ để sử dụng các giá trị đó hay không, ví dụ:
HRESULT GetAssemblyInfo(
[in] AssemblyID assemblyId,
[in] ULONG cchName,
[out] ULONG *pcchName,
[out, size_is(cchName), length_is(*pcchName)]
WCHAR szName[] ,
[out] AppDomainID *pAppDomainId,
[out] ModuleID *pModuleId);
Mẫu điển hình khi làm việc trực tiếp với profiling APIs là thực hiện lời gọi, kiểm tra giá trị trả về và sau đó quyết định có tiếp tục hay không. HResult<T> cũng cho phép mẫu đó, nhưng bạn cũng có thể đi theo phong cách YOLO. Thay vì tự mình thực hiện tất cả các kiểm tra, bạn có thể gọi HResult<T>.ThrowIfFailed(), phương thức này trả về T nếu lời gọi thành công và ném ra Win32Exception nếu không. Điều này có thể làm cho code trở nên đơn giản hơn rất nhiều để đọc và viết, vì vậy đó là một lợi thế thực sự.
Tất nhiên, liệu bạn có muốn làm điều này với một profiler cấp độ sản xuất hay không lại là một câu chuyện hoàn toàn khác. Nhưng liệu bạn có thực sự nên sử dụng bất cứ thứ gì từ bài viết này cho sản xuất không? Có lẽ là không 😉
Sử dụng cách tiếp cận ThrowIfFailed() cho chúng ta đoạn code dưới đây. Chúng ta cố gắng lấy tên assembly và nếu có được, in nó ra:
protected override HResult AssemblyLoadFinished(AssemblyId assemblyId, HResult hrStatus)
{
try
{
// Cố gắng lấy AssemblyInfoWithName, và nếu HResult trả về không thành công, hãy ném ngoại lệ
AssemblyInfoWithName assemblyInfo = ICorProfilerInfo5.GetAssemblyInfo(assemblyId).ThrowIfFailed();
Console.WriteLine($"[SilhouetteProf] AssemblyLoadFinished: {assemblyInfo.AssemblyName}");
return HResult.S_OK;
}
catch (Win32Exception ex)
{
// GetAssemblyInfo() thất bại vì lý do nào đó, kỳ lạ.
Console.WriteLine($"[SilhouetteProf] AssemblyLoadFinished failed: {ex}");
return ex.NativeErrorCode;
}
}
Lợi ích của ThrowIfFailed() không thực sự rõ ràng nếu bạn chỉ có một lời gọi duy nhất. Nơi nó thực sự tỏa sáng là khi bạn muốn xâu chuỗi nhiều lời gọi. Ví dụ: nếu chúng ta muốn triển khai ClassLoadStarted, chúng ta sẽ cần xâu chuỗi nhiều lời gọi, và đó là lúc ThrowIfFailed() phát huy tác dụng:
protected override HResult ClassLoadStarted(ClassId classId)
{
try
{
ClassIdInfo classIdInfo = ICorProfilerInfo.GetClassIdInfo(classId).ThrowIfFailed();
using ComPtr<IMetaDataImport>? metaDataImport = ICorProfilerInfo2
.GetModuleMetaDataImport(classIdInfo.ModuleId, CorOpenFlags.ofRead)
.ThrowIfFailed()
.Wrap();
TypeDefPropsWithName classProps = metaDataImport.Value.GetTypeDefProps(classIdInfo.TypeDef).ThrowIfFailed();
Console.WriteLine($"[SilhouetteProf] ClassLoadStarted: {classProps.TypeName}");
return HResult.S_OK;
}
catch (Win32Exception ex)
{
Console.WriteLine($"[SilhouetteProf] ClassLoadStarted failed: {ex}");
return ex.NativeErrorCode;
}
}
Chúng ta có ba lời gọi ThrowIfFailed() trong đoạn code trên, giúp duy trì luồng xử lý thủ tục, gọn gàng. Thay vào đó, chúng ta có thể thêm ba lệnh if (result != HResult.S_OK) vào code, nhưng điều đó khó theo dõi hơn, đặc biệt nếu bạn đang viết một cái gì đó tương tự hoặc chỉ đang tạo mẫu thử.
OK, bây giờ chúng ta đã có đủ chức năng để thử nghiệm profiler của mình!
Kiểm thử profiler mới
Để kiểm thử profiler, chúng ta cần làm ba việc:
- Xuất bản ứng dụng kiểm thử (test app).
- Xuất bản profiler.
- Thiết lập các biến môi trường profiling cần thiết.
Xuất bản ứng dụng kiểm thử
Chúng ta sẽ bắt đầu bằng cách xuất bản ứng dụng kiểm thử. Về mặt kỹ thuật, điều này không bắt buộc, chúng ta có thể chỉ cần chạy ứng dụng bằng dotnet run chẳng hạn. Khó khăn là điều này sẽ gọi .NET SDK, mà bản thân nó là một ứng dụng .NET, điều đó có nghĩa là chúng ta sẽ profile cả SDK đó nữa. Điều đó cũng ổn, nhưng không phải là điều chúng ta đang cố gắng làm.
Chúng ta có thể xuất bản ứng dụng hello world của mình bằng một lệnh dotnet publish đơn giản:
❯ dotnet publish .\TestApp\ -c Release
Restore complete (0.6s)
TestApp net10.0 succeeded (0.9s) → TestApp\bin\Release\net10.0\publish\
Xuất bản profiler
Xuất bản profiler cũng tương tự, nhưng vì chúng ta sử dụng NativeAOT, chúng ta cũng cần cung cấp một runtime ID. Trong .NET 10, bạn cũng có thể sử dụng tùy chọn --use-current-runtime để xuất bản cho “bất kỳ runtime nào bạn đang sử dụng”. Như bạn thấy bên dưới, SDK đã sử dụng win-x64 vì tôi đang chạy trên Windows:
❯ dotnet publish .\SilhouetteProf\ -c Release --use-current-runtime
Restore complete (0.6s)
SilhouetteProf net10.0 win-x64 succeeded (4.2s) → SilhouetteProf\bin\Release\net10.0\win-x64\publish\
Build succeeded in 5.5s
Vì chúng ta sử dụng NativeAOT, kết quả là một tệp dll duy nhất, tự chứa (cộng với các symbol debug riêng biệt). Đây là ứng dụng .NET của chúng ta, được biên dịch dưới dạng .NET profiler NativeAOT!
Thiết lập các biến môi trường profiling
Để gắn một profiler vào .NET runtime, bạn cần thiết lập một số biến môi trường. Các biến này khác nhau tùy thuộc vào việc bạn đang profile ứng dụng .NET Framework hay .NET Core. Có ba biến khác nhau cần thiết lập:
Đối với profile ứng dụng .NET Framework:
COR_ENABLE_PROFILING=1— Bật profilingCOR_PROFILER={9FD62131-BF21-47C1-A4D4-3AEF5D7C75C6}— Đặt giá trị GUID từ thuộc tính[Profiler].COR_PROFILER_PATH=c:\path\to\profiler— Đường dẫn đến tệp dll profiler
Đối với profile ứng dụng .NET Core/.NET 5+:
CORECLR_ENABLE_PROFILING=1— Bật profilingCORECLR_PROFILER={9FD62131-BF21-47C1-A4D4-3AEF5D7C75C6}— Đặt giá trị GUID từ thuộc tính[Profiler].CORECLR_PROFILER_PATH=c:\path\to\profiler— Đường dẫn đến tệp dll profiler
Có các phiên bản dành riêng cho nền tảng khác của biến đường dẫn mà bạn có thể thiết lập nếu cần hỗ trợ nhiều nền tảng.
Sau khi xuất bản profiler và ứng dụng, tôi đã sao chép đường dẫn tuyệt đối đến tệp dll profiler và thiết lập các biến môi trường cần thiết bằng PowerShell.
Về mặt kỹ thuật, bạn không bắt buộc phải sử dụng đường dẫn tuyệt đối cho tệp dll, bạn có thể sử dụng đường dẫn tương đối, nhưng đường dẫn đó là tương đối so với ứng dụng mục tiêu? Hay so với thư mục làm việc? Tôi thích sử dụng đường dẫn tuyệt đối vì chúng không có sự mơ hồ!
$env:CORECLR_ENABLE_PROFILING=1
$env:CORECLR_PROFILER="{9FD62131-BF21-47C1-A4D4-3AEF5D7C75C6}"
$env:CORECLR_PROFILER_PATH="D:\repos\temp\silouette-prof\SilhouetteProf\bin\Release\net10.0\win-x64\publish\SilhouetteProf.dll"
Lưu ý rằng biến GUID có bao gồm các dấu ngoặc nhọn {}. Sau khi các biến được thiết lập, chúng ta có thể thử nghiệm profiler của mình!
Kiểm thử ứng dụng với NativeAOT profiler
Khi chúng ta chạy ứng dụng, .NET runtime kiểm tra các biến CORECLR_ và tải profiler NativeAOT của chúng ta, phát ra các sự kiện khi ứng dụng thực thi. Khi mỗi sự kiện được kích hoạt, chúng ta ghi ra console và có thể thấy tất cả các assembly đang được tải khi ứng dụng “Hello World!” chạy!
❯ .\TestApp.exe
[SilhouetteProf] Initialize
[SilhouetteProf] AssemblyLoadFinished: System.Private.CoreLib
[SilhouetteProf] AssemblyLoadFinished: TestApp
[SilhouetteProf] AssemblyLoadFinished: System.Runtime
[SilhouetteProf] AssemblyLoadFinished: System.Console
[SilhouetteProf] AssemblyLoadFinished: System.Threading
[SilhouetteProf] AssemblyLoadFinished: System.Text.Encoding.Extensions
[SilhouetteProf] AssemblyLoadFinished: System.Runtime.InteropServices
Hello, World!
[SilhouetteProf] Shutdown
Và đây rồi, .NET profiler của chúng ta, được viết bằng .NET, hoạt động như mong đợi!🎉 Rõ ràng, đây là một triển khai rất đơn giản, nhưng nó cho tôi thấy việc sử dụng thư viện Silhouette dễ dàng như thế nào để có được một thứ gì đó hoạt động nhanh hơn rất nhiều so với việc phải vật lộn với C++.
Một điều cần lưu ý là mặc dù Silhouette giúp ích về mặt cơ chế lắng nghe sự kiện và tương tác với các C++ interfaces, bạn vẫn cần biết cách sử dụng các native APIs. Silhouette giúp giảm bớt độ khó trong việc học, nhưng bạn có thể vẫn cần phải nghiên cứu để đạt được điều mình muốn.
Theo quan điểm của tôi, Silhouette rõ ràng là một công cụ hữu ích để đáp ứng một nhu cầu cụ thể. Bạn không nhất thiết muốn sử dụng nó để tạo ra một profiler cấp độ sản xuất, nhưng đối với proof of concept hoặc công việc phát triển, nó dường như là vô giá. Đặc biệt nếu Kevin tiếp tục đăng các ví dụ thực tế về việc tự mình sử dụng Silhouette!
Tổng kết
Trong bài viết này, tôi đã giới thiệu ngắn gọn về các unmanaged .NET profiling APIs và cách bạn thường tương tác với các API này bằng C++. Sau đó, tôi mô tả cách bạn có thể sử dụng .NET để tạo ra một tệp nhị phân có thể tương tác với các API này thay vào đó, mang lại tất cả lợi ích của việc làm việc trong .NET, trong khi vẫn có thể gọi các native APIs.
Sau đó, tôi giới thiệu thư viện Silhouette của Kevin Gosse, và chỉ ra cách thư viện này giúp việc tạo ra một profiler với NativeAOT trở nên đơn giản, bằng cách kế thừa từ một lớp cơ sở và ghi đè các phương thức mà bạn quan tâm. Tôi đã tạo ra một profiler đơn giản, xuất bản nó và sử dụng nó để hiển thị tất cả các assembly được tải bởi một ứng dụng console hello world. Nhìn chung, tôi rất ấn tượng với mức độ đơn giản của Silhouette và có thể sẽ khám phá nó nhiều hơn nữa trong tương lai!



