├── app.config ├── packages.config ├── .gitattributes ├── license.txt ├── readme.md ├── cpu-analyzer.sln ├── Properties └── AssemblyInfo.cs ├── .gitignore ├── cpu-analyzer.csproj └── Program.cs /app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | Forked from Sam Saffron's code which seems adandoned, see: http://samsaffron.com/archive/2009/11/11/Diagnosing+runaway+CPU+in+a+Net+production+application 3 | 4 | All required dependencies are now added via NuGET, no need to link MS libraried. 5 | 6 | Usage: 7 | 8 | `cpu-analyzer ProcessName|PID [options]` 9 | 10 | /S indicates how many samples to take (default:10) 11 | 12 | /I the interval between samples in milliseconds (default:1000) 13 | 14 | Example: `cpu-analyzer w3wp.exe /s 60 /i 500` - "Take 60 samples once every 500 milliseconds" 15 | 16 | The tool output can be quite lengthy, so use it like this: 17 | 18 | `cpu-analyser.exe w3wp.exe >> log.txt` 19 | 20 | We used this tool many times to successfully find CPU "leaks" in our [helpdesk app](https://jitbit.github.com/helpdesk/) on production server, which has hundreds of background threads. 21 | -------------------------------------------------------------------------------- /cpu-analyzer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.30501.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cpu-analyzer", "cpu-analyzer.csproj", "{CD9413CE-67BB-435E-B060-256C6486EF07}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {CD9413CE-67BB-435E-B060-256C6486EF07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {CD9413CE-67BB-435E-B060-256C6486EF07}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {CD9413CE-67BB-435E-B060-256C6486EF07}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {CD9413CE-67BB-435E-B060-256C6486EF07}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | packages\Microsoft.Samples.Debugging.CorApi.1.4.0.0\lib\Microsoft.Samples.Debugging.CorApi.dll 59 | 60 | 61 | packages\Microsoft.Samples.Debugging.CorApi.1.4.0.0\lib\Microsoft.Samples.Debugging.CorApi.NativeApi.dll 62 | 63 | 64 | packages\Microsoft.Samples.Debugging.MdbgEngine.1.4.0.0\lib\Microsoft.Samples.Debugging.MdbgEngine.dll 65 | 66 | 67 | packages\Microsoft.Samples.Debugging.CorApi.1.4.0.0\lib\Microsoft.Samples.Debugging.Native.dll 68 | 69 | 70 | 71 | 3.5 72 | 73 | 74 | 3.5 75 | 76 | 77 | 3.5 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | False 93 | .NET Framework 3.5 SP1 Client Profile 94 | false 95 | 96 | 97 | False 98 | .NET Framework 3.5 SP1 99 | true 100 | 101 | 102 | False 103 | Windows Installer 3.1 104 | true 105 | 106 | 107 | 108 | 115 | -------------------------------------------------------------------------------- /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 | try 105 | { 106 | foreach (MDbgFrame frame in thread.Frames) 107 | { 108 | try 109 | { 110 | snapshot.StackTrace.Add(frame.Function.FullName); 111 | } 112 | catch 113 | { 114 | // no frame, so ignore 115 | } 116 | } 117 | } 118 | catch 119 | { 120 | // ignore the "cannot attach to thred" errors, we can't do anything about it, but leaving unhandled destroys the whole w3wp in production :( 121 | } 122 | 123 | return snapshot; 124 | } 125 | } 126 | 127 | class Program { 128 | 129 | enum ParseState { 130 | Unknown, Samples, Interval 131 | } 132 | 133 | static void Usage() { 134 | Console.WriteLine("Usage: cpu-analyzer ProcessName|PID [options]"); 135 | Console.WriteLine(); 136 | Console.WriteLine(" /S indicates how many samples to take (default:10)"); 137 | Console.WriteLine(" /I the interval between samples in milliseconds (default:1000)"); 138 | Console.WriteLine(""); 139 | Console.WriteLine("Example: cpu-analyzer aspnet_wp /s 60 /i 500"); 140 | Console.WriteLine(" Take 60 samples once every 500 milliseconds"); 141 | 142 | } 143 | 144 | static void Main(string[] args) { 145 | 146 | if (args.Length < 1) { 147 | Usage(); 148 | return; 149 | } 150 | 151 | int samples = 10; 152 | int sampleInterval = 1000; 153 | 154 | 155 | var state = ParseState.Unknown; 156 | foreach (var arg in args.Skip(1)) { 157 | switch (state) { 158 | case ParseState.Unknown: 159 | if (arg.ToLower() == "/s") { 160 | state = ParseState.Samples; 161 | } else if (arg.ToLower() == "/i") { 162 | state = ParseState.Interval; 163 | } else { 164 | Usage(); 165 | return; 166 | } 167 | break; 168 | case ParseState.Samples: 169 | if (!Int32.TryParse(arg, out samples)) { 170 | Usage(); 171 | return; 172 | } 173 | state = ParseState.Unknown; 174 | break; 175 | case ParseState.Interval: 176 | if (!Int32.TryParse(arg, out sampleInterval)) { 177 | Usage(); 178 | return; 179 | } 180 | state = ParseState.Unknown; 181 | break; 182 | default: 183 | break; 184 | } 185 | } 186 | 187 | string pidOrProcess = args[0]; 188 | 189 | 190 | var stats = new Dictionary>(); 191 | var debugger = new MDbgEngine(); 192 | int pid = -1; 193 | 194 | var processes = Process.GetProcessesByName(pidOrProcess); 195 | if (processes.Length < 1) { 196 | try { 197 | pid = Int32.Parse(pidOrProcess); 198 | } catch { 199 | Console.WriteLine("Error: could not find any processes with that name or pid"); 200 | return; 201 | } 202 | } else { 203 | if (processes.Length > 1) { 204 | Console.WriteLine("Warning: multiple processes share that name, attaching to the first"); 205 | } 206 | pid = processes[0].Id; 207 | } 208 | 209 | 210 | MDbgProcess attached = null; 211 | try { 212 | attached = debugger.Attach(pid); 213 | } catch(Exception e) { 214 | Console.WriteLine("Error: failed to attach to process: " + e); 215 | return; 216 | } 217 | 218 | attached.Go().WaitOne(); 219 | 220 | for (int i = 0; i < samples; i++) { 221 | 222 | foreach (MDbgThread thread in attached.Threads) { 223 | var snapshot = ThreadSnapshot.GetThreadSnapshot(thread); 224 | List snapshots; 225 | if (!stats.TryGetValue(snapshot.Id, out snapshots)) { 226 | snapshots = new List(); 227 | stats[snapshot.Id] = snapshots; 228 | } 229 | 230 | snapshots.Add(snapshot); 231 | } 232 | 233 | attached.Go(); 234 | Thread.Sleep(sampleInterval); 235 | attached.AsyncStop().WaitOne(); 236 | } 237 | 238 | attached.Detach().WaitOne(); 239 | 240 | // perform basic analysis to see which are the top N stack traces observed, 241 | // weighted on cost 242 | 243 | Dictionary costs = new Dictionary(); 244 | Dictionary stacks = new Dictionary(); 245 | 246 | foreach (var stat in stats.Values) 247 | { 248 | long prevTime = -1; 249 | foreach (var snapshot in stat) 250 | { 251 | long time = snapshot.KernelTime + snapshot.UserTime; 252 | if (prevTime != -1) 253 | { 254 | foreach (var tuple in snapshot.StackHashes) 255 | { 256 | if (costs.ContainsKey(tuple.Item1)) 257 | { 258 | costs[tuple.Item1] += time - prevTime; 259 | } 260 | else 261 | { 262 | costs[tuple.Item1] = time - prevTime; 263 | stacks[tuple.Item1] = tuple.Item2; 264 | } 265 | } 266 | } 267 | prevTime = time; 268 | } 269 | } 270 | 271 | Console.WriteLine("Most expensive stacks"); 272 | Console.WriteLine("------------------------------------"); 273 | foreach (var group in costs.OrderByDescending(p => p.Value).GroupBy(p => p.Value)) 274 | { 275 | List stacksToShow = new List(); 276 | 277 | foreach (var pair in group.OrderByDescending(p => stacks[p.Key].Length)) 278 | { 279 | if (!stacksToShow.Any(s => s.Contains(stacks[pair.Key]))) 280 | { 281 | stacksToShow.Add(stacks[pair.Key]); 282 | } 283 | } 284 | 285 | foreach (var stack in stacksToShow) 286 | { 287 | Console.WriteLine(stack); 288 | Console.WriteLine("===> Cost ({0})", group.Key); 289 | Console.WriteLine(); 290 | } 291 | } 292 | 293 | 294 | var offenders = stats.Values 295 | .Select(_ => ThreadSnapshotStats.FromSnapshots(_)) 296 | .OrderBy(stat => stat.TotalKernelTime + stat.TotalUserTime) 297 | .Reverse(); 298 | 299 | foreach (var stat in offenders) { 300 | Console.WriteLine("------------------------------------"); 301 | Console.WriteLine(stat.ThreadId); 302 | Console.WriteLine("Kernel: {0} User: {1}", stat.TotalKernelTime, stat.TotalUserTime); 303 | foreach (var method in stat.CommonStack) { 304 | Console.WriteLine(method); 305 | } 306 | Console.WriteLine("Other Stacks:"); 307 | var prev = new List(); 308 | foreach (var trace in stats[stat.ThreadId].Select(_ => _.StackTrace)) { 309 | if (!prev.SequenceEqual(trace)) { 310 | Console.WriteLine(); 311 | foreach (var method in trace) { 312 | Console.WriteLine(method); 313 | } 314 | } else { 315 | Console.WriteLine(""); 316 | } 317 | prev = trace; 318 | } 319 | Console.WriteLine("------------------------------------"); 320 | 321 | } 322 | 323 | } 324 | } 325 | } 326 | --------------------------------------------------------------------------------