Skip to content

Commit

Permalink
Merge pull request #186 from Perfexionists/extending-diff-views
Browse files Browse the repository at this point in the history
Extend diff views with flamegraph diff
  • Loading branch information
tfiedor authored Mar 21, 2024
2 parents f4dd6bb + 41f5d5e commit 6dbedb9
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 25 deletions.
11 changes: 4 additions & 7 deletions perun/collect/kperf/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
from __future__ import annotations

# Standard Imports
from pathlib import Path
from typing import Any
import os
import subprocess
import time

Expand All @@ -16,6 +14,7 @@
from perun.collect.kperf import parser
from perun.logic import runner
from perun.utils import log
from perun.utils.common import script_kit
from perun.utils.structs import Executable, CollectStatus
from perun.utils.external import commands

Expand All @@ -32,8 +31,7 @@ def before(**_: Any) -> tuple[CollectStatus, str, dict[str, Any]]:
log.minor_fail(f"{log.cmd_style('perf')}", "not-executable")

# Check that helper script can be run
script_dir = Path(Path(__file__).resolve().parent, "scripts")
parse_script = os.path.join(script_dir, "stackcollapse-perf.pl")
parse_script = script_kit.get_script("stackcollapse-perf.pl")
if commands.is_executable(f'echo "" | {parse_script}'):
log.minor_success(f"{log.cmd_style(parse_script)}", "executable")
else:
Expand All @@ -42,7 +40,7 @@ def before(**_: Any) -> tuple[CollectStatus, str, dict[str, Any]]:

if not all_found:
log.minor_fail("Checking dependencies")
return CollectStatus.ERROR, "Some depedencies cannot be run", {}
return CollectStatus.ERROR, "Some dependencies cannot be run", {}
else:
log.minor_success("Checking dependencies")

Expand All @@ -56,8 +54,7 @@ def run_perf(executable: Executable, run_with_sudo: bool = False) -> str:
:param run_with_sudo: if the command should be run with sudo
:return: parsed output of perf
"""
script_dir = Path(Path(__file__).resolve().parent, "scripts")
parse_script = os.path.join(script_dir, "stackcollapse-perf.pl")
parse_script = script_kit.get_script("stackcollapse-perf.pl")

if run_with_sudo:
perf_record_command = f"sudo perf record -q -g -o collected.data {executable}"
Expand Down
115 changes: 115 additions & 0 deletions perun/scripts/difffolded.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/perl -w
#
# difffolded.pl diff two folded stack files. Use this for generating
# flame graph differentials.
#
# USAGE: ./difffolded.pl [-hns] folded1 folded2 | ./flamegraph.pl > diff2.svg
#
# Options are described in the usage message (-h).
#
# The flamegraph will be colored based on higher samples (red) and smaller
# samples (blue). The frame widths will be based on the 2nd folded file.
# This might be confusing if stack frames disappear entirely; it will make
# the most sense to ALSO create a differential based on the 1st file widths,
# while switching the hues; eg:
#
# ./difffolded.pl folded2 folded1 | ./flamegraph.pl --negate > diff1.svg
#
# Here's what they mean when comparing a before and after profile:
#
# diff1.svg: widths show the before profile, colored by what WILL happen
# diff2.svg: widths show the after profile, colored by what DID happen
#
# INPUT: See stackcollapse* programs.
#
# OUTPUT: The full list of stacks, with two columns, one from each file.
# If a stack wasn't present in a file, the column value is zero.
#
# folded_stack_trace count_from_folded1 count_from_folded2
#
# eg:
#
# funca;funcb;funcc 31 33
# ...
#
# COPYRIGHT: Copyright (c) 2014 Brendan Gregg.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# (http://www.gnu.org/copyleft/gpl.html)
#
# 28-Oct-2014 Brendan Gregg Created this.

use strict;
use Getopt::Std;

# defaults
my $normalize = 0; # make sample counts equal
my $striphex = 0; # strip hex numbers

sub usage {
print STDERR <<USAGE_END;
USAGE: $0 [-hns] folded1 folded2 | flamegraph.pl > diff2.svg
-h # help message
-n # normalize sample counts
-s # strip hex numbers (addresses)
See stackcollapse scripts for generating folded files.
Also consider flipping the files and hues to highlight reduced paths:
$0 folded2 folded1 | ./flamegraph.pl --negate > diff1.svg
USAGE_END
exit 2;
}

usage() if @ARGV < 2;
our($opt_h, $opt_n, $opt_s);
getopts('ns') or usage();
usage() if $opt_h;
$normalize = 1 if defined $opt_n;
$striphex = 1 if defined $opt_s;

my ($total1, $total2) = (0, 0);
my %Folded;

my $file1 = $ARGV[0];
my $file2 = $ARGV[1];

open FILE, $file1 or die "ERROR: Can't read $file1\n";
while (<FILE>) {
chomp;
my ($stack, $count) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
$stack =~ s/0x[0-9a-fA-F]+/0x.../g if $striphex;
$Folded{$stack}{1} += $count;
$total1 += $count;
}
close FILE;

open FILE, $file2 or die "ERROR: Can't read $file2\n";
while (<FILE>) {
chomp;
my ($stack, $count) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
$stack =~ s/0x[0-9a-fA-F]+/0x.../g if $striphex;
$Folded{$stack}{2} += $count;
$total2 += $count;
}
close FILE;

foreach my $stack (keys %Folded) {
$Folded{$stack}{1} = 0 unless defined $Folded{$stack}{1};
$Folded{$stack}{2} = 0 unless defined $Folded{$stack}{2};
if ($normalize && $total1 != $total2) {
$Folded{$stack}{1} = int($Folded{$stack}{1} * $total2 / $total1);
}
print "$stack $Folded{$stack}{1} $Folded{$stack}{2}\n";
}
File renamed without changes.
12 changes: 12 additions & 0 deletions perun/scripts/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
perun_scripts_dir = perun_dir / 'scripts'

perun_scripts_files = files(
'difffolded.pl',
'flamegraph.pl',
'stackcollapse-perf.pl',
)

py3.install_sources(
perun_scripts_files,
subdir: perun_scripts_dir
)
File renamed without changes.
50 changes: 37 additions & 13 deletions perun/templates/diff_view_flamegraph.html.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@
html {
font-family: "Courier New", Courier, monospace;
}
html, body {
margin: 0;
padding: 0;
}
.middle {
width: 98%;
float: left;
margin: 0 1%;
justify-content: center;
}
.column {
width: 48%;
margin: 0 1%;
Expand All @@ -23,7 +33,6 @@
}
.column-head {
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
text-align: center;
}
.clear {
Expand All @@ -34,22 +43,31 @@
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 100%;
vertical-align: top;
overflow: hidden;
}
div.column .svg-container {
padding-bottom: 84%;
}
div.middle .svg-container {
padding-bottom: 42%;
}
.svg-content {
display: inline-block;
position: absolute;
top: 0;
left: 0;
}
.help {
border-top: 1px solid #ddd;
margin: 0 auto 2em auto;
text-align: center;
}
.help h2 {
border-bottom: 1px solid #ddd;
}
.help ul {
list-style-type: none;
margin: 0;
Expand Down Expand Up @@ -83,19 +101,25 @@
</div>
</div>

<div class="clear"></div>
<div class="middle">
<h2 class="column-head">Difference of profiles</h2>
<div class='svg-container'>
{{ diff_flamegraph }}
</div>

<div class="help">
<h2>Help</h2>
<ul>
<li>> Click on the square to nested into selected trace.</li>
<li>> The size of the rectangle represents relative consumption with respect to parent.</li>
<li>> The color of the rectangle represents nothing.</li>
<li>> Use <it>reset zoom</it> (top left) to return to original view.</li>
<li>> Use <it>search</it> (top right) to highlight selected functions.</li>
</ul>
<div class="help">
<h2>Help</h2>
<ul>
<li>> Click on the square to nested into selected trace.</li>
<li>> The size of the rectangle represents relative consumption with respect to parent.</li>
<li>> The color of the rectangle represents nothing.</li>
<li>> Use <it>reset zoom</it> (top left) to return to original view.</li>
<li>> Use <it>search</it> (top right) to highlight selected functions.</li>
</ul>
</div>
</div>


<script>
{{ profile_overview.toggle_script('toggleLeftCollapse', 'left-info') }}
{{ profile_overview.toggle_script('toggleLeftTopCollapse', 'left-top') }}
Expand Down
1 change: 1 addition & 0 deletions perun/templates/diff_view_report.html.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
}
.right {
float: right;
background-color: #0e84b5;
}
.column-head {
border-bottom: 1px solid #ddd;
Expand Down
4 changes: 4 additions & 0 deletions perun/templates/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ perun_templates_files = files(
'check.jinja2',
'collect.__init__.jinja2',
'collect.run.jinja2',
'diff_view_flamegraph.html.jinja2',
'diff_view_report.html.jinja2',
'macros_accordion.html.jinja2',
'macros_profile_overview.html.jinja2',
'postprocess.__init__.jinja2',
'postprocess.run.jinja2',
'view.__init__.jinja2',
Expand Down
11 changes: 11 additions & 0 deletions perun/utils/common/script_kit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

# Standard Imports
from pathlib import Path
from typing import Any
import os

Expand All @@ -17,6 +18,16 @@
from perun.utils.external import commands


def get_script(script_name: str) -> str:
"""Retrieves path to the script
:param script_name: name of the retrieved script
:return path to the script
"""
script_dir = Path(Path(__file__).resolve().parent, "..", "..", "scripts")
return os.path.join(script_dir, script_name)


def create_unit_from_template(template_type: str, no_edit: bool, **kwargs: Any) -> None:
"""Function for creating a module in the perun developer directory from template
Expand Down
40 changes: 37 additions & 3 deletions perun/view/flamegraph/flamegraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,47 @@

# Perun Imports
from perun.profile import convert
from perun.utils.common import script_kit
from perun.utils.external import commands

if TYPE_CHECKING:
from perun.profile.factory import Profile

_SCRIPT_FILENAME = "flamegraph.pl"

def draw_flame_graph_difference(
lhs_profile: Profile, rhs_profile: Profile, height: int, width: int = 1200, title: str = ""
) -> str:
"""Draws difference of two flame graphs from two profiles
:param lhs_profile: baseline profile
:param rhs_profile: target_profile
"""
# First we create two flamegraph formats
lhs_flame = convert.to_flame_graph_format(lhs_profile)
with open("lhs.flame", "w") as lhs_handle:
lhs_handle.write("".join(lhs_flame))
rhs_flame = convert.to_flame_graph_format(rhs_profile)
with open("rhs.flame", "w") as rhs_handle:
rhs_handle.write("".join(rhs_flame))

header = lhs_profile["header"]
profile_type = header["type"]
cmd, workload = (header["cmd"], header["workload"])
title = title if title != "" else f"{profile_type} consumption of {cmd} {workload}"
units = header["units"][profile_type]

diff_script = script_kit.get_script("difffolded.pl")
flame_script = script_kit.get_script("flamegraph.pl")
difference_script = (
f"{diff_script} -n lhs.flame rhs.flame "
f"| {flame_script} --title '{title}' --countname {units} --reverse "
f"--width {width * 2} --height {height}"
)
out, _ = commands.run_safely_external_command(difference_script)
os.remove("lhs.flame")
os.remove("rhs.flame")

return out.decode("utf-8")


def draw_flame_graph(profile: Profile, height: int, width: int = 1200, title: str = "") -> str:
Expand All @@ -38,13 +73,12 @@ def draw_flame_graph(profile: Profile, height: int, width: int = 1200, title: st
title = title if title != "" else f"{profile_type} consumption of {cmd} {workload}"
units = header["units"][profile_type]

pwd = os.path.dirname(os.path.abspath(__file__))
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp.write("".join(flame).encode("utf-8"))
tmp.close()
cmd = " ".join(
[
os.path.join(pwd, _SCRIPT_FILENAME),
script_kit.get_script("flamegraph.pl"),
tmp.name,
"--title",
f'"{title}"',
Expand Down
1 change: 0 additions & 1 deletion perun/view/flamegraph/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ perun_view_flamegraph_dir = perun_view_dir / 'flamegraph'

perun_view_flamegraph_files = files(
'__init__.py',
'flamegraph.pl',
'flamegraph.py',
'run.py',
)
Expand Down
Loading

0 comments on commit 6dbedb9

Please sign in to comment.