DotTrace系列:1. 理解四大经典的诊断类型(上)

一:背景

1. 讲故事

在所有与 .NET相关的JetBrains产品中,我觉得 DotTrace 是最值得深入学习和研究的一款,个人觉得它的优点如下:

  1. 跨平台诊断 (Windows,Linux,MacOS)
  2. 兼容 dotnet-trace 产出的 nettrace。
  3. 优秀的可视化界面,尤其是 timeline 时间轴。
  4. 支持自我托管和代码的局部诊断。

在我的 .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 会自动打开收集到的追踪文件,截图如下:

从卦中可以挖出如下信息:

  1. dottrace采样了 21s。
  2. 非托管层占 14s + 托管层占 7s
  3. 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%的内部优惠哦。