├── .gitattributes ├── .gitignore ├── Program.cs ├── Properties └── AssemblyInfo.cs ├── app.config ├── cpu-analyzer.csproj ├── license.txt └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | #packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # Installshield output folder 84 | [Ee]xpress 85 | 86 | # DocProject is a documentation generator add-in 87 | DocProject/buildhelp/ 88 | DocProject/Help/*.HxT 89 | DocProject/Help/*.HxC 90 | DocProject/Help/*.hhc 91 | DocProject/Help/*.hhk 92 | DocProject/Help/*.hhp 93 | DocProject/Help/Html2 94 | DocProject/Help/html 95 | 96 | # Click-Once directory 97 | publish 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | *.Cache 105 | ClientBin 106 | stylecop.* 107 | ~$* 108 | *.dbmdl 109 | Generated_Code #added for RIA/Silverlight projects 110 | 111 | # Backup & report files from converting an old project file to a newer 112 | # Visual Studio version. Backup files are not needed, because we have git ;-) 113 | _UpgradeReport_Files/ 114 | Backup*/ 115 | UpgradeLog*.XML 116 | 117 | 118 | 119 | ############ 120 | ## Windows 121 | ############ 122 | 123 | # Windows image file caches 124 | Thumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | 130 | ############# 131 | ## Python 132 | ############# 133 | 134 | *.py[co] 135 | 136 | # Packages 137 | *.egg 138 | *.egg-info 139 | dist 140 | build 141 | eggs 142 | parts 143 | bin 144 | var 145 | sdist 146 | develop-eggs 147 | .installed.cfg 148 | 149 | # Installer logs 150 | pip-log.txt 151 | 152 | # Unit test / coverage reports 153 | .coverage 154 | .tox 155 | 156 | #Translations 157 | *.mo 158 | 159 | #Mr Developer 160 | .mr.developer.cfg 161 | 162 | # Mac crap 163 | .DS_Store 164 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Microsoft.Samples.Debugging.MdbgEngine; 6 | 7 | using System.Diagnostics; 8 | using System.Threading; 9 | using System.Runtime.InteropServices; 10 | using System.Security.Cryptography; 11 | 12 | namespace cpu_analyzer { 13 | 14 | class ThreadSnapshotStats { 15 | 16 | public long TotalKernelTime { get; set; } 17 | public long TotalUserTime { get; set; } 18 | public int ThreadId { get; set; } 19 | 20 | public List CommonStack { get; set; } 21 | 22 | public static ThreadSnapshotStats FromSnapshots(IEnumerable snapshots) { 23 | ThreadSnapshotStats stats = new ThreadSnapshotStats(); 24 | 25 | stats.ThreadId = snapshots.First().Id; 26 | stats.TotalKernelTime = snapshots.Last().KernelTime - snapshots.First().KernelTime; 27 | stats.TotalUserTime = snapshots.Last().UserTime - snapshots.First().UserTime; 28 | 29 | stats.CommonStack = snapshots.First().StackTrace.ToList(); 30 | 31 | 32 | foreach (var stack in snapshots.Select(_ => _.StackTrace.ToList())) { 33 | while (stats.CommonStack.Count > stack.Count) { 34 | stats.CommonStack.RemoveAt(0); 35 | } 36 | 37 | while (stats.CommonStack.Count > 0 && stack.Count > 0 && stats.CommonStack[0] != stack[0]) { 38 | stats.CommonStack.RemoveAt(0); 39 | stack.RemoveAt(0); 40 | } 41 | } 42 | 43 | return stats; 44 | } 45 | 46 | } 47 | 48 | class ThreadSnapshot { 49 | 50 | [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] 51 | private static extern bool GetThreadTimes(IntPtr handle, out long creation, out long exit, out long kernel, out long user); 52 | 53 | 54 | private ThreadSnapshot () 55 | { 56 | } 57 | 58 | public int Id { get; set; } 59 | public DateTime Time { get; set; } 60 | public long KernelTime { get; set; } 61 | public long UserTime { get; set; } 62 | 63 | public List StackTrace {get; set;} 64 | 65 | static MD5CryptoServiceProvider md5Provider = new MD5CryptoServiceProvider(); 66 | 67 | public static Guid GetMD5(string str) 68 | { 69 | lock (md5Provider) 70 | { 71 | return new Guid(md5Provider.ComputeHash(Encoding.Unicode.GetBytes(str))); 72 | } 73 | } 74 | 75 | public IEnumerable> StackHashes 76 | { 77 | get 78 | { 79 | List> rval = new List>(); 80 | 81 | List trace = new List(); 82 | 83 | foreach (var item in ((IEnumerable)StackTrace).Reverse()) 84 | { 85 | trace.Insert(0, item); 86 | var traceString = string.Join(Environment.NewLine, trace); 87 | yield return Tuple.Create(GetMD5(traceString), traceString); 88 | } 89 | } 90 | } 91 | 92 | public static ThreadSnapshot GetThreadSnapshot(MDbgThread thread) { 93 | var snapshot = new ThreadSnapshot(); 94 | 95 | snapshot.Id = thread.Id; 96 | 97 | long creation, exit, kernel, user; 98 | GetThreadTimes(thread.CorThread.Handle, out creation, out exit, out kernel, out user); 99 | 100 | snapshot.KernelTime = kernel; 101 | snapshot.UserTime = user; 102 | snapshot.StackTrace = new List(); 103 | 104 | foreach (MDbgFrame frame in thread.Frames) { 105 | try { 106 | snapshot.StackTrace.Add(frame.Function.FullName); 107 | } catch { 108 | // no frame, so ignore 109 | } 110 | } 111 | 112 | return snapshot; 113 | } 114 | } 115 | 116 | class Program { 117 | 118 | enum ParseState { 119 | Unknown, Samples, Interval 120 | } 121 | 122 | static void Usage() { 123 | Console.WriteLine("Usage: cpu-analyzer ProcessName|PID [options]"); 124 | Console.WriteLine(); 125 | Console.WriteLine(" /S indicates how many samples to take (default:10)"); 126 | Console.WriteLine(" /I the interval between samples in milliseconds (default:1000)"); 127 | Console.WriteLine(""); 128 | Console.WriteLine("Example: cpu-analyzer aspnet_wp /s 60 /i 500"); 129 | Console.WriteLine(" Take 60 samples once every 500 milliseconds"); 130 | 131 | } 132 | 133 | static void Main(string[] args) { 134 | 135 | if (args.Length < 1) { 136 | Usage(); 137 | return; 138 | } 139 | 140 | int samples = 10; 141 | int sampleInterval = 1000; 142 | 143 | 144 | var state = ParseState.Unknown; 145 | foreach (var arg in args.Skip(1)) { 146 | switch (state) { 147 | case ParseState.Unknown: 148 | if (arg.ToLower() == "/s") { 149 | state = ParseState.Samples; 150 | } else if (arg.ToLower() == "/i") { 151 | state = ParseState.Interval; 152 | } else { 153 | Usage(); 154 | return; 155 | } 156 | break; 157 | case ParseState.Samples: 158 | if (!Int32.TryParse(arg, out samples)) { 159 | Usage(); 160 | return; 161 | } 162 | state = ParseState.Unknown; 163 | break; 164 | case ParseState.Interval: 165 | if (!Int32.TryParse(arg, out sampleInterval)) { 166 | Usage(); 167 | return; 168 | } 169 | state = ParseState.Unknown; 170 | break; 171 | default: 172 | break; 173 | } 174 | } 175 | 176 | string pidOrProcess = args[0]; 177 | 178 | 179 | var stats = new Dictionary>(); 180 | var debugger = new MDbgEngine(); 181 | int pid = -1; 182 | 183 | var processes = Process.GetProcessesByName(pidOrProcess); 184 | if (processes.Length < 1) { 185 | try { 186 | pid = Int32.Parse(pidOrProcess); 187 | } catch { 188 | Console.WriteLine("Error: could not find any processes with that name or pid"); 189 | return; 190 | } 191 | } else { 192 | if (processes.Length > 1) { 193 | Console.WriteLine("Warning: multiple processes share that name, attaching to the first"); 194 | } 195 | pid = processes[0].Id; 196 | } 197 | 198 | 199 | MDbgProcess attached = null; 200 | try { 201 | attached = debugger.Attach(pid); 202 | } catch(Exception e) { 203 | Console.WriteLine("Error: failed to attach to process: " + e); 204 | return; 205 | } 206 | 207 | attached.Go().WaitOne(); 208 | 209 | for (int i = 0; i < samples; i++) { 210 | 211 | foreach (MDbgThread thread in attached.Threads) { 212 | var snapshot = ThreadSnapshot.GetThreadSnapshot(thread); 213 | List snapshots; 214 | if (!stats.TryGetValue(snapshot.Id, out snapshots)) { 215 | snapshots = new List(); 216 | stats[snapshot.Id] = snapshots; 217 | } 218 | 219 | snapshots.Add(snapshot); 220 | } 221 | 222 | attached.Go(); 223 | Thread.Sleep(sampleInterval); 224 | attached.AsyncStop().WaitOne(); 225 | } 226 | 227 | attached.Detach().WaitOne(); 228 | 229 | // perform basic analysis to see which are the top N stack traces observed, 230 | // weighted on cost 231 | 232 | Dictionary costs = new Dictionary(); 233 | Dictionary stacks = new Dictionary(); 234 | 235 | foreach (var stat in stats.Values) 236 | { 237 | long prevTime = -1; 238 | foreach (var snapshot in stat) 239 | { 240 | long time = snapshot.KernelTime + snapshot.UserTime; 241 | if (prevTime != -1) 242 | { 243 | foreach (var tuple in snapshot.StackHashes) 244 | { 245 | if (costs.ContainsKey(tuple.Item1)) 246 | { 247 | costs[tuple.Item1] += time - prevTime; 248 | } 249 | else 250 | { 251 | costs[tuple.Item1] = time - prevTime; 252 | stacks[tuple.Item1] = tuple.Item2; 253 | } 254 | } 255 | } 256 | prevTime = time; 257 | } 258 | } 259 | 260 | Console.WriteLine("Most expensive stacks"); 261 | Console.WriteLine("------------------------------------"); 262 | foreach (var group in costs.OrderByDescending(p => p.Value).GroupBy(p => p.Value)) 263 | { 264 | List stacksToShow = new List(); 265 | 266 | foreach (var pair in group.OrderByDescending(p => stacks[p.Key].Length)) 267 | { 268 | if (!stacksToShow.Any(s => s.Contains(stacks[pair.Key]))) 269 | { 270 | stacksToShow.Add(stacks[pair.Key]); 271 | } 272 | } 273 | 274 | foreach (var stack in stacksToShow) 275 | { 276 | Console.WriteLine(stack); 277 | Console.WriteLine("===> Cost ({0})", group.Key); 278 | Console.WriteLine(); 279 | } 280 | } 281 | 282 | 283 | var offenders = stats.Values 284 | .Select(_ => ThreadSnapshotStats.FromSnapshots(_)) 285 | .OrderBy(stat => stat.TotalKernelTime + stat.TotalUserTime) 286 | .Reverse(); 287 | 288 | foreach (var stat in offenders) { 289 | Console.WriteLine("------------------------------------"); 290 | Console.WriteLine(stat.ThreadId); 291 | Console.WriteLine("Kernel: {0} User: {1}", stat.TotalKernelTime, stat.TotalUserTime); 292 | foreach (var method in stat.CommonStack) { 293 | Console.WriteLine(method); 294 | } 295 | Console.WriteLine("Other Stacks:"); 296 | var prev = new List(); 297 | foreach (var trace in stats[stat.ThreadId].Select(_ => _.StackTrace)) { 298 | if (!prev.SequenceEqual(trace)) { 299 | Console.WriteLine(); 300 | foreach (var method in trace) { 301 | Console.WriteLine(method); 302 | } 303 | } else { 304 | Console.WriteLine(""); 305 | } 306 | prev = trace; 307 | } 308 | Console.WriteLine("------------------------------------"); 309 | 310 | } 311 | 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /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("cpu-analyzer")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Microsoft")] 12 | [assembly: AssemblyProduct("cpu-analyzer")] 13 | [assembly: AssemblyCopyright("Copyright © Microsoft 2009")] 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("6ce51f7a-6ade-49f3-9db0-cfaab308df76")] 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 | -------------------------------------------------------------------------------- /app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /cpu-analyzer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | 9.0.30729 7 | 2.0 8 | {CD9413CE-67BB-435E-B060-256C6486EF07} 9 | Exe 10 | Properties 11 | cpu_analyzer 12 | cpu-analyzer 13 | v4.0 14 | 512 15 | 16 | 17 | 18 | 19 | 3.5 20 | 21 | publish\ 22 | true 23 | Disk 24 | false 25 | Foreground 26 | 7 27 | Days 28 | false 29 | false 30 | true 31 | 0 32 | 1.0.0.%2a 33 | false 34 | false 35 | true 36 | 37 | 38 | true 39 | full 40 | false 41 | bin\Debug\ 42 | DEBUG;TRACE 43 | prompt 44 | 4 45 | AllRules.ruleset 46 | 47 | 48 | pdbonly 49 | true 50 | bin\Release\ 51 | TRACE 52 | prompt 53 | 4 54 | AllRules.ruleset 55 | 56 | 57 | 58 | 59 | 3.5 60 | 61 | 62 | 3.5 63 | 64 | 65 | 3.5 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {04EF9865-E1B1-403D-802B-E4FAEA50A634} 77 | corapi 78 | 79 | 80 | {634B90A2-0344-44C5-83F0-06180B2FDA05} 81 | mdbgeng 82 | 83 | 84 | {1511F9E6-6AC4-4F3F-863D-3430469A7335} 85 | mdbgext 86 | 87 | 88 | {3991AB6C-468B-4C28-95FC-3188CFB34180} 89 | NativeDebugWrappers 90 | 91 | 92 | {C18D303B-2C55-43EB-A3DF-39CF3FB1D447} 93 | raw 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | False 102 | .NET Framework 3.5 SP1 Client Profile 103 | false 104 | 105 | 106 | False 107 | .NET Framework 3.5 SP1 108 | true 109 | 110 | 111 | False 112 | Windows Installer 3.1 113 | true 114 | 115 | 116 | 117 | 124 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2012 - Sam Saffron 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | cpu analyzer 2 | 3 | see: http://samsaffron.com/archive/2009/11/11/Diagnosing+runaway+CPU+in+a+Net+production+application 4 | 5 | requires: http://www.microsoft.com/en-au/download/details.aspx?id=19621 to compile 6 | --------------------------------------------------------------------------------