Skip to content

Commit

Permalink
Allow creation of flame graphs from UIforETW menu
Browse files Browse the repository at this point in the history
Flame graphs can be handy for summarizing CPU usage but if they are too
difficult to use they won't get used. This is a first attempt at making
them more accessible. This adds a context menu that creates a flame
graph for the busiest thread.

Future work could include specifying a time range, process of interest,
thread of interest, number of threads to report on, or threshold - so
many choices! It would be better if this was burned in to WPA, but oh
well.

This also rearranges the context menu by putting scripts in a sub-menu
and compression in another - it was getting too big.
  • Loading branch information
randomascii committed Oct 9, 2015
1 parent 7af36b1 commit 2c9d77f
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 19 deletions.
5 changes: 4 additions & 1 deletion UIforETW/Resource.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,16 @@
#define ID_PASTEOVERRIDE 32805
#define ID_TRACES_IDENTIFYCHROMEPROCESSES 32807
#define ID_TRACES_OPENTRACEINEXPERIENCEANALYZER 32808
#define ID_TRACES_SCRIPTS 32809
#define ID_SCRIPTS_CREATEFLAMEGRAPH 32810
#define ID_TRACES_TRACECOMPRESSION 32811

// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 136
#define _APS_NEXT_COMMAND_VALUE 32809
#define _APS_NEXT_COMMAND_VALUE 32812
#define _APS_NEXT_CONTROL_VALUE 1034
#define _APS_NEXT_SYMED_VALUE 101
#endif
Expand Down
19 changes: 13 additions & 6 deletions UIforETW/UIforETW.rc
Original file line number Diff line number Diff line change
Expand Up @@ -244,14 +244,21 @@ BEGIN
MENUITEM "Open trace in Media e&Xperience Analyzer", ID_TRACES_OPENTRACEINEXPERIENCEANALYZER
MENUITEM "&Delete trace\tDelete", ID_TRACES_DELETETRACE
MENUITEM "&Rename trace\tF2", ID_TRACES_RENAMETRACE
MENUITEM "&ETW compress trace (Win 8.0+)", ID_TRACES_COMPRESSTRACE
MENUITEM "&Zip compress trace", ID_TRACES_ZIPCOMPRESSTRACE
MENUITEM "ETW Compress all &traces (Win 8.0+)", ID_TRACES_COMPRESSTRACES
MENUITEM "Zip compress all traces", ID_TRACES_ZIPCOMPRESSALLTRACES
MENUITEM "&Browse folder", ID_TRACES_BROWSEFOLDER
MENUITEM "&Strip Chrome symbols", ID_TRACES_STRIPCHROMESYMBOLS
MENUITEM "&Identify Chrome processes", ID_TRACES_IDENTIFYCHROMEPROCESSES
MENUITEM "Trace path to &clipboard\tCtrl+C", ID_TRACES_TRACEPATHTOCLIPBOARD
POPUP "Trace compression"
BEGIN
MENUITEM "&ETW compress trace (Win 8.0+)", ID_TRACES_COMPRESSTRACE
MENUITEM "&Zip compress trace", ID_TRACES_ZIPCOMPRESSTRACE
MENUITEM "ETW Compress all &traces (Win 8.0+)", ID_TRACES_COMPRESSTRACES
MENUITEM "Zip compress all traces", ID_TRACES_ZIPCOMPRESSALLTRACES
END
POPUP "&Scripts"
BEGIN
MENUITEM "&Strip Chrome symbols", ID_TRACES_STRIPCHROMESYMBOLS
MENUITEM "&Identify Chrome processes", ID_TRACES_IDENTIFYCHROMEPROCESSES
MENUITEM "Create &flame graph", ID_SCRIPTS_CREATEFLAMEGRAPH
END
END
END

Expand Down
21 changes: 21 additions & 0 deletions UIforETW/UIforETWDlg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,7 @@ void CUIforETWDlg::OnContextMenu(CWnd* pWnd, CPoint point)
ID_TRACES_STRIPCHROMESYMBOLS,
ID_TRACES_IDENTIFYCHROMEPROCESSES,
ID_TRACES_TRACEPATHTOCLIPBOARD,
ID_SCRIPTS_CREATEFLAMEGRAPH,
};

for (const auto& id : disableList)
Expand Down Expand Up @@ -1677,6 +1678,9 @@ void CUIforETWDlg::OnContextMenu(CWnd* pWnd, CPoint point)
IdentifyChromeProcesses(tracePath);
UpdateNotesState();
break;
case ID_SCRIPTS_CREATEFLAMEGRAPH:
CreateFlameGraph(tracePath);
break;
case ID_TRACES_TRACEPATHTOCLIPBOARD:
// Comma delimited for easy pasting into DOS commands.
SetClipboardText(L"\"" + tracePath + L"\"");
Expand Down Expand Up @@ -1878,6 +1882,23 @@ void CUIforETWDlg::IdentifyChromeProcesses(const std::wstring& traceFilename)
}


void CUIforETWDlg::CreateFlameGraph(const std::wstring& traceFilename)
{
outputPrintf(L"\nCreating CPU Usage (Sampled) flame graph of busiest process ((requires python, perl and flamegraph.pl)...\n");
std::wstring pythonPath = FindPython();
if (!pythonPath.empty())
{
ChildProcess child(pythonPath);
std::wstring args = L" -u \"" + GetExeDir() + L"xperf_to_collapsedstacks.py\" \"" + traceFilename + L"\"";
child.Run(bShowCommands_, GetFilePart(pythonPath) + args);
}
else
{
outputPrintf(L"Couldn't find python.\n");
}
}


void CUIforETWDlg::PreprocessTrace(const std::wstring& traceFilename)
{
if (bChromeDeveloper_)
Expand Down
1 change: 1 addition & 0 deletions UIforETW/UIforETWDlg.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ class CUIforETWDlg : public CDialogEx
void StripChromeSymbols(const std::wstring& traceFilename);
void IdentifyChromeProcesses(const std::wstring& traceFilename);
void PreprocessTrace(const std::wstring& traceFilename);
void CreateFlameGraph(const std::wstring& traceFilename);
void LaunchTraceViewer(const std::wstring traceFilename, const std::wstring viewerPath);
void SaveNotesIfNeeded();
void ShutdownTasks();
Expand Down
3 changes: 3 additions & 0 deletions bin/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ symsrv.dll
# setting, even though it seems like it should.
*.iobj
*.ipdb

# Exclude flamegraph.pl so that users can install it
flamegraph.pl
55 changes: 43 additions & 12 deletions bin/xperf_to_collapsedstacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,38 @@
import os

# How many threads to create collapsed stacks for.
numToShow = 5
numToShow = 1

if len(sys.argv) < 4:
print "Usage: %s trace.etl begin end"
scriptPath = os.path.abspath(sys.argv[0])
scriptDir = os.path.split(scriptPath)[0]
flameGraphPath = os.path.join(scriptDir, "flamegraph.pl")
if not os.path.exists(flameGraphPath):
print "Couldn't find %s. Download it from https://github.com/brendangregg/FlameGraph/blob/master/flamegraph.pl" % flameGraphPath

if len(sys.argv) < 2:
print "Usage: %s trace.etl begin end" % sys.argv[0]
print "Begin and end specify the time range to be processed, in seconds."
sys.exit(0)

etlFilename = sys.argv[1]
textFilename = os.path.join(os.environ["temp"], os.path.split(etlFilename)[1])
begin = int(float(sys.argv[2])*1e6)
end = int(float(sys.argv[3])*1e6)
textFilename = textFilename.replace(".etl", "_%d_%d.txt" % (begin, end))
if len(sys.argv) >= 4:
begin = int(float(sys.argv[2])*1e6)
end = int(float(sys.argv[3])*1e6)
textFilename = textFilename.replace(".etl", "_%d_%d.txt" % (begin, end))
command = 'xperf -i "%s" -symbols -o "%s" -a dumper -stacktimeshifting -range %d %d' % (etlFilename, textFilename, begin, end)
else:
textFilename = textFilename.replace(".etl", ".txt")
command = 'xperf -i "%s" -symbols -o "%s" -a dumper -stacktimeshifting' % (etlFilename, textFilename)

# Optimization for when working on the script -- don't reprocess the xperf
# trace if the parameters haven't changed.
if True or not os.path.exists(textFilename):
command = 'xperf -i "%s" -symbols -o "%s" -a dumper -stacktimeshifting -range %d %d' % (etlFilename, textFilename, begin, end)
if not os.path.exists(textFilename):
print command
for line in os.popen(command).readlines():
print line
else:
print "Using existing intermediate file - '%s'." % textFilename
print "Using cached xperf results in '%s'." % textFilename

# Create regular expressions for parsing the SampledProfile and Stack lines.
# SampledProfile, TimeStamp, Process Name ( PID), ThreadID, PrgrmCtr, CPU, ThreadStartImage!Function, Image!Function, Count, SampledProfile type
Expand Down Expand Up @@ -134,7 +144,10 @@
process = unresolvedSamples.pop(key)
# Create a fully specified process and thread name under which these
# samples will be stored.
processAndThread = "%s(%d)" % (process.replace(" ", "_"), ThreadID)
#processAndThread = "%s(%d)" % (process.replace(" ", "_"), ThreadID)
processAndThread = "%s_%d" % (process.replace(" ", "_"), ThreadID)
processAndThread = processAndThread.replace("(", "")
processAndThread = processAndThread.replace(")", "")
# Convert the array of stack lines to a single call stack string.
stackSummary = []
for entry in stack:
Expand Down Expand Up @@ -195,14 +208,32 @@

print "Found %d samples from %d threads." % (totalSamples, len(samples))

tempDir = os.environ["temp"]
count = 0
for numSamples, processAndThread in sortedThreads[:numToShow]:
threadSamples = samples[processAndThread]
outputName = "%s_collapse.txt" % processAndThread
print "Writing %d samples to %s" % (numSamples, outputName)
#outputName = "%s_collapse.txt" % processAndThread
outputName = os.path.join(tempDir, "collapsed_stacks_%d.txt" % count)
count += 1
print "Writing %d samples to temporary file %s" % (numSamples, outputName)
sortedStacks = []
for stack in threadSamples:
sortedStacks.append("%s %d\n" % (stack, threadSamples[stack]))
sortedStacks.sort()
out = open(outputName, "wt")
for stack in sortedStacks:
out.write(stack)


destPath = os.path.join(tempDir, "%s.svg" % processAndThread)
title = "CPU Usage flame graph of %s" % processAndThread
perlCommand = 'perl "%s" --title="%s" "%s" >"%s"' % (flameGraphPath, title, outputName, destPath)
#print perlCommand
for line in os.popen(perlCommand).readlines():
print line
resultSize = os.path.getsize(destPath)
if resultSize < 100: # Arbitrary sane minimum
print "Result size is %d bytes - is perl in your path?" % resultSize
else:
os.popen(destPath)
print "Results are in '%s' - they should be auto-opened in the default SVG viewer." % destPath

0 comments on commit 2c9d77f

Please sign in to comment.