summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYabin Cui <yabinc@google.com>2017-08-17 21:30:19 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2017-08-17 21:30:19 +0000
commit001e76be635c208886a5370e51b95913d8c7bb09 (patch)
tree9e5073dec715ece725a77aab769440ab39c01266
parent72dd31f56711a34b9fa1d89859d79142ef888c41 (diff)
parentd3c40e701e2638596bf8bd2d4f8da746e005e144 (diff)
downloadextras-001e76be635c208886a5370e51b95913d8c7bb09.tar.gz
Merge "simpleperf: fix inferno and test content of report.html."
-rw-r--r--simpleperf/scripts/inferno/data_types.py96
-rw-r--r--simpleperf/scripts/inferno/inferno.py106
-rw-r--r--simpleperf/scripts/inferno/script.js2
-rw-r--r--simpleperf/scripts/inferno/svg_renderer.py173
-rw-r--r--simpleperf/scripts/test.py55
5 files changed, 233 insertions, 199 deletions
diff --git a/simpleperf/scripts/inferno/data_types.py b/simpleperf/scripts/inferno/data_types.py
index 80e633f4..4f07ba73 100644
--- a/simpleperf/scripts/inferno/data_types.py
+++ b/simpleperf/scripts/inferno/data_types.py
@@ -25,15 +25,20 @@ class CallSite:
class Thread:
- def __init__(self, tid):
+ def __init__(self, tid, pid):
self.tid = tid
+ self.pid = pid
+ self.name = ""
self.samples = []
- self.flamegraph = {}
+ self.flamegraph = FlameGraphCallSite("root", "", 0)
self.num_samples = 0
+ self.event_count = 0
def add_callchain(self, callchain, symbol, sample):
- chain = []
+ self.name = sample.thread_comm
self.num_samples += 1
+ self.event_count += sample.period
+ chain = []
for j in range(callchain.nr):
entry = callchain.entries[callchain.nr - j - 1]
if entry.ip == 0:
@@ -41,20 +46,7 @@ class Thread:
chain.append(CallSite(entry.ip, entry.symbol.symbol_name, entry.symbol.dso_name))
chain.append(CallSite(sample.ip, symbol.symbol_name, symbol.dso_name))
- self.samples.append(chain)
-
- def collapse_flamegraph(self):
- flamegraph = FlameGraphCallSite("root", "")
- flamegraph.id = 0 # This is used for wasd navigation, 0 = not a valid target.
- self.flamegraph = flamegraph
- for sample in self.samples:
- flamegraph = self.flamegraph
- for callsite in sample:
- flamegraph = flamegraph.get_callsite(callsite.method, callsite.dso)
-
- # Populate root note.
- for node in self.flamegraph.callsites:
- self.flamegraph.num_samples += node.num_samples
+ self.flamegraph.add_callchain(chain, sample.period)
class Process:
@@ -65,51 +57,57 @@ class Process:
self.threads = {}
self.cmd = ""
self.props = {}
- self.args = None
self.num_samples = 0
- def get_thread(self, tid):
- if (tid not in self.threads.keys()):
- self.threads[tid] = Thread(tid)
+ def get_thread(self, tid, pid):
+ if tid not in self.threads.keys():
+ self.threads[tid] = Thread(tid, pid)
return self.threads[tid]
-CALLSITE_COUNTER = 0
-
-
-def get_callsite_id():
- global CALLSITE_COUNTER
- CALLSITE_COUNTER += 1
- toReturn = CALLSITE_COUNTER
- return toReturn
-
class FlameGraphCallSite:
- def __init__(self, method, dso):
- self.callsites = []
+ callsite_counter = 0
+ @classmethod
+ def _get_next_callsite_id(cls):
+ cls.callsite_counter += 1
+ return cls.callsite_counter
+
+ def __init__(self, method, dso, id):
+ self.children = []
self.method = method
self.dso = dso
- self.num_samples = 0
+ self.event_count = 0
self.offset = 0 # Offset allows position nodes in different branches.
- self.id = get_callsite_id()
+ self.id = id
+
+ def weight(self):
+ return float(self.event_count)
+
+ def add_callchain(self, chain, event_count):
+ self.event_count += event_count
+ current = self
+ for callsite in chain:
+ current = current._get_child(callsite)
+ current.event_count += event_count
- def get_callsite(self, name, dso):
- for c in self.callsites:
- if c.equivalent(name, dso):
- c.num_samples += 1
+ def _get_child(self, callsite):
+ for c in self.children:
+ if c._equivalent(callsite.method, callsite.dso):
return c
- callsite = FlameGraphCallSite(name, dso)
- callsite.num_samples = 1
- self.callsites.append(callsite)
- return callsite
+ new_child = FlameGraphCallSite(callsite.method, callsite.dso, self._get_next_callsite_id())
+ self.children.append(new_child)
+ return new_child
- def equivalent(self, method, dso):
+ def _equivalent(self, method, dso):
return self.method == method and self.dso == dso
def get_max_depth(self):
- max = 0
- for c in self.callsites:
- depth = c.get_max_depth()
- if depth > max:
- max = depth
- return max + 1
+ return max([c.get_max_depth() for c in self.children]) + 1 if self.children else 1
+
+ def generate_offset(self, start_offset):
+ self.offset = start_offset
+ child_offset = start_offset
+ for child in self.children:
+ child_offset = child.generate_offset(child_offset)
+ return self.offset + self.event_count
diff --git a/simpleperf/scripts/inferno/inferno.py b/simpleperf/scripts/inferno/inferno.py
index 0b592965..15d7776a 100644
--- a/simpleperf/scripts/inferno/inferno.py
+++ b/simpleperf/scripts/inferno/inferno.py
@@ -47,7 +47,7 @@ from data_types import *
from svg_renderer import *
-def collect_data(args, process):
+def collect_data(args):
app_profiler_args = [sys.executable, "app_profiler.py", "-nb"]
if args.app:
app_profiler_args += ["-p", args.app]
@@ -77,7 +77,6 @@ def collect_data(args, process):
record_arg_str += "-f %d " % args.sample_frequency
log_info("Using frequency sampling (-f %d)." % args.sample_frequency)
record_arg_str += "--duration %d " % args.capture_duration
- process.cmd = " ".join(app_profiler_args) + ' -r "%s"' % record_arg_str
app_profiler_args += ["-r", record_arg_str]
returncode = subprocess.call(app_profiler_args)
return returncode == 0
@@ -93,12 +92,19 @@ def parse_samples(process, args):
lib = ReportLib()
lib.ShowIpForUnknownSymbol()
- if symfs_dir is not None:
+ if symfs_dir:
lib.SetSymfs(symfs_dir)
- if record_file is not None:
+ if record_file:
lib.SetRecordFile(record_file)
- if kallsyms_file is not None:
+ if kallsyms_file:
lib.SetKallsymsFile(kallsyms_file)
+ process.cmd = lib.GetRecordCmd()
+ product_props = lib.MetaInfo().get("product_props")
+ if product_props:
+ tuple = product_props.split(':')
+ process.props['ro.product.manufacturer'] = tuple[0]
+ process.props['ro.product.model'] = tuple[1]
+ process.props['ro.product.name'] = tuple[2]
while True:
sample = lib.GetNextSample()
@@ -107,20 +113,16 @@ def parse_samples(process, args):
break
symbol = lib.GetSymbolOfCurrentSample()
callchain = lib.GetCallChainOfCurrentSample()
- process.get_thread(sample.tid).add_callchain(callchain, symbol, sample)
+ process.get_thread(sample.tid, sample.pid).add_callchain(callchain, symbol, sample)
process.num_samples += 1
- log_info("Parsed %s callchains." % process.num_samples)
-
+ if process.pid == 0:
+ main_threads = [thread for thread in process.threads.values() if thread.tid == thread.pid]
+ if main_threads:
+ process.name = main_threads[0].name
+ process.pid = main_threads[0].pid
-def collapse_callgraphs(process):
- """
- For each thread, collapse all callgraph into one flamegraph.
- :param process: Process object
- :return: None
- """
- for _, thread in process.threads.items():
- thread.collapse_flamegraph()
+ log_info("Parsed %s callchains." % process.num_samples)
def get_local_asset_content(local_path):
@@ -129,13 +131,11 @@ def get_local_asset_content(local_path):
:param local_path: str, filename of local asset
:return: str, the content of local_path
"""
- f = open(os.path.join(os.path.dirname(__file__), local_path), 'r')
- content = f.read()
- f.close()
- return content
+ with open(os.path.join(os.path.dirname(__file__), local_path), 'r') as f:
+ return f.read()
-def output_report(process):
+def output_report(process, args):
"""
Generates a HTML report representing the result of simpleperf sampling as flamegraph
:param process: Process object
@@ -151,19 +151,23 @@ def output_report(process):
f.write('<img height="180" alt = "Embedded Image" src ="data')
f.write(get_local_asset_content("inferno.b64"))
f.write('"/>')
+ process_entry = ("Process : %s (%d)<br/>" % (process.name, process.pid)) if process.pid else ""
+ # TODO: collect capture duration info from perf.data.
+ duration_entry = ("Duration: %s seconds<br/>" % args.capture_duration
+ ) if args.capture_duration else ""
f.write("""<div style='display:inline-block;'>
<font size='8'>
Inferno Flamegraph Report</font><br/><br/>
- Process : %s (%d)<br/>
+ %s
Date&nbsp;&nbsp;&nbsp;&nbsp;: %s<br/>
Threads : %d <br/>
Samples : %d</br>
- Duration: %s seconds<br/>""" % (
- process.name, process.pid,
+ %s""" % (
+ process_entry,
datetime.datetime.now().strftime("%Y-%m-%d (%A) %H:%M:%S"),
len(process.threads),
process.num_samples,
- process.args.capture_duration))
+ duration_entry))
if 'ro.product.model' in process.props:
f.write(
"Machine : %s (%s) by %s<br/>" %
@@ -178,17 +182,17 @@ def output_report(process):
f.write("<script>%s</script>" % get_local_asset_content("script.js"))
# Output tid == pid Thread first.
- main_thread = [x for _, x in process.threads.items() if x.tid == process.pid]
+ main_thread = [x for x in process.threads.values() if x.tid == process.pid]
for thread in main_thread:
- f.write("<br/><br/><b>Main Thread %d (%d samples):</b><br/>\n\n\n\n" % (
- thread.tid, thread.num_samples))
- renderSVG(thread.flamegraph, f, process.args.color, process.args.svg_width)
+ f.write("<br/><br/><b>Main Thread %d (%s) (%d samples):</b><br/>\n\n\n\n" % (
+ thread.tid, thread.name, thread.num_samples))
+ renderSVG(thread.flamegraph, f, args.color, args.svg_width)
- other_threads = [x for _, x in process.threads.items() if x.tid != process.pid]
+ other_threads = [x for x in process.threads.values() if x.tid != process.pid]
for thread in other_threads:
- f.write("<br/><br/><b>Thread %d (%d samples):</b><br/>\n\n\n\n" % (
- thread.tid, thread.num_samples))
- renderSVG(thread.flamegraph, f, process.args.color, process.args.svg_width)
+ f.write("<br/><br/><b>Thread %d (%s) (%d samples):</b><br/>\n\n\n\n" % (
+ thread.tid, thread.name, thread.num_samples))
+ renderSVG(thread.flamegraph, f, args.color, args.svg_width)
f.write("</body>")
f.write("</html>")
@@ -196,17 +200,9 @@ def output_report(process):
return "file://" + filepath
-def generate_flamegraph_offsets(flamegraph):
- rover = flamegraph.offset
- for callsite in flamegraph.callsites:
- callsite.offset = rover
- rover += callsite.num_samples
- generate_flamegraph_offsets(callsite)
-
-
def generate_threads_offsets(process):
- for _, thread in process.threads.items():
- generate_flamegraph_offsets(thread.flamegraph)
+ for thread in process.threads.values():
+ thread.flamegraph.generate_offset(0)
def collect_machine_info(process):
@@ -266,26 +262,26 @@ def main():
parser.add_argument('--disable_adb_root', action='store_true', help="""Force adb to run in non
root mode.""")
args = parser.parse_args()
- process_name = args.app or args.native_program
- process = Process(process_name, 0)
- process.args = args
+ process = Process("", 0)
if not args.skip_collection:
- log_info("Starting data collection stage for process '%s'." % process_name)
- if not collect_data(args, process):
+ process.name = args.app or args.native_program
+ log_info("Starting data collection stage for process '%s'." % process.name)
+ if not collect_data(args):
log_exit("Unable to collect data.")
- try:
- result, output = AdbHelper().run_and_return_output(['shell', 'pidof', process_name])
- if result:
+ result, output = AdbHelper().run_and_return_output(['shell', 'pidof', process.name])
+ if result:
+ try:
process.pid = int(output)
- except:
- raise
+ except:
+ process.pid = 0
collect_machine_info(process)
+ else:
+ args.capture_duration = 0
parse_samples(process, args)
- collapse_callgraphs(process)
generate_threads_offsets(process)
- report_path = output_report(process)
+ report_path = output_report(process, args)
open_report_in_browser(report_path)
log_info("Report generated at '%s'." % report_path)
diff --git a/simpleperf/scripts/inferno/script.js b/simpleperf/scripts/inferno/script.js
index 3288d8be..078100d6 100644
--- a/simpleperf/scripts/inferno/script.js
+++ b/simpleperf/scripts/inferno/script.js
@@ -30,7 +30,7 @@ function adjust_node_text_size(x) {
let width = parseFloat(rect.attributes['width'].value);
// Don't even bother trying to find a best fit. The area is too small.
- if (width < 25) {
+ if (width < 28) {
text.textContent = '';
return;
}
diff --git a/simpleperf/scripts/inferno/svg_renderer.py b/simpleperf/scripts/inferno/svg_renderer.py
index 68559499..b84e058c 100644
--- a/simpleperf/scripts/inferno/svg_renderer.py
+++ b/simpleperf/scripts/inferno/svg_renderer.py
@@ -39,20 +39,20 @@ def getDSOColor(method):
return (r, g, b)
-def getHeatColor(callsite, num_samples):
- r = 245 + 10 * (1 - float(callsite.num_samples) / num_samples)
- g = 110 + 105 * (1 - float(callsite.num_samples) / num_samples)
+def getHeatColor(callsite, total_weight):
+ r = 245 + 10 * (1 - callsite.weight() / total_weight)
+ g = 110 + 105 * (1 - callsite.weight() / total_weight)
b = 100
return (r, g, b)
-def createSVGNode(callsite, depth, f, num_samples, height, color_scheme, nav):
- x = float(callsite.offset) / float(num_samples) * SVG_CANVAS_WIDTH
- y = height - (depth * SVG_NODE_HEIGHT) - SVG_NODE_HEIGHT
- width = float(callsite.num_samples) / float(num_samples) * SVG_CANVAS_WIDTH
+def createSVGNode(callsite, depth, f, total_weight, height, color_scheme, nav):
+ x = float(callsite.offset) / total_weight * SVG_CANVAS_WIDTH
+ y = height - (depth + 1) * SVG_NODE_HEIGHT
+ width = callsite.weight() / total_weight * SVG_CANVAS_WIDTH
method = callsite.method.replace(">", "&gt;").replace("<", "&lt;")
- if (width <= 0):
+ if width <= 0:
return
if color_scheme == "dso":
@@ -60,119 +60,103 @@ def createSVGNode(callsite, depth, f, num_samples, height, color_scheme, nav):
elif color_scheme == "legacy":
r, g, b = getLegacyColor(method)
else:
- r, g, b = getHeatColor(callsite, num_samples)
+ r, g, b = getHeatColor(callsite, total_weight)
- r_border = (r - 50)
- if r_border < 0:
- r_border = 0
-
- g_border = (g - 50)
- if g_border < 0:
- g_border = 0
-
- b_border = (b - 50)
- if (b_border < 0):
- b_border = 0
+ r_border, g_border, b_border = [max(0, color - 50) for color in [r, g, b]]
f.write(
- '<g id=%d class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="%s"> \n\
- <title>%s | %s (%d samples: %3.2f%%)</title>\n \
- <rect x="%f" y="%f" ox="%f" oy="%f" width="%f" owidth="%f" height="15.0" ofill="rgb(%d,%d,%d)" \
- fill="rgb(%d,%d,%d)" style="stroke:rgb(%d,%d,%d)"/>\n \
- <text x="%f" y="%f" font-size="%d" font-family="Monospace"></text>\n \
- </g>\n' %
+ """<g id=%d class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="%s">
+ <title>%s | %s (%.0f events: %3.2f%%)</title>
+ <rect x="%f" y="%f" ox="%f" oy="%f" width="%f" owidth="%f" height="15.0"
+ ofill="rgb(%d,%d,%d)" fill="rgb(%d,%d,%d)" style="stroke:rgb(%d,%d,%d)"/>
+ <text x="%f" y="%f" font-size="%d" font-family="Monospace"></text>
+ </g>""" %
(callsite.id,
- ','.join(
- str(x) for x in nav),
- method,
- callsite.dso,
- callsite.num_samples,
- callsite.num_samples /
- float(num_samples) *
- 100,
- x,
- y,
- x,
- y,
- width,
- width,
- r,
- g,
- b,
- r,
- g,
- b,
- r_border,
- g_border,
- b_border,
- x +
- 2,
- y +
- 12,
- FONT_SIZE))
-
-
-def renderSVGNodes(flamegraph, depth, f, num_samples, height, color_scheme):
- for i, callsite in enumerate(flamegraph.callsites):
+ ','.join(str(x) for x in nav),
+ method,
+ callsite.dso,
+ callsite.weight(),
+ callsite.weight() / total_weight * 100,
+ x,
+ y,
+ x,
+ y,
+ width,
+ width,
+ r,
+ g,
+ b,
+ r,
+ g,
+ b,
+ r_border,
+ g_border,
+ b_border,
+ x + 2,
+ y + 12,
+ FONT_SIZE))
+
+
+def renderSVGNodes(flamegraph, depth, f, total_weight, height, color_scheme):
+ for i, child in enumerate(flamegraph.children):
# Prebuild navigation target for wasd
if i == 0:
left_index = 0
else:
- left_index = flamegraph.callsites[i - 1].id
+ left_index = flamegraph.children[i - 1].id
- if i == len(flamegraph.callsites) - 1:
+ if i == len(flamegraph.children) - 1:
right_index = 0
else:
- right_index = flamegraph.callsites[i + 1].id
+ right_index = flamegraph.children[i + 1].id
- up_index = 0
- max_up = 0
- for upcallsite in callsite.callsites:
- if upcallsite.num_samples > max_up:
- max_up = upcallsite.num_samples
- up_index = upcallsite.id
+ up_index = max(child.children, key=lambda x: x.weight()).id if child.children else 0
# up, left, down, right
nav = [up_index, left_index, flamegraph.id, right_index]
- createSVGNode(callsite, depth, f, num_samples, height, color_scheme, nav)
+ createSVGNode(child, depth, f, total_weight, height, color_scheme, nav)
# Recurse down
- renderSVGNodes(callsite, depth + 1, f, num_samples, height, color_scheme)
+ renderSVGNodes(child, depth + 1, f, total_weight, height, color_scheme)
def renderSearchNode(f):
f.write(
- '<rect id="search_rect" style="stroke:rgb(0,0,0);" onclick="search(this);" class="t" rx="10" ry="10" \
- x="%d" y="10" width="80" height="30" fill="rgb(255,255,255)""/> \
- <text id="search_text" class="t" x="%d" y="30" onclick="search(this);">Search</text>\n' %
- (SVG_CANVAS_WIDTH - 95, SVG_CANVAS_WIDTH - 80))
+ """<rect id="search_rect" style="stroke:rgb(0,0,0);" onclick="search(this);" class="t"
+ rx="10" ry="10" x="%d" y="10" width="80" height="30" fill="rgb(255,255,255)""/>
+ <text id="search_text" class="t" x="%d" y="30" onclick="search(this);">Search</text>
+ """ % (SVG_CANVAS_WIDTH - 95, SVG_CANVAS_WIDTH - 80))
def renderUnzoomNode(f):
f.write(
- '<rect id="zoom_rect" style="display:none;stroke:rgb(0,0,0);" class="t" onclick="unzoom(this);" \
- rx="10" ry="10" x="10" y="10" width="80" height="30" fill="rgb(255,255,255)"/> \
- <text id="zoom_text" style="display:none;" class="t" x="19" y="30" \
- onclick="unzoom(this);">Zoom out</text>\n'
+ """<rect id="zoom_rect" style="display:none;stroke:rgb(0,0,0);" class="t"
+ onclick="unzoom(this);" rx="10" ry="10" x="10" y="10" width="80" height="30"
+ fill="rgb(255,255,255)"/>
+ <text id="zoom_text" style="display:none;" class="t" x="19" y="30"
+ onclick="unzoom(this);">Zoom out</text>"""
)
def renderInfoNode(f):
f.write(
- '<clipPath id="info_clip_path"> <rect id="info_rect" style="stroke:rgb(0,0,0);" \
- rx="10" ry="10" x="120" y="10" width="%d" height="30" fill="rgb(255,255,255)"/></clipPath> \
- <rect id="info_rect" style="stroke:rgb(0,0,0);" \
- rx="10" ry="10" x="120" y="10" width="%d" height="30" fill="rgb(255,255,255)"/> \
- <text clip-path="url(#info_clip_path)" id="info_text" x="128" y="30"></text>\n' % (SVG_CANVAS_WIDTH - 335, SVG_CANVAS_WIDTH - 325)
+ """<clipPath id="info_clip_path"> <rect id="info_rect" style="stroke:rgb(0,0,0);"
+ rx="10" ry="10" x="120" y="10" width="%d" height="30" fill="rgb(255,255,255)"/>
+ </clipPath>
+ <rect id="info_rect" style="stroke:rgb(0,0,0);"
+ rx="10" ry="10" x="120" y="10" width="%d" height="30" fill="rgb(255,255,255)"/>
+ <text clip-path="url(#info_clip_path)" id="info_text" x="128" y="30"></text>
+ """ % (SVG_CANVAS_WIDTH - 335, SVG_CANVAS_WIDTH - 325)
)
def renderPercentNode(f):
f.write(
- '<rect id="percent_rect" style="stroke:rgb(0,0,0);" \
- rx="10" ry="10" x="%d" y="10" width="82" height="30" fill="rgb(255,255,255)"/> \
- <text id="percent_text" text-anchor="end" x="%d" y="30">100.00%%</text>\n' % (SVG_CANVAS_WIDTH - (95 * 2), SVG_CANVAS_WIDTH - (125))
+ """<rect id="percent_rect" style="stroke:rgb(0,0,0);"
+ rx="10" ry="10" x="%d" y="10" width="82" height="30" fill="rgb(255,255,255)"/>
+ <text id="percent_text" text-anchor="end" x="%d" y="30">100.00%%</text>
+ """ % (SVG_CANVAS_WIDTH - (95 * 2), SVG_CANVAS_WIDTH - (125))
)
@@ -180,14 +164,17 @@ def renderSVG(flamegraph, f, color_scheme, width):
global SVG_CANVAS_WIDTH
SVG_CANVAS_WIDTH = width
height = (flamegraph.get_max_depth() + 2) * SVG_NODE_HEIGHT
- f.write('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" \
- width="%d" height="%d" style="border: 1px solid black;" \
- onload="adjust_text_size(this);" rootid="%d">\n' % (SVG_CANVAS_WIDTH, height, flamegraph.callsites[0].id))
- f.write('<defs > <linearGradient id="background_gradiant" y1="0" y2="1" x1="0" x2="0" > \
- <stop stop-color="#eeeeee" offset="5%" /> <stop stop-color="#efefb1" offset="90%" /> </linearGradient> </defs>')
- f.write('<rect x="0.0" y="0" width="%d" height="%d" fill="url(#background_gradiant)" />' %
- (SVG_CANVAS_WIDTH, height))
- renderSVGNodes(flamegraph, 0, f, flamegraph.num_samples, height, color_scheme)
+ f.write("""<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
+ width="%d" height="%d" style="border: 1px solid black;"
+ onload="adjust_text_size(this);" rootid="%d">
+ """ % (SVG_CANVAS_WIDTH, height, flamegraph.children[0].id))
+ f.write("""<defs > <linearGradient id="background_gradiant" y1="0" y2="1" x1="0" x2="0" >
+ <stop stop-color="#eeeeee" offset="5%" /> <stop stop-color="#efefb1" offset="90%" />
+ </linearGradient> </defs>""")
+ f.write("""<rect x="0.0" y="0" width="%d" height="%d" fill="url(#background_gradiant)" />
+ """ % (SVG_CANVAS_WIDTH, height))
+ renderSVGNodes(flamegraph, 0, f, flamegraph.weight(), height, color_scheme)
renderSearchNode(f)
renderUnzoomNode(f)
renderInfoNode(f)
diff --git a/simpleperf/scripts/test.py b/simpleperf/scripts/test.py
index 942243cc..7efd44ce 100644
--- a/simpleperf/scripts/test.py
+++ b/simpleperf/scripts/test.py
@@ -142,7 +142,7 @@ class TestExampleBase(TestBase):
def cleanupTestFiles(cls):
remove("binary_cache")
remove("annotated_files")
- #remove("perf.data")
+ remove("perf.data")
remove("report.txt")
remove("pprof.profile")
@@ -215,6 +215,23 @@ class TestExampleBase(TestBase):
fulfilled[i] = True
self.assertEqual(len(fulfilled), sum([int(x) for x in fulfilled]), fulfilled)
+ def check_inferno_report_html(self, check_entries):
+ file = "report.html"
+ self.check_exist(file=file)
+ with open(file, 'r') as fh:
+ data = fh.read()
+ fulfilled = [False for x in check_entries]
+ for line in data.split('\n'):
+ # each entry is a (function_name, min_percentage) pair.
+ for i, entry in enumerate(check_entries):
+ if fulfilled[i] or line.find(entry[0]) == -1:
+ continue
+ m = re.search(r'(\d+\.\d+)%', line)
+ if m and float(m.group(1)) >= entry[1]:
+ fulfilled[i] = True
+ break
+ self.assertEqual(fulfilled, [True for x in check_entries])
+
def common_test_app_profiler(self):
self.run_cmd(["app_profiler.py", "-h"])
remove("binary_cache")
@@ -306,8 +323,16 @@ class TestExamplePureJava(TestExampleBase):
def test_app_profiler_with_ctrl_c(self):
if is_windows():
return
+ # `adb root` and `adb unroot` may consumes more time than 3 sec. So
+ # do it in advance to make sure ctrl-c happens when recording.
+ if self.adb_root:
+ self.adb.switch_to_root()
+ else:
+ self.adb._unroot()
args = [sys.executable, "app_profiler.py", "--app", self.package_name,
"-r", "--duration 10000", "-nc"]
+ if not self.adb_root:
+ args.append("--disable_adb_root")
subproc = subprocess.Popen(args)
time.sleep(3)
@@ -348,6 +373,10 @@ class TestExamplePureJava(TestExampleBase):
def test_inferno(self):
self.common_test_inferno()
+ self.run_app_profiler()
+ self.run_cmd([inferno_script, "-sc"])
+ self.check_inferno_report_html(
+ [('com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()', 80)])
class TestExamplePureJavaRoot(TestExampleBase):
@@ -390,6 +419,12 @@ class TestExamplePureJavaTraceOffCpu(TestExampleBase):
("line 24", 20, 0),
("line 32", 20, 0)])
self.run_cmd([inferno_script, "-sc"])
+ self.check_inferno_report_html(
+ [('com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.run() ', 80),
+ ('com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.RunFunction()',
+ 20),
+ ('com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.SleepFunction(long)',
+ 20)])
class TestExampleWithNative(TestExampleBase):
@@ -435,6 +470,9 @@ class TestExampleWithNative(TestExampleBase):
def test_inferno(self):
self.common_test_inferno()
+ self.run_app_profiler()
+ self.run_cmd([inferno_script, "-sc"])
+ self.check_inferno_report_html([('BusyLoopThread', 80)])
class TestExampleWithNativeRoot(TestExampleBase):
@@ -478,6 +516,9 @@ class TestExampleWithNativeTraceOffCpu(TestExampleBase):
("line 73", 20, 0),
("line 83", 20, 0)])
self.run_cmd([inferno_script, "-sc"])
+ self.check_inferno_report_html([('SleepThread', 80),
+ ('RunFunction', 20),
+ ('SleepFunction', 20)])
class TestExampleWithNativeJniCall(TestExampleBase):
@@ -580,6 +621,11 @@ class TestExampleOfKotlin(TestExampleBase):
def test_inferno(self):
self.common_test_inferno()
+ self.run_app_profiler()
+ self.run_cmd([inferno_script, "-sc"])
+ self.check_inferno_report_html(
+ [('com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()',
+ 80)])
class TestExampleOfKotlinRoot(TestExampleBase):
@@ -622,6 +668,13 @@ class TestExampleOfKotlinTraceOffCpu(TestExampleBase):
("line 24", 20, 0),
("line 32", 20, 0)])
self.run_cmd([inferno_script, "-sc"])
+ self.check_inferno_report_html(
+ [('void com.example.simpleperf.simpleperfexampleofkotlin.SleepActivity$createRunSleepThread$1.run()',
+ 80),
+ ('long com.example.simpleperf.simpleperfexampleofkotlin.SleepActivity$createRunSleepThread$1.RunFunction()',
+ 20),
+ ('long com.example.simpleperf.simpleperfexampleofkotlin.SleepActivity$createRunSleepThread$1.SleepFunction(long)',
+ 20)])
class TestProfilingNativeProgram(TestExampleBase):