|
| 1 | +using D2SSharp.Model; |
| 2 | +using Xunit; |
| 3 | +using Xunit.Abstractions; |
| 4 | + |
| 5 | +namespace D2SSharp.Tests; |
| 6 | + |
| 7 | +/// <summary> |
| 8 | +/// Diagnostics to identify allocation sources. |
| 9 | +/// </summary> |
| 10 | +public class AllocationDiagnostics |
| 11 | +{ |
| 12 | + private readonly ITestOutputHelper _output; |
| 13 | + |
| 14 | + public AllocationDiagnostics(ITestOutputHelper output) |
| 15 | + { |
| 16 | + _output = output; |
| 17 | + } |
| 18 | + |
| 19 | + [Fact] |
| 20 | + public void AnalyzeDeserializationAllocations() |
| 21 | + { |
| 22 | + // Load test file |
| 23 | + string path = Path.Combine("Resources", "99", "Roka.d2s"); |
| 24 | + byte[] fileBytes = File.ReadAllBytes(path); |
| 25 | + |
| 26 | + // Warm up |
| 27 | + _ = D2Save.Read(fileBytes); |
| 28 | + GC.Collect(); |
| 29 | + GC.WaitForPendingFinalizers(); |
| 30 | + GC.Collect(); |
| 31 | + |
| 32 | + // Measure total allocation |
| 33 | + long before = GC.GetAllocatedBytesForCurrentThread(); |
| 34 | + var save = D2Save.Read(fileBytes); |
| 35 | + long after = GC.GetAllocatedBytesForCurrentThread(); |
| 36 | + long totalBytes = after - before; |
| 37 | + |
| 38 | + _output.WriteLine($"=== ALLOCATION ANALYSIS ==="); |
| 39 | + _output.WriteLine($"Total allocated: {totalBytes:N0} bytes ({totalBytes / 1024.0:N1} KB)"); |
| 40 | + _output.WriteLine(""); |
| 41 | + |
| 42 | + // Count objects |
| 43 | + int itemCount = CountItemsRecursive(save.Items); |
| 44 | + int corpseItemCount = save.Corpses.Sum(c => CountItemsRecursive(c.Items)); |
| 45 | + int mercItemCount = CountItemsRecursive(save.MercItems?.Items); |
| 46 | + int golemItemCount = save.IronGolem?.GolemItem != null ? CountItemsRecursive([save.IronGolem.GolemItem]) : 0; |
| 47 | + int totalItems = itemCount + corpseItemCount + mercItemCount + golemItemCount; |
| 48 | + |
| 49 | + int totalStats = save.Items.Sum(CountStatsRecursive); |
| 50 | + int playerStats = save.Stats.Count; |
| 51 | + |
| 52 | + _output.WriteLine($"=== OBJECT COUNTS ==="); |
| 53 | + _output.WriteLine($"Player items: {itemCount}"); |
| 54 | + _output.WriteLine($"Corpse items: {corpseItemCount}"); |
| 55 | + _output.WriteLine($"Merc items: {mercItemCount}"); |
| 56 | + _output.WriteLine($"Golem items: {golemItemCount}"); |
| 57 | + _output.WriteLine($"Total items: {totalItems}"); |
| 58 | + _output.WriteLine($"Total item stats: {totalStats}"); |
| 59 | + _output.WriteLine($"Player stats: {playerStats}"); |
| 60 | + _output.WriteLine(""); |
| 61 | + |
| 62 | + // Estimate per-object costs |
| 63 | + _output.WriteLine($"=== ESTIMATED COSTS ==="); |
| 64 | + _output.WriteLine($"Bytes per item (avg): {(totalItems > 0 ? totalBytes / totalItems : 0):N0}"); |
| 65 | + _output.WriteLine(""); |
| 66 | + |
| 67 | + // Measure individual object allocations |
| 68 | + _output.WriteLine($"=== INDIVIDUAL OBJECT SIZES ==="); |
| 69 | + |
| 70 | + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); |
| 71 | + before = GC.GetAllocatedBytesForCurrentThread(); |
| 72 | + var item1 = new Item(); |
| 73 | + after = GC.GetAllocatedBytesForCurrentThread(); |
| 74 | + _output.WriteLine($"Empty Item: {after - before} bytes"); |
| 75 | + |
| 76 | + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); |
| 77 | + before = GC.GetAllocatedBytesForCurrentThread(); |
| 78 | + var item2 = new Item(); |
| 79 | + _ = item2.Stats; // Force lazy init |
| 80 | + after = GC.GetAllocatedBytesForCurrentThread(); |
| 81 | + _output.WriteLine($"Item + Stats list init: {after - before} bytes"); |
| 82 | + |
| 83 | + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); |
| 84 | + before = GC.GetAllocatedBytesForCurrentThread(); |
| 85 | + var item3 = new Item(); |
| 86 | + _ = item3.Stats; |
| 87 | + _ = item3.Sockets; |
| 88 | + _ = item3.SetBonusStats; |
| 89 | + after = GC.GetAllocatedBytesForCurrentThread(); |
| 90 | + _output.WriteLine($"Item + all lists init: {after - before} bytes"); |
| 91 | + |
| 92 | + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); |
| 93 | + before = GC.GetAllocatedBytesForCurrentThread(); |
| 94 | + var list1 = new List<Stat>(); |
| 95 | + after = GC.GetAllocatedBytesForCurrentThread(); |
| 96 | + _output.WriteLine($"Empty List<Stat>: {after - before} bytes"); |
| 97 | + |
| 98 | + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); |
| 99 | + before = GC.GetAllocatedBytesForCurrentThread(); |
| 100 | + var list2 = new List<Stat>(10); |
| 101 | + after = GC.GetAllocatedBytesForCurrentThread(); |
| 102 | + _output.WriteLine($"List<Stat> capacity 10: {after - before} bytes"); |
| 103 | + |
| 104 | + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); |
| 105 | + before = GC.GetAllocatedBytesForCurrentThread(); |
| 106 | + var list3 = new List<Item?>(); |
| 107 | + after = GC.GetAllocatedBytesForCurrentThread(); |
| 108 | + _output.WriteLine($"Empty List<Item?>: {after - before} bytes"); |
| 109 | + |
| 110 | + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); |
| 111 | + before = GC.GetAllocatedBytesForCurrentThread(); |
| 112 | + var qd = new RareCraftQualityData(); |
| 113 | + after = GC.GetAllocatedBytesForCurrentThread(); |
| 114 | + _output.WriteLine($"RareCraftQualityData: {after - before} bytes"); |
| 115 | + |
| 116 | + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); |
| 117 | + before = GC.GetAllocatedBytesForCurrentThread(); |
| 118 | + var qd2 = new SetUniqueQualityData(); |
| 119 | + after = GC.GetAllocatedBytesForCurrentThread(); |
| 120 | + _output.WriteLine($"SetUniqueQualityData: {after - before} bytes"); |
| 121 | + |
| 122 | + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); |
| 123 | + before = GC.GetAllocatedBytesForCurrentThread(); |
| 124 | + var str = new string('X', 20); |
| 125 | + after = GC.GetAllocatedBytesForCurrentThread(); |
| 126 | + _output.WriteLine($"String (20 chars): {after - before} bytes (str.Length={str.Length})"); |
| 127 | + |
| 128 | + _output.WriteLine(""); |
| 129 | + _output.WriteLine($"=== ITEM BREAKDOWN ==="); |
| 130 | + |
| 131 | + // Analyze what's in each item |
| 132 | + int itemsWithStats = save.Items.Count(i => i.Stats.Count > 0); |
| 133 | + int itemsWithSockets = save.Items.Count(i => i.Sockets.Count > 0); |
| 134 | + int itemsWithSetBonus = save.Items.Count(i => i.SetBonusStats.Count > 0); |
| 135 | + int itemsWithRunewordStats = save.Items.Count(i => i.RunewordStats != null); |
| 136 | + int itemsWithQualityData = save.Items.Count(i => i.QualityData != null); |
| 137 | + int itemsWithPersonalization = save.Items.Count(i => !string.IsNullOrEmpty(i.PersonalizedName)); |
| 138 | + int itemsWithRealmData = save.Items.Count(i => i.RealmData != null); |
| 139 | + |
| 140 | + _output.WriteLine($"Items with stats: {itemsWithStats}/{save.Items.Count}"); |
| 141 | + _output.WriteLine($"Items with sockets: {itemsWithSockets}/{save.Items.Count}"); |
| 142 | + _output.WriteLine($"Items with set bonus: {itemsWithSetBonus}/{save.Items.Count}"); |
| 143 | + _output.WriteLine($"Items with runeword stats: {itemsWithRunewordStats}/{save.Items.Count}"); |
| 144 | + _output.WriteLine($"Items with quality data: {itemsWithQualityData}/{save.Items.Count}"); |
| 145 | + _output.WriteLine($"Items with personalization: {itemsWithPersonalization}/{save.Items.Count}"); |
| 146 | + _output.WriteLine($"Items with realm data: {itemsWithRealmData}/{save.Items.Count}"); |
| 147 | + |
| 148 | + // Quality data breakdown |
| 149 | + _output.WriteLine(""); |
| 150 | + _output.WriteLine($"=== QUALITY DATA TYPES ==="); |
| 151 | + var qualityGroups = save.Items |
| 152 | + .Where(i => i.QualityData != null) |
| 153 | + .GroupBy(i => i.QualityData!.GetType().Name) |
| 154 | + .OrderByDescending(g => g.Count()); |
| 155 | + foreach (var group in qualityGroups) |
| 156 | + { |
| 157 | + _output.WriteLine($"{group.Key}: {group.Count()}"); |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + private static int CountItemsRecursive(IEnumerable<Item?>? items) |
| 162 | + { |
| 163 | + if (items == null) return 0; |
| 164 | + int count = 0; |
| 165 | + foreach (var item in items) |
| 166 | + { |
| 167 | + if (item == null) continue; |
| 168 | + count++; |
| 169 | + count += CountItemsRecursive(item.Sockets); |
| 170 | + } |
| 171 | + return count; |
| 172 | + } |
| 173 | + |
| 174 | + private static int CountStatsRecursive(Item item) |
| 175 | + { |
| 176 | + int count = item.Stats.Count; |
| 177 | + count += item.SetBonusStats.Sum(l => l.Count); |
| 178 | + count += item.RunewordStats?.Count ?? 0; |
| 179 | + foreach (var socket in item.Sockets) |
| 180 | + { |
| 181 | + if (socket != null) |
| 182 | + count += CountStatsRecursive(socket); |
| 183 | + } |
| 184 | + return count; |
| 185 | + } |
| 186 | +} |
0 commit comments