Skip to content

Commit

Permalink
Update xperf_to_collapsedstacks.py to Python 3
Browse files Browse the repository at this point in the history
xperf_to_collapsedstacks.py was written for Python 2 and was never
updated until now. This fixes its print statements and also puts its
string constants into standard single-quote form. With the python 3
fixes it now works under Python 2 or 3.

While the code still works under Python 2 that may not be maintained
forever.
  • Loading branch information
randomascii committed May 22, 2021
1 parent 7e4bca1 commit 5d5e97e
Showing 1 changed file with 50 additions and 44 deletions.
94 changes: 50 additions & 44 deletions bin/xperf_to_collapsedstacks.py
Expand Up @@ -30,6 +30,9 @@
column labels and the rest is the fields from the profile - process, threadID, and '/' separated
stack.
"""

from __future__ import print_function

import sys
import os
import time
Expand All @@ -38,8 +41,8 @@

# Our usage of subprocess seems to require Python 2.7+
if sys.version_info.major == 2 and sys.version_info.minor < 7:
print("Your python version is too old - 2.7 or higher required.")
print("Python version is %s" % sys.version)
print('Your python version is too old - 2.7 or higher required.')
print('Python version is %s' % sys.version)
sys.exit(0)

parser = argparse.ArgumentParser(description='Process xperf ETL file and generate flamegraph(s).')
Expand All @@ -49,7 +52,7 @@
parser.add_argument('-b', '--begin', help='Time range. Begin from the specified value. In seconds.', type=float)
parser.add_argument('-e', '--end', help='Time range. End at the specified value. In seconds.', type=float)
parser.add_argument('-o', '--output', help='Path to directory where output will written into. Default is system TEMP directory', type=str)
parser.set_defaults(output=os.environ["temp"])
parser.set_defaults(output=os.environ['temp'])
parser.add_argument('-n', '--numshow', help='Number of top processes to generate flame graph for. Default is 1', type=int)
parser.set_defaults(numshow=1)
parser.add_argument('-d', '--dontopen', help='Do not open the generated SVG file automatically. Default is open', action='store_true')
Expand All @@ -65,38 +68,38 @@

scriptPath = os.path.abspath(sys.argv[0])
scriptDir = os.path.split(scriptPath)[0]
flameGraphPath = os.path.join(scriptDir, "flamegraph.pl")
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
print('Couldn\'t find "%s". Download it from https://github.com/brendangregg/FlameGraph/blob/master/flamegraph.pl' % flameGraphPath)
sys.exit(0)

profilePath = os.path.join(scriptDir, "ExportCPUUsageSampled.wpaProfile")
profilePath = os.path.join(scriptDir, 'ExportCPUUsageSampled.wpaProfile')
if not os.path.exists(profilePath):
print "Couldn't find \"%s\". This should be part of the UIforETW repo and releases" % profilePath
print('Couldn\'t find "%s". This should be part of the UIforETW repo and releases' % profilePath)
sys.exit(0)

#if not os.environ.has_key("_NT_SYMBOL_PATH"):
# print "_NT_SYMBOL_PATH is not set. Exiting."
#if not '_NT_SYMBOL_PATH' in os.environ:
# print('_NT_SYMBOL_PATH is not set. Exiting.')
# sys.exit(0)

if os.environ.has_key("ProgramFiles(x86)"):
progFilesx86 = os.environ["ProgramFiles(x86)"]
if 'ProgramFiles(x86)' in os.environ:
progFilesx86 = os.environ['ProgramFiles(x86)']
else:
progFilesx86 = os.environ["ProgramFiles"]
wpaExporterPath = os.path.join(progFilesx86, r"Windows Kits\10\Windows Performance Toolkit\wpaexporter.EXE")
progFilesx86 = os.environ['ProgramFiles']
wpaExporterPath = os.path.join(progFilesx86, r'Windows Kits\10\Windows Performance Toolkit\wpaexporter.EXE')
if not os.path.exists(wpaExporterPath):
print "Couldn't find \"%s\". Make sure WPT 10 is installed." % wpaExporterPath
print('Couldn\'t find "%s". Make sure WPT 10 is installed.' % wpaExporterPath)
sys.exit(0)

if args.begin and args.end:
wpaCommand = r'"%s" "%s" -range %ss %ss -profile "%s" -symbols' % (wpaExporterPath, args.etlFilename, args.begin, args.end, profilePath)
else:
wpaCommand = r'"%s" "%s" -profile "%s" -symbols' % (wpaExporterPath, args.etlFilename, profilePath)

print "> %s" % wpaCommand
start = time.clock()
print subprocess.check_output(wpaCommand, stderr=subprocess.STDOUT)
print "Elapsed time for wpaexporter: %1.3f s" % (time.clock() - start)
print('> %s' % wpaCommand)
start = time.time()
print(subprocess.check_output(wpaCommand, stderr=subprocess.STDOUT))
print('Elapsed time for wpaexporter: %1.3f s' % (time.time() - start))


# This dictionary of dictionaries accumulates sample data. The first key is
Expand All @@ -109,37 +112,37 @@

# Process all of the lines in the output of wpaexporter, skipping the first line
# which is just the column names.
csvName = "CPU_Usage_(Sampled)_Randomascii_Export.csv"
csvName = 'CPU_Usage_(Sampled)_Randomascii_Export.csv'
for line in open(csvName).readlines()[1:]:
line = line.strip()
firstCommaPos = line.find(",")
firstCommaPos = line.find(',')
process = line[:firstCommaPos]
if processList and process.split(' ')[0].lower() not in processList:
continue
secondCommaPos = line.find(",", firstCommaPos + 1)
secondCommaPos = line.find(',', firstCommaPos + 1)
threadID = line[firstCommaPos + 1 : secondCommaPos]
stackSummary = line[secondCommaPos + 1:]
if stackSummary == "n/a":
if stackSummary == 'n/a':
continue
# Since we are using semicolon separators we can't have semicolons
# in the function names.
stackSummary = stackSummary.replace(";", ":")
stackSummary = stackSummary.replace(';', ':')
# Spaces seem like a bad idea also.
stackSummary = stackSummary.replace(" ", "_")
stackSummary = stackSummary.replace(' ', '_')
# Having single-quote characters in the call stacks gives flamegraph.pl heartburn.
# Replace them with back ticks.
stackSummary = stackSummary.replace("'", "`")
stackSummary = stackSummary.replace('\'', '`')
# Double-quote characters also cause problems. Remove them.
stackSummary = stackSummary.replace('"', "")
stackSummary = stackSummary.replace('"', '')
# Remove <PDB_not_found> labels
stackSummary = stackSummary.replace("<PDB_not_found>", "Unknown")
stackSummary = stackSummary.replace('<PDB_not_found>', 'Unknown')
# Convert the wpaexporter stack separators to flamegraph stack separators
stackSummary = stackSummary.replace("/", ";")
#stackSummary = "A;B;C"
stackSummary = stackSummary.replace('/', ';')
#stackSummary = 'A;B;C'

processAndThread = "%s_%s" % (process.replace(" ", "_"), threadID)
processAndThread = processAndThread.replace("(", "")
processAndThread = processAndThread.replace(")", "")
processAndThread = '%s_%s' % (process.replace(' ', '_'), threadID)
processAndThread = processAndThread.replace('(', '')
processAndThread = processAndThread.replace(')', '')
# Add the record to the nested dictionary, and increment the count
# if it already exists.
if not processAndThread in samples:
Expand All @@ -162,40 +165,43 @@
sortedThreads.sort() # Put the threads in order by number of samples
sortedThreads.reverse() # Put the thread with the most samples first

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

if len(processList)>0:
numToShow = len(sortedThreads)

count = 0
for numSamples, processAndThread in sortedThreads[:numToShow]:
threadSamples = samples[processAndThread]
outputName = os.path.join(args.output, "collapsed_stacks_%d.txt" % count)
outputName = os.path.join(args.output, 'collapsed_stacks_%d.txt' % count)
count += 1
print "Writing %d samples to temporary file %s" % (numSamples, outputName)
print('Writing %d samples to temporary file %s' % (numSamples, outputName))
sortedStacks = []
for stack in threadSamples:
sortedStacks.append("%s %d\n" % (stack, threadSamples[stack]))
sortedStacks.append('%s %d\n' % (stack, threadSamples[stack]))
sortedStacks.sort()
# Some versions of perl (the version that ships with Chromium's depot_tools
# for one) can't handle reading files with CRLF line endings, so write the
# file as binary to avoid line-ending translation.
out = open(outputName, "wb")
# file with Linux-style line endings. This won't work with Python 2.
try:
out = open(outputName, 'w', newline='\n')
except TypeError:
out = open(outputName, 'wb')
for stack in sortedStacks:
out.write(stack)
# Force the file closed so that the results will be available when we the
# perl script is run.
out.close()

destPath = os.path.join(args.output, "%s.svg" % processAndThread)
title = "CPU Usage flame graph of %s" % processAndThread
destPath = os.path.join(args.output, '%s.svg' % processAndThread)
title = 'CPU Usage flame graph of %s' % processAndThread
perlCommand = 'perl "%s" --title="%s" "%s"' % (flameGraphPath, title, outputName)
print "> %s" % perlCommand
print('> %s' % perlCommand)
svgOutput = subprocess.check_output(perlCommand)
if len(svgOutput) > 100:
open(destPath, "wt").write(svgOutput)
open(destPath, 'wt').write(svgOutput.decode())
if not args.dontopen:
os.popen(destPath)
print 'Results are in "%s" - they should be auto-opened in the default SVG viewer.' % destPath
print('Results are in "%s" - they should be auto-opened in the default SVG viewer.' % destPath)
else:
print "Result size is %d bytes - is perl in your path?" % len(svgOutput)
print('Result size is %d bytes - is perl in your path?' % len(svgOutput))

0 comments on commit 5d5e97e

Please sign in to comment.