如何使用 dotTrace 來診斷 netcore 應用的效能問題

Newbe36524發表於2020-10-09

最近在為 Newbe.Claptrap 做效能升級,因此將過程中使用到的 dotTrace 軟體的基礎用法介紹給各位開發者。

Newbe.Claptrap 是一個用於輕鬆應對併發問題的分散式開發框架。如果您是首次閱讀本系列文章。建議可以先從本文末尾的入門文章開始瞭解。

開篇摘要

dotTrace 是 Jetbrains 公司為 .net 應用提供的一款 profile 軟體。有助於對於軟體中的耗時函式和記憶體問題進行診斷分析。

本篇,我們將使用 Jetbrains 公司的 dotTrace 軟體對一些已知的效能問題進行分析。從而使讀者能夠掌握使用該軟體的基本技能。

過程中我們將搭配一些經典的面試問題進行演示,逐步解釋該軟體的使用。

此次示例使用的是 Rider 作為主要演示的 IDE。 開發者也可以使用 VS + Resharper 做出相同的效果。

如何獲取 dotTrace

dotTrace 是付費軟體。目前只要購買 dotUltimate 及以上的許可證便可以直接使用該軟體。

當然,該軟體也包含試用版本,可以免費開啟 7 天的試用時間。Jetbrains 的 IDE 購買滿一年以上即可獲取一個當前最新的永久使用版本。

或者也可以直接購買 Jetbrains 全家桶許可證,一次性全部帶走。

經典場景再現

接下來,我們通過一些經典的面試問題,來體驗一下如何使用 dotTrace。

何時要使用 StringBuilder

這是多麼經典的面試問題。能夠看到這篇文章的朋友,我相信各位都知道 StringBuilder 能夠減少 string 直接拼接的碎片,減少記憶體壓力這個道理。

我們這是真的嗎?會不會只是面試官想要刁難我,欺負我資訊不對稱呢?

沒有關係,接下來,讓我們使用 dotTrace 來具體的結合程式碼來分析一波。看看使用 StringBuilder 究竟有沒有減低記憶體分配的壓力。

首先,我們建立一個單元測試專案,並新增以下這樣一個測試類:

 
using System.Linq;
using System.Text;
using NUnit.Framework;

namespace Newbe.DotTrace.Tests
{
    public class X01StringBuilderTest
    {
        [Test]
        public void UsingString()
        {
            var source = Enumerable.Range(0, 10)
                .Select(x => x.ToString())
                .ToArray();
            var re = string.Empty;
            for (int i = 0; i < 10_000; i++)
            {
                re += source[i % 10];
            }
        }

        [Test]
        public void UsingStringBuilder()
        {
            var source = Enumerable.Range(0, 10)
                .Select(x => x.ToString())
                .ToArray();
            var sb = new StringBuilder();
            for (var i = 0; i < 10_000; i++)
            {
                sb.Append(source[i % 10]);
            }

            var _ = sb.ToString();
        }
    }
}

 

然後,如下圖所示,我們將 Rider 中的 profile 模式設定為 Timeline 。

設定profile模式

TimeLine 是多種模式中的一種,相較而言,該模式可以更全面的瞭解各個執行緒的工作情況,包括有記憶體分配、IO 處理、鎖、反射等等多維度資料。這將會作為本示例主要使用的一種模式。

接著,如下圖所示,通過單元測試左側的小圖示啟動對應測試的 profile。

啟動profile

啟動 profile 之後,等待一段時間之後,便會出現最新生成的 timeline 報告。檢視報告的位置如下所示:

啟動profile

右鍵選擇對應的報告,選擇”Open in External Viewer”,便可以使用 dotTrace 開啟生成好的報告。

那麼首先,讓我開啟第一個報告,檢視 UsingString 方法生成的報告。

如下圖所示,選擇 .Net Memory Allocations 以檢視該測試執行過程中分配的記憶體數額。

啟動profile

根據上圖我們可以得出以下結論:

  1. 在這測試中,有 102M 的記憶體被分配給 String 。注意,在 dotTrace 中顯示的分配是指整個執行過程中全部分配的記憶體。即使後續被回收,該數值也不會減少。
  2. 記憶體的分配只要在 CLR Worker 執行緒進行。並且非常的密集。

Tip: Timeline 所顯示的執行時間比正常執行測試的時間更長,因為在 profile 過程中需要對資料進行記錄會有額外的消耗。

因此,我們就得出了第一個結論:使用 string 進行直接拼接,確實會消耗更多的記憶體分配。

接著,我們繼續按照上面的步驟,檢視一下 UsingStringBuilder 方法的報告,如下所示:

啟動profile

根據上圖,我們可以得出第二個結論:使用 StringBuilder 可以明顯的減少相較於 string 直接拼接所消耗的記憶體。

當然,我們得到的最終的結論其實是:看來面試官不是糊弄人。

class 和 struct 對記憶體有什麼影響

class 和 struct 的區別有很多,面試題常客了。其中,兩者在記憶體方面就存在區別。

那麼我們通過一個測試來看看區別。

 
using System;
using System.Collections.Generic;
using NUnit.Framework;

namespace Newbe.DotTrace.Tests
{
    public class X02ClassAndStruct
    {
        [Test]
        public void UsingClass()
        {
            Console.WriteLine($"memory in bytes before execution: {GC.GetGCMemoryInfo().TotalAvailableMemoryBytes}");
            const int count = 1_000_000;
            var list = new List<Student>(count);
            for (var i = 0; i < count; i++)
            {
                list.Add(new Student
                {
                    Level = int.MinValue
                });
            }

            list.Clear();

            var gcMemoryInfo = GC.GetGCMemoryInfo();
            Console.WriteLine($"heap size: {gcMemoryInfo.HeapSizeBytes}");
            Console.WriteLine($"memory in bytes end of execution: {gcMemoryInfo.TotalAvailableMemoryBytes}");
        }

        [Test]
        public void UsingStruct()
        {
            Console.WriteLine($"memory in bytes before execution: {GC.GetGCMemoryInfo().TotalAvailableMemoryBytes}");
            const int count = 1_000_000;
            var list = new List<Yueluo>(count);
            for (var i = 0; i < count; i++)
            {
                list.Add(new Yueluo
                {
                    Level = int.MinValue
                });
            }

            list.Clear();

            var gcMemoryInfo = GC.GetGCMemoryInfo();
            Console.WriteLine($"heap size: {gcMemoryInfo.HeapSizeBytes}");
            Console.WriteLine($"memory in bytes end of execution: {gcMemoryInfo.TotalAvailableMemoryBytes}");
        }

        public class Student
        {
            public int Level { get; set; }
        }

        public struct Yueluo
        {
            public int Level { get; set; }
        }
    }
}

 

程式碼要點:

  1. 兩個測試,分別建立 1,000,000 個 class 和 struct 加入到 List 中。
  2. 執行測試之後,在測試的末尾輸出當前堆空間的大小。

按照上一節提供的基礎步驟,我們對比兩個方法生成的報告。

UsingClass

UsingClass

UsingStruct

UsingClass

對比兩個報告,可以得出以下這些結論:

  1. Timeline 報告中的記憶體分配,只包含分配在堆上的記憶體情況。
  2. struct 不需要分配在堆上,但是,陣列是引用物件,需要分配在堆上。
  3. List 自增的過程本質是擴張陣列的特性在報告中也得到了體現。
  4. 另外,沒有展示在報告上,而展示在測試列印文字中可以看到,UsingStruct 執行之後的堆大小也證實了 struct 不會被分配在堆上。

裝箱和拆箱

經典面試題 X3,來,上程式碼,上報告!

 
using NUnit.Framework;

namespace Newbe.DotTrace.Tests
{
    public class X03Boxing
    {
        [Test]
        public void Boxing()
        {
            for (int i = 0; i < 1_000_000; i++)
            {
                UseObject(i);
            }
        }

        [Test]
        public void NoBoxing()
        {
            for (int i = 0; i < 1_000_000; i++)
            {
                UseInt(i);
            }
        }

        public static void UseInt(int age)
        {
            // nothing
        }

        public static void UseObject(object obj)
        {
            // nothing
        }
    }
}

 

Boxing, 發生裝箱拆箱

Boxing

NoBoxing,未發生裝箱拆箱

NoBoxing

對比兩個報告,可以得出以下這些結論:

  1. 沒有買賣就沒有殺害,沒有裝拆就沒有分配消耗。

Thread.Sleep 和 Task.Delay 有什麼區別

經典面試題 X4,來,上程式碼,上報告!

 
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;

namespace Newbe.DotTrace.Tests
{
    public class X04SleepTest
    {
        [Test]
        public Task TaskDelay()
        {
            return Task.Delay(TimeSpan.FromSeconds(3));
        }

        [Test]
        public Task ThreadSleep()
        {
            return Task.Run(() => { Thread.Sleep(TimeSpan.FromSeconds(3)); });
        }
    }
}

 

ThreadSleep

ThreadSleep

TaskDelay

TaskDelay

對比兩個報告,可以得出以下這些結論:

  1. 在 dotTrace 中 Thread.Sleep 會被單獨標記,因為這是一種效能不不佳的做法,容易造成執行緒飢餓。
  2. Thread.Sleep 比起 Task.Delay 會多出一個執行緒處於 Sleep 狀態

阻塞大量的 Task 真的會導致應用一動不動嗎

有了上一步的結論,筆者產生了一個大膽的想法。我們都知道執行緒的有限的,那如果啟動非常多的 Thread.Sleep 或者 Task.Delay 會如何呢?

來,程式碼:

 
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;

namespace Newbe.DotTrace.Tests
{
    public class X04SleepTest
    {

        [Test]
        public Task RunThreadSleep()
        {
            return Task.WhenAny(GetTasks(50));

            IEnumerable<Task> GetTasks(int count)
            {
                for (int i = 0; i < count; i++)
                {
                    var i1 = i;
                    yield return Task.Run(() =>
                    {
                        Console.WriteLine($"Task {i1}");
                        Thread.Sleep(int.MaxValue);
                    });
                }

                yield return Task.Run(() => { Console.WriteLine("yueluo is the only one dalao"); });
            }
        }

        [Test]
        public Task RunTaskDelay()
        {
            return Task.WhenAny(GetTasks(50));

            IEnumerable<Task> GetTasks(int count)
            {
                for (int i = 0; i < count; i++)
                {
                    var i1 = i;
                    yield return Task.Run(() =>
                    {
                        Console.WriteLine($"Task {i1}");
                        return Task.Delay(TimeSpan.FromSeconds(int.MaxValue));
                    });
                }

                yield return Task.Run(() => { Console.WriteLine("yueluo is the only one dalao"); });
            }
        }
    }
}

 

這裡就不貼報告了,讀者可以試一下這個測試,也可以將報告的內容寫在本文的評論中參與討論~

反射呼叫和表示式樹編譯呼叫

有時,我們需要動態呼叫一個方法。最廣為人知的方式就是使用反射。

但是,這也是廣為人知的耗時相對較高的方式。

這裡,筆者提供一種使用表示式樹建立委託來取代反射提高效率的思路。

那麼,究竟有沒有減少時間消耗呢?好報告,自己會說話。

 
using System;
using System.Diagnostics;
using System.Linq.Expressions;
using NUnit.Framework;

namespace Newbe.DotTrace.Tests
{
    public class X05ReflectionTest
    {
        [Test]
        public void RunReflection()
        {
            var methodInfo = GetType().GetMethod(nameof(MoYue));
            Debug.Assert(methodInfo != null, nameof(methodInfo) + " != null");
            for (int i = 0; i < 1_000_000; i++)
            {
                methodInfo.Invoke(null, null);
            }

            Console.WriteLine(_count);
        }

        [Test]
        public void RunExpression()
        {
            var methodInfo = GetType().GetMethod(nameof(MoYue));
            Debug.Assert(methodInfo != null, nameof(methodInfo) + " != null");
            var methodCallExpression = Expression.Call(methodInfo);
            var lambdaExpression = Expression.Lambda<Action>(methodCallExpression);
            var func = lambdaExpression.Compile();
            for (int i = 0; i < 1_000_000; i++)
            {
                func.Invoke();
            }

            Console.WriteLine(_count);
        }

        private static int _count = 0;

        public static void MoYue()
        {
            _count++;
        }
    }
}

 

RunReflection,直接使用反射呼叫。

RunReflection

RunExpression,使用表示式樹編譯一個委託。

RunExpression

本篇小結

使用 dotTrace 可以檢視方法的記憶體和時間消耗。本篇所演示的內容只是其中很小的部分。開發者們可以嘗試上手,大有裨益。

本篇內容中的示例程式碼,均可以在以下連結倉庫中找到:

最後但是最重要!

如果讀者對該內容感興趣,歡迎轉發、評論、收藏文章以及專案。

最近作者正在構建以反應式Actor模式事件溯源為理論基礎的一套服務端開發框架。希望為開發者提供能夠便於開發出 “分散式”、“可水平擴充套件”、“可測試性高” 的應用系統 ——Newbe.Claptrap

本篇文章是該框架的一篇技術選文,屬於技術構成的一部分。

聯絡方式:

您還可以查閱本系列的其他選文:

理論入門篇

  1. Newbe.Claptrap - 一套以 “事件溯源” 和 “Actor 模式” 作為基本理論的服務端開發框架

術語介紹篇

  1. Actor 模式
  2. 事件溯源(Event Sourcing)
  3. Claptrap
  4. Minion
  5. 事件 (Event)
  6. 狀態 (State)
  7. 狀態快照 (State Snapshot)
  8. Claptrap 設計圖 (Claptrap Design)
  9. Claptrap 工廠 (Claptrap Factory)
  10. Claptrap Identity
  11. Claptrap Box
  12. Claptrap 生命週期(Claptrap Lifetime Scope)
  13. 序列化(Serialization)

實現入門篇

  1. Newbe.Claptrap 框架入門,第一步 —— 建立專案,實現簡易購物車
  2. Newbe.Claptrap 框架入門,第二步 —— 簡單業務,清空購物車
  3. Newbe.Claptrap 框架入門,第三步 —— 定義 Claptrap,管理商品庫存
  4. Newbe.Claptrap 框架入門,第四步 —— 利用 Minion,商品下單

樣例實踐篇

  1. 構建一個簡易的火車票售票系統,Newbe.Claptrap 框架用例,第一步 —— 業務分析
  2. 線上體驗火車票售票系統

其他番外篇

  1. 談反應式程式設計在服務端中的應用,資料庫操作優化,從 20 秒到 0.5 秒
  2. 談反應式程式設計在服務端中的應用,資料庫操作優化,提速 Upsert
  3. 十萬同時線上使用者,需要多少記憶體?——Newbe.Claptrap 框架水平擴充套件實驗
  4. docker-mcr 助您全速下載 dotnet 映象
  5. 十多位全球技術專家,為你獻上近十個小時的.Net 微服務介紹
  6. 年輕的樵夫喲,你掉的是這個免費 8 核 4G 公網伺服器,還是這個隨時可用的 Docker 實驗平臺?
  7. 如何使用 dotTrace 來診斷 netcore 應用的效能問題

GitHub 專案地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 專案地址:https://gitee.com/yks/Newbe.Claptrap

您當前檢視的是先行釋出於 www.newbe.pro 上的部落格文章,實際開發文件隨版本而迭代。若要檢視最新的開發文件,需要移步 claptrap.newbe.pro

Newbe.Claptrap

相關文章