一:背景
1. 讲故事
在所有与 .NET相关的JetBrains产品中,我觉得 DotTrace
是最值得深入学习和研究的一款,个人觉得它的优点如下:
- 跨平台诊断 (Windows,Linux,MacOS)
- 兼容 dotnet-trace 产出的 nettrace。
- 优秀的可视化界面,尤其是 timeline 时间轴。
- 支持自我托管和代码的局部诊断。
在我的 .NET高级调试知识系列
下,这是一款不可或缺的利器,话不多说,我们就从四大诊断类型
来开聊吧。
二:四大诊断类型
1. Sampling 模式
如果你的程序出现了性能变慢,但你又不知道是哪里的变慢?不知道从何入手,这时候就可以使用 Sampling
模式,它是从应用程序
的角度帮你宏观洞察程序的性能,相当于性能洞察的第一道关卡。
Sampling 模式默认 5~11ms 对各个线程栈进行采样,通过大量的样本就能通过 group by
的方式计算出每个函数的累计执行时间,这里有一个小细节,如果 函数执行时间<5ms
的话,肯定是捕获不到的,这个也能理解。
接下来我们写一个简单的矩阵运算,然后寻找耗时的函数,参考代码如下:
C#
using System;
using System.Diagnostics;
namespace MatrixOperations
{
internal class Program
{
static void Main(string[] args)
{
const int baseSize = 1000;
const int iterations = 3;
for (int i = 0; i < iterations; i++)
{
int matrixSize = baseSize - (i * 100);
PerformMatrixMultiplication(matrixSize);
}
}
static void PerformMatrixMultiplication(int matrixSize)
{
Console.WriteLine($"\n=== 处理 {matrixSize}x{matrixSize} 矩阵 ===");
Console.WriteLine("创建随机矩阵...");
var matrixA = GenerateRandomMatrix(matrixSize, matrixSize);
var matrixB = GenerateRandomMatrix(matrixSize, matrixSize);
Console.WriteLine("执行矩阵乘法...");
var timer = Stopwatch.StartNew();
var resultMatrix = MultiplyMatrices(matrixA, matrixB);
timer.Stop();
Console.WriteLine($"运算完成,耗时: {timer.Elapsed.TotalSeconds:0.000} 秒");
DisplayMatrixPreview(resultMatrix);
}
static double[,] GenerateRandomMatrix(int rows, int cols)
{
var random = new Random();
var matrix = new double[rows, cols];
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
matrix[i, j] = random.NextDouble() * 100;
}
}
return matrix;
}
static double[,] MultiplyMatrices(double[,] matrixA, double[,] matrixB)
{
int aRows = matrixA.GetLength(0);
int aCols = matrixA.GetLength(1);
int bCols = matrixB.GetLength(1);
if (matrixA.GetLength(1) != matrixB.GetLength(0))
throw new ArgumentException("矩阵维度不匹配");
var result = new double[aRows, bCols];
for (int i = 0; i < aRows; i++)
{
for (int j = 0; j < bCols; j++)
{
double sum = 0;
for (int k = 0; k < aCols; k++)
{
sum += matrixA[i, k] * matrixB[k, j];
}
result[i, j] = sum;
}
}
return result;
}
static void DisplayMatrixPreview(double[,] matrix, int previewSize = 3)
{
Console.WriteLine($"\n矩阵预览 (前{previewSize}x{previewSize}个元素):");
int rows = Math.Min(previewSize, matrix.GetLength(0));
int cols = Math.Min(previewSize, matrix.GetLength(1));
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
Console.Write($"{matrix[i, j],8:0.00} ");
}
Console.WriteLine();
}
}
}
}
打开 dottrace,选择 Sampling
模式,点击 Start 即可,截图如下:

程序在运行完之后,会自动退出,dottrace 会自动打开收集到的追踪文件,截图如下:

从卦中可以挖出如下信息:
- dottrace采样了 21s。
- 非托管层占 14s + 托管层占 7s
- MultiplyMatrices 吃了7s,相对其他方法来说最耗cpu
要想知道 MultiplyMatrices 为什么会吃 7s,这个就属于细节问题了,Sampling模式就无法告诉你答案了。
值得一提的是,Sampling 属于大粒度的性能跟踪,生成的采样文件很小,适合天级别的长期监控。
2. Tracing 模式
刚才我们从应用程序
角度做了一个宏观洞察,发现了可疑函数 MultiplyMatrices
,我相信你此时会非常感兴趣,这个方法为什么会吃那么多的时间???
这就是本节要谈到的 Tracing
模式,相比 Sampling,它是方法级别
的洞察,你会看到方法的更多信息,比如:
- 方法的调用时间
- 方法的调用次数
有些人可能会好奇,方法的调用次数底层是怎么算出来的? 这个是得益于 coreclr 的 ICorProfilerCallback 通知机制,在进入方法和退出方法时,coreclr都会通知给注册的第三方(DotTrace),具体细节就不说了。
接下来修改为 Tracing
模式,重新执行程序,截图如下:

DotTrace 跟踪完成之后,会产生跟踪文件,然后用F5搜索目标函数MultiplyMatrices
。截图如下:

从卦中我们获得了更多的信息,比如发现有人对 MultiplyMatrices 方法做了三次调用,总计花费近 8s,平均下来每次call 近 3s,如果觉得单次 3s 还是有点长,接下来该如何继续下钻呢?
值得一提的是,Tracing 属于方法级作用域,生成的采样文件相对较大,适合小时级监控。
3. Line-by-Line 模式
刚才我们说到的 Tracing
属于一种方法级作用域,再往下走的话只能是 语句级
了,它的底层主要借助了IL插桩技术
,有一些像 harmony 的 transpiler,由于插入了大量的垃圾代码,会导致程序运行速度极度的下降,久久不能跟踪结束!所以在这种细粒度的场景下,更适合用代码实现局部跟踪,后续的文章会跟大家继续聊。
当我跟踪了100s后,停止跟踪,打开视图后,右键点击 MultiplyMatrices 方法查看源代码,截图如下:

从卦中可以清晰的看到,在我跟踪的100s周期内都是被MultiplyMatrices方法给吃掉了,从 命中次数
角度看,耗时都在三层的 for 循环中 O(N3)。其中第三层的 for 已执行了 10亿+ 次。。。
三:总结
DotTrace 是一款非常🐂👃的可视化商业工具,非常适合程序突然变慢
的场景分析。
作为JetBrains社区内容合作者,如有购买jetbrains的产品,可以用我的折扣码 HUANGXINCHENG,有25%的内部优惠哦。