pebble/tools/profiling/profile.py

216 lines
8.2 KiB
Python
Raw Permalink Normal View History

# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import subprocess
import sys
import argparse
import pexpect
import time
import pprint
import operator
##########################################################################################
class Profiler(object):
""" This class encapsulates the profiling functionality """
#####################################################################################
def __init__(self, openocd_port, pblprog_board):
self.openocd_port = openocd_port
self.pblprog_board = pblprog_board
#####################################################################################
def capture(self, filename, total_secs, sample_period_ms):
""" Sample the program counter from the board attached via openocd """
if self.pblprog_board:
num_samples = 0
from pebble import programmer
with programmer.get_device(self.pblprog_board, reset=False, frequency=25E6) as prog:
pcs = prog.profile(total_secs)
num_samples = sum(pcs.values())
elapsed_time = total_secs
else:
# Setup telnet connection and discard banner
try:
import telnetlib
tn = telnetlib.Telnet('localhost', self.openocd_port)
except Exception:
print "Could not connect to OpenOCD via telnet"
sys.exit()
# Discard banner
time.sleep(1)
tn.read_some()
# Capture the samples
num_samples = total_secs * 1000 / sample_period_ms
print "Capturing %d samples..." % (num_samples)
n = 0
pcs = dict()
sample_period_sec = sample_period_ms * 0.001
start_time = time.time()
last_sample_time = time.time()
while(n < num_samples):
if (n % 1000) == 0:
print "%d..." % (n),
sys.stdout.flush()
# Space the samples apart by the requested amount
elapsed = time.time() - last_sample_time
if (elapsed < sample_period_sec):
time.sleep(sample_period_sec - elapsed)
last_sample_time = time.time()
# mdw = read word, 0xE000101C is the PC sampling reg
tn.write('mdw 0xE000101C 1\n')
_ = tn.read_until(': ')
res = tn.read_eager().split(' ')[0]
res = '0x' + res
pcs[res] = pcs.get(res, 0) + 1
n += 1
elapsed_time = time.time() - start_time
# Save results to a file
print "\n%d samples collected in %f seconds (%f ms/sample)" % (num_samples, elapsed_time,
elapsed_time * 1000.0 / num_samples)
print "Saving samples to %s..." % (filename)
with open(filename, 'w') as out:
for k,v in pcs.iteritems():
out.write("%s %d\n" % (k, v))
#####################################################################################
def view(self, filename, elf):
ADDR2LINE = "arm-none-eabi-addr2line"
output = dict()
# Read in the raw samples
pcs = dict()
total_samples = 0
with open(filename) as f:
for line in f:
addr, count = line.strip().split()
pcs[addr] = int(count)
total_samples += int(count)
# Lookup the method name, filename and line number for each PC
cmdline = [ADDR2LINE, "-e", elf, "-a", "-f"]
cmdline += pcs.keys()
output = subprocess.check_output(cmdline)
# Collect results by method name and by file:line
method_count = dict()
file_line_count = dict()
# Map file:line to method
method_lookup = dict()
# Map PC to file:line
file_line_lookup = dict()
items = output.splitlines()
result_count = len(items) / 3
for i in range(result_count):
addr, method, file_line = items[i*3:(i+1)*3]
if file_line == '?':
file_line = addr
else:
file_line = file_line.split('/')[-1]
count = pcs[addr]
method_count[method] = method_count.get(method, 0) + count
file_line_count[file_line] = file_line_count.get(file_line, 0) + count
method_lookup[file_line] = method
file_line_lookup[addr] = file_line
# Print results in sorted order
format_str = "%-64s %7.2f%% %5d"
print "\n\nSamples grouped by method: "
print "---------------------------------------------------------------"
sorted_values = sorted(method_count.items(), key=operator.itemgetter(1), reverse=True)
for k, v in sorted_values:
print format_str % (k, v * 100.0 / total_samples, v)
print "\n\nSamples grouped by file:line: "
print "---------------------------------------------------------------"
sorted_values = sorted(file_line_count.items(), key=operator.itemgetter(1), reverse=True)
for k, v in sorted_values:
k = "{0} ({1:.24})".format(k, method_lookup[k])
print format_str % (k, v * 100.0 / total_samples, v)
print "\n\nSamples grouped by address "
print "---------------------------------------------------------------"
sorted_values = sorted(pcs.items(), key=operator.itemgetter(1), reverse=True)
for k, v in sorted_values:
k = "{0} ({1:.48})".format(k, file_line_lookup[k])
print format_str % (k, v * 100.0 / total_samples, v)
####################################################################################################
if __name__ == '__main__':
# Collect our command line arguments
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('action', choices=['capture', 'view'], default='capture',
help="Action to perform. capture - capture sampled data from attached " +
"board using openocd or pblprog; view - view results from a prior " +
"capture session")
parser.add_argument('--name', type=str, default='profile.txt',
help="Name of file to store capture data into (for 'capture' action) or " +
"to evaluate (for 'print' action)")
parser.add_argument('--pblprog_board', type=str, default=None,
help="The board to target with pblprog")
parser.add_argument('--openocd_port', type=int, default=4444, help="Telnet port of openocd")
parser.add_argument('--secs', type=int, default=1,
help="Number of seconds to capture profile information for")
parser.add_argument('--sample_period_ms', type=int, default=2,
help="Number of milliseconds between each sample")
parser.add_argument('--elf', type=str, default='build/src/fw/tintin_fw.elf',
help="Path to the elf file to use to lookup method and file names")
parser.add_argument('--debug', action='store_true', help="Turn on debug logging")
args = parser.parse_args()
level = logging.INFO
if args.debug:
level = logging.DEBUG
logging.basicConfig(level=level)
if args.sample_period_ms <= 0:
raise Exception("sample_period_ms must be >= 0")
profiler = Profiler(args.openocd_port, args.pblprog_board)
if args.action == 'capture':
profiler.capture(filename=args.name, total_secs=args.secs,
sample_period_ms=args.sample_period_ms)
elif args.action == 'view':
profiler.view(filename=args.name, elf=args.elf)