├── 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 |
--------------------------------------------------------------------------------