├── HeapStringAnalyser
├── HeapStringAnalyser
│ ├── packages.config
│ ├── App.config
│ ├── Properties
│ │ └── AssemblyInfo.cs
│ ├── HeapStringAnalyser.csproj
│ └── Program.cs
└── HeapStringAnalyser.sln
├── .gitattributes
├── .gitignore
└── README.md
/HeapStringAnalyser/HeapStringAnalyser/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/HeapStringAnalyser/HeapStringAnalyser/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/HeapStringAnalyser/HeapStringAnalyser.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 14
4 | VisualStudioVersion = 14.0.23107.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeapStringAnalyser", "HeapStringAnalyser\HeapStringAnalyser.csproj", "{DD25D831-5305-4E5C-862E-80AFA688EFE0}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|x64 = Debug|x64
11 | Debug|x86 = Debug|x86
12 | Release|x64 = Release|x64
13 | Release|x86 = Release|x86
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {DD25D831-5305-4E5C-862E-80AFA688EFE0}.Debug|x64.ActiveCfg = Debug|x64
17 | {DD25D831-5305-4E5C-862E-80AFA688EFE0}.Debug|x64.Build.0 = Debug|x64
18 | {DD25D831-5305-4E5C-862E-80AFA688EFE0}.Debug|x86.ActiveCfg = Debug|x86
19 | {DD25D831-5305-4E5C-862E-80AFA688EFE0}.Debug|x86.Build.0 = Debug|x86
20 | {DD25D831-5305-4E5C-862E-80AFA688EFE0}.Release|x64.ActiveCfg = Release|x64
21 | {DD25D831-5305-4E5C-862E-80AFA688EFE0}.Release|x64.Build.0 = Release|x64
22 | {DD25D831-5305-4E5C-862E-80AFA688EFE0}.Release|x86.ActiveCfg = Release|x86
23 | {DD25D831-5305-4E5C-862E-80AFA688EFE0}.Release|x86.Build.0 = Release|x86
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | EndGlobal
29 |
--------------------------------------------------------------------------------
/HeapStringAnalyser/HeapStringAnalyser/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("HeapStringAnalyser")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("Unknown")]
12 | [assembly: AssemblyProduct("HeapStringAnalyser")]
13 | [assembly: AssemblyCopyright("Copyright © Matt Warren 2016")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("dd25d831-5305-4e5c-862e-80afa688efe0")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | build/
21 | bld/
22 | [Bb]in/
23 | [Oo]bj/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 |
28 | # MSTest test Results
29 | [Tt]est[Rr]esult*/
30 | [Bb]uild[Ll]og.*
31 |
32 | # NUNIT
33 | *.VisualState.xml
34 | TestResult.xml
35 |
36 | # Build Results of an ATL Project
37 | [Dd]ebugPS/
38 | [Rr]eleasePS/
39 | dlldata.c
40 |
41 | # DNX
42 | project.lock.json
43 | artifacts/
44 |
45 | *_i.c
46 | *_p.c
47 | *_i.h
48 | *.ilk
49 | *.meta
50 | *.obj
51 | *.pch
52 | *.pdb
53 | *.pgc
54 | *.pgd
55 | *.rsp
56 | *.sbr
57 | *.tlb
58 | *.tli
59 | *.tlh
60 | *.tmp
61 | *.tmp_proj
62 | *.log
63 | *.vspscc
64 | *.vssscc
65 | .builds
66 | *.pidb
67 | *.svclog
68 | *.scc
69 |
70 | # Chutzpah Test files
71 | _Chutzpah*
72 |
73 | # Visual C++ cache files
74 | ipch/
75 | *.aps
76 | *.ncb
77 | *.opensdf
78 | *.sdf
79 | *.cachefile
80 |
81 | # Visual Studio profiler
82 | *.psess
83 | *.vsp
84 | *.vspx
85 |
86 | # TFS 2012 Local Workspace
87 | $tf/
88 |
89 | # Guidance Automation Toolkit
90 | *.gpState
91 |
92 | # ReSharper is a .NET coding add-in
93 | _ReSharper*/
94 | *.[Rr]e[Ss]harper
95 | *.DotSettings.user
96 |
97 | # JustCode is a .NET coding add-in
98 | .JustCode
99 |
100 | # TeamCity is a build add-in
101 | _TeamCity*
102 |
103 | # DotCover is a Code Coverage Tool
104 | *.dotCover
105 |
106 | # NCrunch
107 | _NCrunch_*
108 | .*crunch*.local.xml
109 |
110 | # MightyMoose
111 | *.mm.*
112 | AutoTest.Net/
113 |
114 | # Web workbench (sass)
115 | .sass-cache/
116 |
117 | # Installshield output folder
118 | [Ee]xpress/
119 |
120 | # DocProject is a documentation generator add-in
121 | DocProject/buildhelp/
122 | DocProject/Help/*.HxT
123 | DocProject/Help/*.HxC
124 | DocProject/Help/*.hhc
125 | DocProject/Help/*.hhk
126 | DocProject/Help/*.hhp
127 | DocProject/Help/Html2
128 | DocProject/Help/html
129 |
130 | # Click-Once directory
131 | publish/
132 |
133 | # Publish Web Output
134 | *.[Pp]ublish.xml
135 | *.azurePubxml
136 | ## TODO: Comment the next line if you want to checkin your
137 | ## web deploy settings but do note that will include unencrypted
138 | ## passwords
139 | #*.pubxml
140 |
141 | *.publishproj
142 |
143 | # NuGet Packages
144 | *.nupkg
145 | # The packages folder can be ignored because of Package Restore
146 | **/packages/*
147 | # except build/, which is used as an MSBuild target.
148 | !**/packages/build/
149 | # Uncomment if necessary however generally it will be regenerated when needed
150 | #!**/packages/repositories.config
151 |
152 | # Windows Azure Build Output
153 | csx/
154 | *.build.csdef
155 |
156 | # Windows Store app package directory
157 | AppPackages/
158 |
159 | # Visual Studio cache files
160 | # files ending in .cache can be ignored
161 | *.[Cc]ache
162 | # but keep track of directories ending in .cache
163 | !*.[Cc]ache/
164 |
165 | # Others
166 | ClientBin/
167 | [Ss]tyle[Cc]op.*
168 | ~$*
169 | *~
170 | *.dbmdl
171 | *.dbproj.schemaview
172 | *.pfx
173 | *.publishsettings
174 | node_modules/
175 | orleans.codegen.cs
176 |
177 | # RIA/Silverlight projects
178 | Generated_Code/
179 |
180 | # Backup & report files from converting an old project file
181 | # to a newer Visual Studio version. Backup files are not needed,
182 | # because we have git ;-)
183 | _UpgradeReport_Files/
184 | Backup*/
185 | UpgradeLog*.XML
186 | UpgradeLog*.htm
187 |
188 | # SQL Server files
189 | *.mdf
190 | *.ldf
191 |
192 | # Business Intelligence projects
193 | *.rdl.data
194 | *.bim.layout
195 | *.bim_*.settings
196 |
197 | # Microsoft Fakes
198 | FakesAssemblies/
199 |
200 | # Node.js Tools for Visual Studio
201 | .ntvs_analysis.dat
202 |
203 | # Visual Studio 6 build log
204 | *.plg
205 |
206 | # Visual Studio 6 workspace options file
207 | *.opt
208 |
209 | # LightSwitch generated files
210 | GeneratedArtifacts/
211 | _Pvt_Extensions/
212 | ModelManifest.xml
213 |
--------------------------------------------------------------------------------
/HeapStringAnalyser/HeapStringAnalyser/HeapStringAnalyser.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | x86
7 | {DD25D831-5305-4E5C-862E-80AFA688EFE0}
8 | Exe
9 | Properties
10 | HeapStringAnalyser
11 | HeapStringAnalyser
12 | v4.5
13 | 512
14 |
15 |
16 | true
17 | bin\x64\Debug\
18 | DEBUG;TRACE
19 | full
20 | x64
21 | prompt
22 | MinimumRecommendedRules.ruleset
23 | true
24 |
25 |
26 | bin\x64\Release\
27 | TRACE
28 | true
29 | pdbonly
30 | x64
31 | prompt
32 | MinimumRecommendedRules.ruleset
33 | true
34 |
35 |
36 | true
37 | bin\x86\Debug\
38 | DEBUG;TRACE
39 | full
40 | x86
41 | prompt
42 | MinimumRecommendedRules.ruleset
43 | true
44 |
45 |
46 | bin\x86\Release\
47 | TRACE
48 | true
49 | pdbonly
50 | x86
51 | prompt
52 | MinimumRecommendedRules.ruleset
53 | true
54 |
55 |
56 |
57 | ..\packages\Microsoft.Diagnostics.Runtime.0.8.31-beta\lib\net40\Microsoft.Diagnostics.Runtime.dll
58 | True
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HeapStringAnalyser
2 | Analyse Memory Dumps looking at .NET String types
3 |
4 | ## Usage Instructions
5 |
6 | 1. Create a **Full** Process Dump (e.g. in Task Manager, right-click on the process and click **Create Dump File**)
7 | 1. Run the tool by executing the command line `HeapStringAnalyser.exe `
8 | 1. You can also add the `--gcInfo` argument to the cmd-line and you will get additional information about the GC Heaps
9 |
10 | ## Sample output
11 |
12 | ```
13 | .NET Memory Dump Heap Analyser - created by Matt Warren - github.com/mattwarren
14 |
15 | Found CLR Version: v4.6.1076.00
16 |
17 | Dac already exists on the local machine at:
18 | C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscordacwks.dll
19 |
20 | Memory Region Information
21 | ------------------------------------------------
22 | Type Count Total Size (MB)
23 | ------------------------------------------------
24 | GCSegment 6 75.67
25 | ReservedGCSegment 6 20.30
26 | LowFrequencyLoaderHeap 246 16.10
27 | HighFrequencyLoaderHeap 208 12.92
28 | ResolveHeap 22 1.25
29 | DispatchHeap 12 0.61
30 | HandleTableChunk 8 0.50
31 | StubHeap 10 0.47
32 | CacheEntryHeap 7 0.29
33 | IndcellHeap 6 0.21
34 | LookupHeap 6 0.21
35 | ------------------------------------------------
36 |
37 | GC Heap Information - Workstation
38 | -----------------------------------------------------------
39 | Heap 0: 78,030,788 bytes (74.42 MB) in use
40 | -----------------------------------------------------------
41 | Type Size (MB) Committed (MB) Reserved (MB)
42 | -----------------------------------------------------------
43 | Ephemeral 2.66 3.79 16.00
44 | Gen2 16.00 16.00 16.00
45 | Gen2 16.00 16.00 16.00
46 | Gen2 16.00 16.00 16.00
47 | Gen2 15.89 15.89 16.00
48 | Large 7.88 8.00 16.00
49 | -----------------------------------------------------------
50 | Total (across all heaps): 78,030,788 bytes (74.42 MB)
51 | -----------------------------------------------------------
52 |
53 | "System.String" memory usage info
54 | Overall 145,872 "System.String" objects take up 12,391,286 bytes (11.82 MB)
55 | Of this underlying byte arrays (as Unicode) take up 10,349,078 bytes (9.87 MB)
56 | Remaining data (object headers, other fields, etc) are 2,042,208 bytes (1.95 MB), at 14 bytes per object
57 |
58 | Actual Encoding that the "System.String" could be stored as (with corresponding data size)
59 | 10,339,638 bytes ( 145,505 strings) as ASCII
60 | 3,370 bytes ( 65 strings) as ISO-8859-1 (Latin-1)
61 | 6,070 bytes ( 302 strings) as Unicode
62 | Total: 10,349,078 bytes (expected: 10,349,078)
63 |
64 | Compression Summary:
65 | 5,171,504 bytes Compressed (to ISO-8859-1 (Latin-1))
66 | 6,070 bytes Uncompressed (as Unicode)
67 | 145,872 bytes EXTRA to enable compression (1-byte field, per "System.String" object)
68 |
69 | Total Usage: 5,323,446 bytes (5.08 MB), compared to 10,349,078 (9.87 MB) before compression
70 | Total Saving: 5,025,632 bytes (4.79 MB)
71 |
72 | Press any key to continue . . .
73 | ```
--------------------------------------------------------------------------------
/HeapStringAnalyser/HeapStringAnalyser/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Diagnostics.Runtime;
2 | using System;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 |
7 | namespace HeapStringAnalyser
8 | {
9 | // See https://github.com/Microsoft/clrmd/blob/master/Documentation/GettingStarted.md
10 | // and https://github.com/Microsoft/clrmd/blob/master/Documentation/ClrRuntime.md
11 | // and https://github.com/Microsoft/clrmd/blob/master/Documentation/WalkingTheHeap.md
12 | // A useful list of instructions for working with CLRMD,
13 | // see http://blogs.msdn.com/b/kirillosenkov/archive/2014/07/05/get-most-duplicated-strings-from-a-heap-dump-using-clrmd.aspx
14 | class Program
15 | {
16 | // See "Some simple starter measurements" on http://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
17 | private static ulong HeaderSize = (ulong)(Environment.Is64BitProcess ? 26 : 14);
18 |
19 | static void Main(string[] args)
20 | {
21 | Console.ForegroundColor = ConsoleColor.Cyan;
22 | Console.WriteLine(".NET Memory Dump Heap Analyser - created by Matt Warren - github.com/mattwarren\n");
23 | Console.ResetColor();
24 |
25 | if (args.Length < 1)
26 | {
27 | Console.WriteLine("Usage:\n HeapStringAnalyser.exe \n");
28 | return;
29 | }
30 |
31 | var memoryDumpPath = args[0];
32 | if (File.Exists(memoryDumpPath) == false)
33 | {
34 | Console.WriteLine("{0} - does not exist!", memoryDumpPath);
35 | return;
36 | }
37 |
38 | using (DataTarget target = DataTarget.LoadCrashDump(memoryDumpPath))
39 | {
40 | ClrRuntime runtime = CreateRuntime(target);
41 | if (runtime == null)
42 | return;
43 |
44 | var heap = runtime.GetHeap();
45 |
46 | var showGcHeapInfo = args.Any(a => a.ToLowerInvariant() == "--gcinfo" || a.ToLowerInvariant() == "-gcinfo");
47 | if (showGcHeapInfo)
48 | {
49 | PrintMemoryRegionInfo(runtime);
50 |
51 | PrintGCHeapInfo(runtime, heap);
52 | }
53 |
54 | ExamineProcessHeap(runtime, heap, showGcHeapInfo);
55 | }
56 | }
57 |
58 | private static void ExamineProcessHeap(ClrRuntime runtime, ClrHeap heap, bool showGcHeapInfo)
59 | {
60 | if (!heap.CanWalkHeap)
61 | {
62 | Console.WriteLine("Cannot walk the heap!");
63 | return;
64 | }
65 |
66 | ulong totalStringObjectSize = 0, stringObjectCounter = 0, byteArraySize = 0;
67 | ulong asciiStringSize = 0, unicodeStringSize = 0, isoStringSize = 0; //, utf8StringSize = 0;
68 | ulong asciiStringCount = 0, unicodeStringCount = 0, isoStringCount = 0; //, utf8StringCount = 0;
69 | ulong compressedStringSize = 0, uncompressedStringSize = 0;
70 | foreach (var obj in heap.EnumerateObjectAddresses())
71 | {
72 | ClrType type = heap.GetObjectType(obj);
73 | // If heap corruption, continue past this object. Or if it's NOT a String we also ignore it
74 | if (type == null || type.IsString == false)
75 | continue;
76 |
77 | stringObjectCounter++;
78 | var text = (string)type.GetValue(obj);
79 | var rawBytes = Encoding.Unicode.GetBytes(text);
80 | totalStringObjectSize += type.GetSize(obj);
81 | byteArraySize += (ulong)rawBytes.Length;
82 |
83 | VerifyStringObjectSize(runtime, type, obj, text);
84 |
85 | // Try each encoding in order, so we find the most-compact encoding that the text would fit in
86 | byte[] textAsBytes = null;
87 | if (IsASCII(text, out textAsBytes))
88 | {
89 | asciiStringSize += (ulong)rawBytes.Length;
90 | asciiStringCount++;
91 |
92 | // ASCII is compressed as ISO-8859-1 (Latin-1) NOT ASCII
93 | if (IsIsoLatin1(text, out textAsBytes))
94 | compressedStringSize += (ulong)textAsBytes.Length;
95 | else
96 | Console.WriteLine("ERROR: \"{0}\" is ASCII but can't be encoded as ISO-8859-1 (Latin-1)", text);
97 | }
98 | // From http://stackoverflow.com/questions/7048745/what-is-the-difference-between-utf-8-and-iso-8859-1
99 | // "ISO 8859-1 is a single-byte encoding that can represent the first 256 Unicode characters"
100 | else if (IsIsoLatin1(text, out textAsBytes))
101 | {
102 | isoStringSize += (ulong)rawBytes.Length;
103 | isoStringCount++;
104 | compressedStringSize += (ulong)textAsBytes.Length;
105 | }
106 | // UTF-8 and UTF-16 can both support the same range of text/character values ("Code Points"), they just store it in different ways
107 | // From http://stackoverflow.com/questions/4655250/difference-between-utf-8-and-utf-16/4655335#4655335
108 | // "Both UTF-8 and UTF-16 are variable length (multi-byte) encodings.
109 | // However, in UTF-8 a character may occupy a minimum of 8 bits, while in UTF-16 character length starts with 16 bits."
110 | //else if (IsUTF8(text, out textAsBytes))
111 | //{
112 | // utf8StringSize += (ulong)rawBytes.Length;
113 | // utf8StringCount++;
114 | // compressedStringSize += (ulong)textAsBytes.Length;
115 | //}
116 | else
117 | {
118 | unicodeStringSize += (ulong)rawBytes.Length;
119 | unicodeStringCount++;
120 | uncompressedStringSize += (ulong)rawBytes.Length;
121 | }
122 | }
123 |
124 | Console.ForegroundColor = ConsoleColor.DarkYellow;
125 | Console.WriteLine("\n\"System.String\" memory usage info");
126 | Console.ResetColor();
127 | Console.WriteLine("Overall {0:N0} \"System.String\" objects take up {1:N0} bytes ({2:N2} MB)",
128 | stringObjectCounter, totalStringObjectSize, totalStringObjectSize / 1024.0 / 1024.0);
129 | Console.WriteLine("Of this underlying byte arrays (as Unicode) take up {0:N0} bytes ({1:N2} MB)",
130 | byteArraySize, byteArraySize / 1024.0 / 1024.0);
131 | Console.WriteLine("Remaining data (object headers, other fields, etc) are {0:N0} bytes ({1:N2} MB), at {2:0.##} bytes per object\n",
132 | totalStringObjectSize - byteArraySize,
133 | (totalStringObjectSize - byteArraySize) / 1024.0 / 1024.0,
134 | (totalStringObjectSize - byteArraySize) / (double)stringObjectCounter);
135 |
136 | Console.ForegroundColor = ConsoleColor.DarkYellow;
137 | Console.WriteLine("Actual Encoding that the \"System.String\" could be stored as (with corresponding data size)");
138 | Console.ResetColor();
139 | Console.WriteLine(" {0,15:N0} bytes ({1,8:N0} strings) as ASCII", asciiStringSize, asciiStringCount);
140 | Console.WriteLine(" {0,15:N0} bytes ({1,8:N0} strings) as ISO-8859-1 (Latin-1)", isoStringSize, isoStringCount);
141 | //Console.WriteLine(" {0,15:N0} bytes ({1,8:N0} strings) are UTF-8", utf8StringSize, utf8StringCount);
142 | Console.WriteLine(" {0,15:N0} bytes ({1,8:N0} strings) as Unicode", unicodeStringSize, unicodeStringCount);
143 | Console.WriteLine("Total: {0:N0} bytes (expected: {1:N0}{2})\n",
144 | asciiStringSize + isoStringSize + unicodeStringSize, byteArraySize,
145 | (asciiStringSize + isoStringSize + unicodeStringSize != byteArraySize) ? " - ERROR" : "");
146 |
147 | Console.ForegroundColor = ConsoleColor.DarkYellow;
148 | Console.WriteLine("Compression Summary:");
149 | Console.ResetColor();
150 | Console.WriteLine(" {0,15:N0} bytes Compressed (to ISO-8859-1 (Latin-1))", compressedStringSize);
151 | Console.WriteLine(" {0,15:N0} bytes Uncompressed (as Unicode)", uncompressedStringSize);
152 | Console.WriteLine(" {0,15:N0} bytes EXTRA to enable compression (1-byte field, per \"System.String\" object)", stringObjectCounter);
153 | var totalBytesUsed = compressedStringSize + uncompressedStringSize + stringObjectCounter;
154 | var totalBytesSaved = byteArraySize - totalBytesUsed;
155 | Console.WriteLine("\nTotal Usage: {0:N0} bytes ({1:N2} MB), compared to {2:N0} ({3:N2} MB) before compression",
156 | totalBytesUsed, totalBytesUsed / 1024.0 / 1024.0,
157 | byteArraySize, byteArraySize / 1024.0 / 1024.0);
158 | Console.WriteLine("Total Saving: {0:N0} bytes ({1:N2} MB)\n", totalBytesSaved, totalBytesSaved / 1024.0 / 1024.0);
159 | }
160 |
161 | // By default the encoder just replaces the invalid characters, so force it to throw an exception
162 | private static Encoding asciiEncoder = Encoding.GetEncoding(Encoding.ASCII.EncodingName, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
163 | private static Encoding isoLatin1Encoder = Encoding.GetEncoding("ISO-8859-1", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
164 |
165 | private static bool IsASCII(string text, out byte[] textAsBytes)
166 | {
167 | var unicodeBytes = Encoding.Unicode.GetBytes(text);
168 | try
169 | {
170 | textAsBytes = Encoding.Convert(Encoding.Unicode, asciiEncoder, unicodeBytes);
171 | return true;
172 | }
173 | catch (EncoderFallbackException /*efEx*/)
174 | {
175 | textAsBytes = null;
176 | return false;
177 | }
178 | }
179 |
180 | private static bool IsIsoLatin1(string text, out byte[] textAsBytes)
181 | {
182 | var unicodeBytes = Encoding.Unicode.GetBytes(text);
183 | try
184 | {
185 | textAsBytes = Encoding.Convert(Encoding.Unicode, isoLatin1Encoder, unicodeBytes);
186 | return true;
187 | }
188 | catch (EncoderFallbackException /*efEx*/)
189 | {
190 | textAsBytes = null;
191 | return false;
192 | }
193 | }
194 |
195 | private static bool IsUTF8(string text, out byte[] textAsBytes)
196 | {
197 | var unicodeBytes = Encoding.Unicode.GetBytes(text);
198 | try
199 | {
200 | textAsBytes = Encoding.Convert(Encoding.Unicode, Encoding.UTF8, unicodeBytes);
201 | return true;
202 | }
203 | catch (EncoderFallbackException /*efEx*/)
204 | {
205 | textAsBytes = null;
206 | return false;
207 | }
208 | }
209 |
210 | private static ClrRuntime CreateRuntime(DataTarget target)
211 | {
212 | string dacLocation = null;
213 | foreach (ClrInfo version in target.ClrVersions)
214 | {
215 | Console.WriteLine("Found CLR Version: " + version.Version.ToString());
216 | dacLocation = LoadCorrectDacForMemoryDump(version);
217 | }
218 |
219 | var runtimeInfo = target.ClrVersions[0]; // just using the first runtime
220 | ClrRuntime runtime = null;
221 | try
222 | {
223 | if (string.IsNullOrEmpty(dacLocation))
224 | {
225 | Console.WriteLine(dacLocation);
226 | runtime = runtimeInfo.CreateRuntime();
227 | }
228 | else
229 | {
230 | runtime = runtimeInfo.CreateRuntime(dacLocation);
231 | }
232 | }
233 | catch (InvalidOperationException ex)
234 | {
235 | Console.WriteLine("\n" + ex);
236 | Console.ForegroundColor = ConsoleColor.Red;
237 | Console.WriteLine("\nEnsure that this program is compliled for the same architecture as the memory dump (i.e. 32-bit or 64-bit)");
238 | Console.WriteLine(String.Format(".NET Memory Dump Heap Analyser is compiled as {0}-bit\n", Environment.Is64BitProcess ? "64" : "32"));
239 | Console.ResetColor();
240 | return null;
241 | }
242 | catch (Exception ex)
243 | {
244 | Console.WriteLine("\n" + ex);
245 | Console.WriteLine("\nUnable to process the Memory Dump file!?");
246 | return null;
247 | }
248 |
249 | return runtime;
250 | }
251 |
252 | private static string LoadCorrectDacForMemoryDump(ClrInfo version)
253 | {
254 | // First try the main location, i.e.:
255 | // C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscordacwks.dll
256 | if (version.LocalMatchingDac != null && File.Exists(version.LocalMatchingDac))
257 | {
258 | Console.WriteLine("\nDac already exists on the local machine at:\n{0}", version.LocalMatchingDac);
259 | return version.LocalMatchingDac;
260 | }
261 |
262 | // Location: \symbols\mscordacwks_amd64_amd64_4.0.30319.18444.dll\52717f9a96b000\mscordacwks_amd64_amd64_4.0.30319.18444.dll
263 | ModuleInfo dacInfo = version.DacInfo;
264 | var dacLocation = string.Format(@"{0}symbols\{1}\{2:x}{3:x}\{4}",
265 | Path.GetTempPath(),
266 | dacInfo.FileName,
267 | dacInfo.TimeStamp,
268 | dacInfo.FileSize,
269 | dacInfo.FileName);
270 |
271 | if (File.Exists(dacLocation))
272 | {
273 | Console.WriteLine("\nDac {0} already exists in the local cache at:\n{1}", dacInfo.FileName, dacLocation);
274 | return dacLocation;
275 | }
276 | else
277 | {
278 | Console.ForegroundColor = ConsoleColor.Red;
279 | Console.WriteLine("\nUnable to find copy of the dac ({0}) on the local machine.", dacInfo);
280 | Console.WriteLine("Expected location:\n" + dacLocation);
281 | Console.WriteLine("\nIt will now be downloaded from the Microsoft Symbol Server.");
282 | Console.WriteLine("Press if you are okay with this, if not you can just type Ctrl-C to exit");
283 | Console.ResetColor();
284 | Console.ReadLine();
285 |
286 | string downloadLocation = version.TryDownloadDac(new SymbolNotification());
287 | Console.WriteLine("Downloaded a copy of the dac to:\n" + downloadLocation);
288 | return downloadLocation;
289 | }
290 | }
291 |
292 | private static void PrintMemoryRegionInfo(ClrRuntime runtime)
293 | {
294 | Console.ForegroundColor = ConsoleColor.DarkYellow;
295 | Console.WriteLine("\nMemory Region Information");
296 | Console.ResetColor();
297 | var seperator = "------------------------------------------------";
298 | Console.WriteLine(seperator);
299 | Console.WriteLine("{0,24} {1,4} {2,15}", "Type", "Count", "Total Size (MB)");
300 | Console.WriteLine(seperator);
301 | foreach (var region in (from r in runtime.EnumerateMemoryRegions()
302 | //where r.Type != ClrMemoryRegionType.ReservedGCSegment
303 | group r by r.Type into g
304 | let total = g.Sum(p => (uint)p.Size)
305 | orderby total descending
306 | select new
307 | {
308 | TotalSize = total,
309 | Count = g.Count(),
310 | Type = g.Key
311 | }))
312 | {
313 | Console.WriteLine("{0,24} {1,5} {2,15:N2}", region.Type.ToString(), region.Count, region.TotalSize / 1024.0 / 1024.0);
314 | }
315 | Console.WriteLine(seperator);
316 | }
317 |
318 | private static void PrintGCHeapInfo(ClrRuntime runtime, ClrHeap heap)
319 | {
320 | Console.ForegroundColor = ConsoleColor.DarkYellow;
321 | Console.WriteLine("\nGC Heap Information - {0}", runtime.ServerGC ? "Server" : "Workstation");
322 | Console.ResetColor();
323 |
324 | var seperator = "-----------------------------------------------------------";
325 | var heapSegmentInfo = from seg in heap.Segments
326 | group seg by seg.ProcessorAffinity into g
327 | orderby g.Key
328 | select new
329 | {
330 | Heap = g.Key,
331 | Size = g.Sum(p => (uint)p.Length)
332 | };
333 | foreach (var item in heapSegmentInfo)
334 | {
335 | Console.WriteLine(seperator);
336 | Console.ForegroundColor = ConsoleColor.DarkGreen;
337 | Console.WriteLine("Heap {0,2}: {1,12:N0} bytes ({2:N2} MB) in use",
338 | item.Heap, item.Size, item.Size / 1024.0 / 1024.0);
339 | Console.ResetColor();
340 | Console.WriteLine(seperator);
341 | Console.WriteLine("{0,12} {1,12} {2,14} {3,14}", "Type", "Size (MB)", "Committed (MB)", "Reserved (MB)");
342 | Console.WriteLine(seperator);
343 | var heapSegments = heap.Segments.Where(s => s.ProcessorAffinity == item.Heap)
344 | .OrderBy(s => s.IsEphemeral ? 1 : 0 + (s.IsLarge ? 3 : 2));
345 | foreach (ClrSegment segment in heapSegments)
346 | {
347 | string type;
348 | if (segment.IsEphemeral)
349 | type = "Ephemeral";
350 | else if (segment.IsLarge)
351 | type = "Large";
352 | else
353 | type = "Gen2";
354 |
355 | Console.WriteLine("{0,12} {1,12:N2} {2,14:N2} {3,14:N2}",
356 | type,
357 | (segment.End - segment.Start) / 1024.0 / 1024.0, // This is the same as segment.Length
358 | (segment.CommittedEnd - segment.Start) / 1024.0 / 1024.0,
359 | (segment.ReservedEnd - segment.Start) / 1024.0 / 1024.0);
360 | }
361 | }
362 | Console.WriteLine(seperator);
363 | Console.WriteLine("Total (across all heaps): {0:N0} bytes ({1:N2} MB)",
364 | heap.Segments.Sum(s => (long)s.Length),
365 | heap.Segments.Sum(s => (long)s.Length) / 1024.0 / 1024.0);
366 | Console.WriteLine(seperator);
367 | }
368 |
369 | private static void VerifyStringObjectSize(ClrRuntime runtime, ClrType type, ulong obj, string text)
370 | {
371 | var objSize = type.GetSize(obj);
372 | var objAsHex = obj.ToString("x");
373 | var rawBytes = Encoding.Unicode.GetBytes(text);
374 |
375 | if (runtime.ClrInfo.Version.Major == 2)
376 | {
377 | // This only works in .NET 2.0, the "m_array_Length" field was removed in .NET 4.0
378 | var arrayLength = (int)type.GetFieldByName("m_arrayLength").GetValue(obj);
379 | var stringLength = (int)type.GetFieldByName("m_stringLength").GetValue(obj);
380 |
381 | var calculatedSize = (((ulong)arrayLength - 1) * 2) + HeaderSize;
382 | if (objSize != calculatedSize)
383 | {
384 | Console.WriteLine("Object Size Mismatch: arrayLength: {0,4}, stringLength: {1,4}, Object Size: {2,4}, Object: {3} -> \n\"{4}\"",
385 | arrayLength, stringLength, objSize, objAsHex, text);
386 | }
387 | }
388 | else
389 | {
390 | // In .NET 4.0 we can do a more normal check, i.e. ("object size" - "raw byte array length") should equal the expected header size
391 | var theRest = objSize - (ulong)rawBytes.Length;
392 | if (theRest != HeaderSize)
393 | {
394 | Console.WriteLine("Object Size Mismatch: Raw Bytes Length: {0,4}, Object Size: {1,4}, Object: {2} -> \n\"{3}\"",
395 | rawBytes.Length, objSize, objAsHex, text);
396 | }
397 | }
398 | }
399 | }
400 |
401 | class SymbolNotification : ISymbolNotification
402 | {
403 | public void DecompressionComplete(string localPath)
404 | {
405 | Console.WriteLine("DecompressionComplete: " + (localPath ?? ""));
406 | }
407 |
408 | public void DownloadComplete(string localPath, bool requiresDecompression)
409 | {
410 | Console.WriteLine("DecompressionComplete: " + (localPath ?? ""));
411 | }
412 |
413 | public void DownloadProgress(int bytesDownloaded)
414 | {
415 | Console.WriteLine("DownloadProgress: bytesDownloaded = " + bytesDownloaded);
416 | }
417 |
418 | public void FoundSymbolInCache(string localPath)
419 | {
420 | Console.WriteLine("FoundSymbolInCache: " + (localPath ?? ""));
421 | }
422 |
423 | public void FoundSymbolOnPath(string url)
424 | {
425 | Console.WriteLine("FoundSymbolOnPath: " + (url ?? ""));
426 | }
427 |
428 | public void ProbeFailed(string url)
429 | {
430 | Console.WriteLine("ProbeFailed: " + (url ?? ""));
431 | }
432 | }
433 | }
434 |
--------------------------------------------------------------------------------