Unverified Commit a6689a2d authored by Ge Jun's avatar Ge Jun Committed by GitHub

Merge pull request #864 from skilxnTL/master

Add flamegraph view for profiling builtin service
parents 907ac3d8 613bcc7b
// Copyright 2016 Netflix, Inc.
// Copyright 2011 Joyent, Inc. All rights reserved.
// Copyright 2011 Brendan Gregg. All rights reserved.
//
// CDDL HEADER START
//
// The contents of this file are subject to the terms of the
// Common Development and Distribution License (the "License").
// You may not use this file except in compliance with the License.
//
// You can obtain a copy of the license at docs/cddl1.txt or
// http://opensource.org/licenses/CDDL-1.0.
// See the License for the specific language governing permissions
// and limitations under the License.
//
// When distributing Covered Code, include this CDDL HEADER in each
// file and include the License file at docs/cddl1.txt.
// If applicable, add the following below this CDDL HEADER, with the
// fields enclosed by brackets "[]" replaced with your own identifying
// information: Portions Copyright [yyyy] [name of copyright owner]
//
// CDDL HEADER END
//
// 11-Oct-2014 Adrien Mahieux Added zoom.
// 21-Nov-2013 Shawn Sterling Added consistent palette file option
// 17-Mar-2013 Tim Bunce Added options and more tunables.
// 15-Dec-2011 Dave Pacheco Support for frames with whitespace.
// 10-Sep-2011 Brendan Gregg Created this.
#include "brpc/builtin/flamegraph_perl.h"
namespace brpc {
const char* flamegraph_perl() {
return "#!/usr/bin/perl -w\n"
"#\n"
"# flamegraph.pl flame stack grapher.\n"
"#\n"
"# This takes stack samples and renders a call graph, allowing hot functions\n"
"# and codepaths to be quickly identified. Stack samples can be generated using\n"
"# tools such as DTrace, perf, SystemTap, and Instruments.\n"
"#\n"
"# USAGE: ./flamegraph.pl [options] input.txt > graph.svg\n"
"#\n"
"# grep funcA input.txt | ./flamegraph.pl [options] > graph.svg\n"
"#\n"
"# Then open the resulting .svg in a web browser, for interactivity: mouse-over\n"
"# frames for info, click to zoom, and ctrl-F to search.\n"
"#\n"
"# Options are listed in the usage message (--help).\n"
"#\n"
"# The input is stack frames and sample counts formatted as single lines. Each\n"
"# frame in the stack is semicolon separated, with a space and count at the end\n"
"# of the line. These can be generated for Linux perf script output using\n"
"# stackcollapse-perf.pl, for DTrace using stackcollapse.pl, and for other tools\n"
"# using the other stackcollapse programs. Example input:\n"
"#\n"
"# swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 1\n"
"#\n"
"# An optional extra column of counts can be provided to generate a differential\n"
"# flame graph of the counts, colored red for more, and blue for less. This\n"
"# can be useful when using flame graphs for non-regression testing.\n"
"# See the header comment in the difffolded.pl program for instructions.\n"
"#\n"
"# The input functions can optionally have annotations at the end of each\n"
"# function name, following a precedent by some tools (Linux perf's _[k]):\n"
"# _[k] for kernel\n"
"# _[i] for inlined\n"
"# _[j] for jit\n"
"# _[w] for waker\n"
"# Some of the stackcollapse programs support adding these annotations, eg,\n"
"# stackcollapse-perf.pl --kernel --jit. They are used merely for colors by\n"
"# some palettes, eg, flamegraph.pl --color=java.\n"
"#\n"
"# The output flame graph shows relative presence of functions in stack samples.\n"
"# The ordering on the x-axis has no meaning; since the data is samples, time\n"
"# order of events is not known. The order used sorts function names\n"
"# alphabetically.\n"
"#\n"
"# While intended to process stack samples, this can also process stack traces.\n"
"# For example, tracing stacks for memory allocation, or resource usage. You\n"
"# can use --title to set the title to reflect the content, and --countname\n"
"# to change \"samples\" to \"bytes\" etc.\n"
"#\n"
"# There are a few different palettes, selectable using --color. By default,\n"
"# the colors are selected at random (except for differentials). Functions\n"
"# called \"-\" will be printed gray, which can be used for stack separators (eg,\n"
"# between user and kernel stacks).\n"
"#\n"
"# HISTORY\n"
"#\n"
"# This was inspired by Neelakanth Nadgir's excellent function_call_graph.rb\n"
"# program, which visualized function entry and return trace events. As Neel\n"
"# wrote: \"The output displayed is inspired by Roch's CallStackAnalyzer which\n"
"# was in turn inspired by the work on vftrace by Jan Boerhout\". See:\n"
"# https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and\n"
"#\n"
"# Copyright 2016 Netflix, Inc.\n"
"# Copyright 2011 Joyent, Inc. All rights reserved.\n"
"# Copyright 2011 Brendan Gregg. All rights reserved.\n"
"#\n"
"# CDDL HEADER START\n"
"#\n"
"# The contents of this file are subject to the terms of the\n"
"# Common Development and Distribution License (the \"License\").\n"
"# You may not use this file except in compliance with the License.\n"
"#\n"
"# You can obtain a copy of the license at docs/cddl1.txt or\n"
"# http://opensource.org/licenses/CDDL-1.0.\n"
"# See the License for the specific language governing permissions\n"
"# and limitations under the License.\n"
"#\n"
"# When distributing Covered Code, include this CDDL HEADER in each\n"
"# file and include the License file at docs/cddl1.txt.\n"
"# If applicable, add the following below this CDDL HEADER, with the\n"
"# fields enclosed by brackets \"[]\" replaced with your own identifying\n"
"# information: Portions Copyright [yyyy] [name of copyright owner]\n"
"#\n"
"# CDDL HEADER END\n"
"#\n"
"# 11-Oct-2014 Adrien Mahieux Added zoom.\n"
"# 21-Nov-2013 Shawn Sterling Added consistent palette file option\n"
"# 17-Mar-2013 Tim Bunce Added options and more tunables.\n"
"# 15-Dec-2011 Dave Pacheco Support for frames with whitespace.\n"
"# 10-Sep-2011 Brendan Gregg Created this.\n"
"\n"
"use strict;\n"
"\n"
"use Getopt::Long;\n"
"\n"
"use open qw(:std :utf8);\n"
"\n"
"# tunables\n"
"my $encoding;\n"
"my $fonttype = \"Verdana\";\n"
"my $imagewidth = 1200; # max width, pixels\n"
"my $frameheight = 16; # max height is dynamic\n"
"my $fontsize = 12; # base text size\n"
"my $fontwidth = 0.59; # avg width relative to fontsize\n"
"my $minwidth = 0.1; # min function width, pixels\n"
"my $nametype = \"Function:\"; # what are the names in the data?\n"
"my $countname = \"samples\"; # what are the counts in the data?\n"
"my $colors = \"hot\"; # color theme\n"
"my $bgcolor1 = \"#eeeeee\"; # background color gradient start\n"
"my $bgcolor2 = \"#eeeeb0\"; # background color gradient stop\n"
"my $nameattrfile; # file holding function attributes\n"
"my $timemax; # (override the) sum of the counts\n"
"my $factor = 1; # factor to scale counts by\n"
"my $hash = 0; # color by function name\n"
"my $palette = 0; # if we use consistent palettes (default off)\n"
"my %palette_map; # palette map hash\n"
"my $pal_file = \"palette.map\"; # palette map file name\n"
"my $stackreverse = 0; # reverse stack order, switching merge end\n"
"my $inverted = 0; # icicle graph\n"
"my $negate = 0; # switch differential hues\n"
"my $titletext = \"\"; # centered heading\n"
"my $titledefault = \"Flame Graph\"; # overwritten by --title\n"
"my $titleinverted = \"Icicle Graph\"; # \" \"\n"
"my $searchcolor = \"rgb(230,0,230)\"; # color for search highlighting\n"
"my $notestext = \"\"; # embedded notes in SVG\n"
"my $subtitletext = \"\"; # second level title (optional)\n"
"my $help = 0;\n"
"\n"
"sub usage {\n"
" die <<USAGE_END;\n"
"USAGE: $0 [options] infile > outfile.svg\\n\n"
" --title TEXT # change title text\n"
" --subtitle TEXT # second level title (optional)\n"
" --width NUM # width of image (default 1200)\n"
" --height NUM # height of each frame (default 16)\n"
" --minwidth NUM # omit smaller functions (default 0.1 pixels)\n"
" --fonttype FONT # font type (default \"Verdana\")\n"
" --fontsize NUM # font size (default 12)\n"
" --countname TEXT # count type label (default \"samples\")\n"
" --nametype TEXT # name type label (default \"Function:\")\n"
" --colors PALETTE # set color palette. choices are: hot (default), mem,\n"
" # io, wakeup, chain, java, js, perl, red, green, blue,\n"
" # aqua, yellow, purple, orange\n"
" --hash # colors are keyed by function name hash\n"
" --cp # use consistent palette (palette.map)\n"
" --reverse # generate stack-reversed flame graph\n"
" --inverted # icicle graph\n"
" --negate # switch differential hues (blue<->red)\n"
" --notes TEXT # add notes comment in SVG (for debugging)\n"
" --help # this message\n"
"\n"
" eg,\n"
" $0 --title=\"Flame Graph: malloc()\" trace.txt > graph.svg\n"
"USAGE_END\n"
"}\n"
"\n"
"GetOptions(\n"
" 'fonttype=s' => \\$fonttype,\n"
" 'width=i' => \\$imagewidth,\n"
" 'height=i' => \\$frameheight,\n"
" 'encoding=s' => \\$encoding,\n"
" 'fontsize=f' => \\$fontsize,\n"
" 'fontwidth=f' => \\$fontwidth,\n"
" 'minwidth=f' => \\$minwidth,\n"
" 'title=s' => \\$titletext,\n"
" 'subtitle=s' => \\$subtitletext,\n"
" 'nametype=s' => \\$nametype,\n"
" 'countname=s' => \\$countname,\n"
" 'nameattr=s' => \\$nameattrfile,\n"
" 'total=s' => \\$timemax,\n"
" 'factor=f' => \\$factor,\n"
" 'colors=s' => \\$colors,\n"
" 'hash' => \\$hash,\n"
" 'cp' => \\$palette,\n"
" 'reverse' => \\$stackreverse,\n"
" 'inverted' => \\$inverted,\n"
" 'negate' => \\$negate,\n"
" 'notes=s' => \\$notestext,\n"
" 'help' => \\$help,\n"
") or usage();\n"
"$help && usage();\n"
"\n"
"# internals\n"
"my $ypad1 = $fontsize * 3; # pad top, include title\n"
"my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels\n"
"my $ypad3 = $fontsize * 2; # pad top, include subtitle (optional)\n"
"my $xpad = 10; # pad lefm and right\n"
"my $framepad = 1; # vertical padding for frames\n"
"my $depthmax = 0;\n"
"my %Events;\n"
"my %nameattr;\n"
"\n"
"if ($titletext eq \"\") {\n"
" unless ($inverted) {\n"
" $titletext = $titledefault;\n"
" } else {\n"
" $titletext = $titleinverted;\n"
" }\n"
"}\n"
"\n"
"if ($nameattrfile) {\n"
" # The name-attribute file format is a function name followed by a tab then\n"
" # a sequence of tab separated name=value pairs.\n"
" open my $attrfh, $nameattrfile or die \"Can't read $nameattrfile: $!\\n\";\n"
" while (<$attrfh>) {\n"
" chomp;\n"
" my ($funcname, $attrstr) = split /\\t/, $_, 2;\n"
" die \"Invalid format in $nameattrfile\" unless defined $attrstr;\n"
" $nameattr{$funcname} = { map { split /=/, $_, 2 } split /\\t/, $attrstr "
"};\n"
" }\n"
"}\n"
"\n"
"if ($notestext =~ /[<>]/) {\n"
" die \"Notes string can't contain < or >\"\n"
"}\n"
"\n"
"# background colors:\n"
"# - yellow gradient: default (hot, java, js, perl)\n"
"# - blue gradient: mem, chain\n"
"# - gray gradient: io, wakeup, flat colors (red, green, blue, ...)\n"
"if ($colors eq \"mem\" or $colors eq \"chain\") {\n"
" $bgcolor1 = \"#eeeeee\"; $bgcolor2 = \"#e0e0ff\";\n"
"}\n"
"if ($colors =~ /^(io|wakeup|red|green|blue|aqua|yellow|purple|orange)$/) {\n"
" $bgcolor1 = \"#f8f8f8\"; $bgcolor2 = \"#e8e8e8\";\n"
"}\n"
"\n"
"# SVG functions\n"
"{ package SVG;\n"
" sub new {\n"
" my $class = shift;\n"
" my $self = {};\n"
" bless ($self, $class);\n"
" return $self;\n"
" }\n"
"\n"
" sub header {\n"
" my ($self, $w, $h) = @_;\n"
" my $enc_attr = '';\n"
" if (defined $encoding) {\n"
" $enc_attr = qq{ encoding=\"$encoding\"};\n"
" }\n"
" $self->{svg} .= <<SVG;\n"
"<?xml version=\"1.0\"$enc_attr standalone=\"no\"?>\n"
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" "
"\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"
"<svg version=\"1.1\" width=\"$w\" height=\"$h\" onload=\"init(evt)\" viewBox=\"0 0 $w "
"$h\" xmlns=\"http://www.w3.org/2000/svg\" "
"xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n"
"<!-- Flame graph stack visualization. See https://github.com/brendangregg/FlameGraph "
"for latest version, and http://www.brendangregg.com/flamegraphs.html for examples. "
"-->\n"
"<!-- NOTES: $notestext -->\n"
"SVG\n"
" }\n"
"\n"
" sub include {\n"
" my ($self, $content) = @_;\n"
" $self->{svg} .= $content;\n"
" }\n"
"\n"
" sub colorAllocate {\n"
" my ($self, $r, $g, $b) = @_;\n"
" return \"rgb($r,$g,$b)\";\n"
" }\n"
"\n"
" sub group_start {\n"
" my ($self, $attr) = @_;\n"
"\n"
" my @g_attr = map {\n"
" exists $attr->{$_} ? sprintf(qq/$_=\"%s\"/, $attr->{$_}) : ()\n"
" } qw(class style onmouseover onmouseout onclick);\n"
" push @g_attr, $attr->{g_extra} if $attr->{g_extra};\n"
" $self->{svg} .= sprintf qq/<g %s>\\n/, join(' ', @g_attr);\n"
"\n"
" $self->{svg} .= sprintf qq/<title>%s<\\/title>/, $attr->{title}\n"
" if $attr->{title}; # should be first element within g container\n"
"\n"
" if ($attr->{href}) {\n"
" my @a_attr;\n"
" push @a_attr, sprintf qq/xlink:href=\"%s\"/, $attr->{href} if "
"$attr->{href};\n"
" # default target=_top else links will open within SVG <object>\n"
" push @a_attr, sprintf qq/target=\"%s\"/, $attr->{target} || "
"\"_top\";\n"
" push @a_attr, $attr->{a_extra} if "
"$attr->{a_extra};\n"
" $self->{svg} .= sprintf qq/<a %s>/, join(' ', @a_attr);\n"
" }\n"
" }\n"
"\n"
" sub group_end {\n"
" my ($self, $attr) = @_;\n"
" $self->{svg} .= qq/<\\/a>\\n/ if $attr->{href};\n"
" $self->{svg} .= qq/<\\/g>\\n/;\n"
" }\n"
"\n"
" sub filledRectangle {\n"
" my ($self, $x1, $y1, $x2, $y2, $fill, $extra) = @_;\n"
" $x1 = sprintf \"%0.1f\", $x1;\n"
" $x2 = sprintf \"%0.1f\", $x2;\n"
" my $w = sprintf \"%0.1f\", $x2 - $x1;\n"
" my $h = sprintf \"%0.1f\", $y2 - $y1;\n"
" $extra = defined $extra ? $extra : \"\";\n"
" $self->{svg} .= qq/<rect x=\"$x1\" y=\"$y1\" width=\"$w\" height=\"$h\" "
"fill=\"$fill\" $extra \\/>\\n/;\n"
" }\n"
"\n"
" sub stringTTF {\n"
" my ($self, $color, $font, $size, $angle, $x, $y, $str, $loc, $extra) = "
"@_;\n"
" $x = sprintf \"%0.2f\", $x;\n"
" $loc = defined $loc ? $loc : \"left\";\n"
" $extra = defined $extra ? $extra : \"\";\n"
" $self->{svg} .= qq/<text text-anchor=\"$loc\" x=\"$x\" y=\"$y\" "
"font-size=\"$size\" font-family=\"$font\" fill=\"$color\" $extra >$str<\\/text>\\n/;\n"
" }\n"
"\n"
" sub svg {\n"
" my $self = shift;\n"
" return \"$self->{svg}</svg>\\n\";\n"
" }\n"
" 1;\n"
"}\n"
"\n"
"sub namehash {\n"
" # Generate a vector hash for the name string, weighting early over\n"
" # later characters. We want to pick the same colors for function\n"
" # names across different flame graphs.\n"
" my $name = shift;\n"
" my $vector = 0;\n"
" my $weight = 1;\n"
" my $max = 1;\n"
" my $mod = 10;\n"
" # if module name present, trunc to 1st char\n"
" $name =~ s/.(.*?)`//;\n"
" foreach my $c (split //, $name) {\n"
" my $i = (ord $c) % $mod;\n"
" $vector += ($i / ($mod++ - 1)) * $weight;\n"
" $max += 1 * $weight;\n"
" $weight *= 0.70;\n"
" last if $mod > 12;\n"
" }\n"
" return (1 - $vector / $max)\n"
"}\n"
"\n"
"sub color {\n"
" my ($type, $hash, $name) = @_;\n"
" my ($v1, $v2, $v3);\n"
"\n"
" if ($hash) {\n"
" $v1 = namehash($name);\n"
" $v2 = $v3 = namehash(scalar reverse $name);\n"
" } else {\n"
" $v1 = rand(1);\n"
" $v2 = rand(1);\n"
" $v3 = rand(1);\n"
" }\n"
"\n"
" # theme palettes\n"
" if (defined $type and $type eq \"hot\") {\n"
" my $r = 205 + int(50 * $v3);\n"
" my $g = 0 + int(230 * $v1);\n"
" my $b = 0 + int(55 * $v2);\n"
" return \"rgb($r,$g,$b)\";\n"
" }\n"
" if (defined $type and $type eq \"mem\") {\n"
" my $r = 0;\n"
" my $g = 190 + int(50 * $v2);\n"
" my $b = 0 + int(210 * $v1);\n"
" return \"rgb($r,$g,$b)\";\n"
" }\n"
" if (defined $type and $type eq \"io\") {\n"
" my $r = 80 + int(60 * $v1);\n"
" my $g = $r;\n"
" my $b = 190 + int(55 * $v2);\n"
" return \"rgb($r,$g,$b)\";\n"
" }\n"
"\n"
" # multi palettes\n"
" if (defined $type and $type eq \"java\") {\n"
" # Handle both annotations (_[j], _[i], ...; which are\n"
" # accurate), as well as input that lacks any annotations, as\n"
" # best as possible. Without annotations, we get a little hacky\n"
" # and match on java|org|com, etc.\n"
" if ($name =~ m:_\\[j\\]$:) { # jit annotation\n"
" $type = \"green\";\n"
" } elsif ($name =~ m:_\\[i\\]$:) { # inline annotation\n"
" $type = \"aqua\";\n"
" } elsif ($name =~ m:^L?(java|org|com|io|sun)/:) { # Java\n"
" $type = \"green\";\n"
" } elsif ($name =~ /::/) { # C++\n"
" $type = \"yellow\";\n"
" } elsif ($name =~ m:_\\[k\\]$:) { # kernel annotation\n"
" $type = \"orange\";\n"
" } else { # system\n"
" $type = \"red\";\n"
" }\n"
" # fall-through to color palettes\n"
" }\n"
" if (defined $type and $type eq \"perl\") {\n"
" if ($name =~ /::/) { # C++\n"
" $type = \"yellow\";\n"
" } elsif ($name =~ m:Perl: or $name =~ m:\\.pl:) { # Perl\n"
" $type = \"green\";\n"
" } elsif ($name =~ m:_\\[k\\]$:) { # kernel\n"
" $type = \"orange\";\n"
" } else { # system\n"
" $type = \"red\";\n"
" }\n"
" # fall-through to color palettes\n"
" }\n"
" if (defined $type and $type eq \"js\") {\n"
" # Handle both annotations (_[j], _[i], ...; which are\n"
" # accurate), as well as input that lacks any annotations, as\n"
" # best as possible. Without annotations, we get a little hacky,\n"
" # and match on a \"/\" with a \".js\", etc.\n"
" if ($name =~ m:_\\[j\\]$:) { # jit annotation\n"
" if ($name =~ m:/:) {\n"
" $type = \"green\"; # source\n"
" } else {\n"
" $type = \"aqua\"; # builtin\n"
" }\n"
" } elsif ($name =~ /::/) { # C++\n"
" $type = \"yellow\";\n"
" } elsif ($name =~ m:/.*\\.js:) { # JavaScript (match \"/\" in "
"path)\n"
" $type = \"green\";\n"
" } elsif ($name =~ m/:/) { # JavaScript (match \":\" in builtin)\n"
" $type = \"aqua\";\n"
" } elsif ($name =~ m/^ $/) { # Missing symbol\n"
" $type = \"green\";\n"
" } elsif ($name =~ m:_\\[k\\]:) { # kernel\n"
" $type = \"orange\";\n"
" } else { # system\n"
" $type = \"red\";\n"
" }\n"
" # fall-through to color palettes\n"
" }\n"
" if (defined $type and $type eq \"wakeup\") {\n"
" $type = \"aqua\";\n"
" # fall-through to color palettes\n"
" }\n"
" if (defined $type and $type eq \"chain\") {\n"
" if ($name =~ m:_\\[w\\]:) { # waker\n"
" $type = \"aqua\"\n"
" } else { # off-CPU\n"
" $type = \"blue\";\n"
" }\n"
" # fall-through to color palettes\n"
" }\n"
"\n"
" # color palettes\n"
" if (defined $type and $type eq \"red\") {\n"
" my $r = 200 + int(55 * $v1);\n"
" my $x = 50 + int(80 * $v1);\n"
" return \"rgb($r,$x,$x)\";\n"
" }\n"
" if (defined $type and $type eq \"green\") {\n"
" my $g = 200 + int(55 * $v1);\n"
" my $x = 50 + int(60 * $v1);\n"
" return \"rgb($x,$g,$x)\";\n"
" }\n"
" if (defined $type and $type eq \"blue\") {\n"
" my $b = 205 + int(50 * $v1);\n"
" my $x = 80 + int(60 * $v1);\n"
" return \"rgb($x,$x,$b)\";\n"
" }\n"
" if (defined $type and $type eq \"yellow\") {\n"
" my $x = 175 + int(55 * $v1);\n"
" my $b = 50 + int(20 * $v1);\n"
" return \"rgb($x,$x,$b)\";\n"
" }\n"
" if (defined $type and $type eq \"purple\") {\n"
" my $x = 190 + int(65 * $v1);\n"
" my $g = 80 + int(60 * $v1);\n"
" return \"rgb($x,$g,$x)\";\n"
" }\n"
" if (defined $type and $type eq \"aqua\") {\n"
" my $r = 50 + int(60 * $v1);\n"
" my $g = 165 + int(55 * $v1);\n"
" my $b = 165 + int(55 * $v1);\n"
" return \"rgb($r,$g,$b)\";\n"
" }\n"
" if (defined $type and $type eq \"orange\") {\n"
" my $r = 190 + int(65 * $v1);\n"
" my $g = 90 + int(65 * $v1);\n"
" return \"rgb($r,$g,0)\";\n"
" }\n"
"\n"
" return \"rgb(0,0,0)\";\n"
"}\n"
"\n"
"sub color_scale {\n"
" my ($value, $max) = @_;\n"
" my ($r, $g, $b) = (255, 255, 255);\n"
" $value = -$value if $negate;\n"
" if ($value > 0) {\n"
" $g = $b = int(210 * ($max - $value) / $max);\n"
" } elsif ($value < 0) {\n"
" $r = $g = int(210 * ($max + $value) / $max);\n"
" }\n"
" return \"rgb($r,$g,$b)\";\n"
"}\n"
"\n"
"sub color_map {\n"
" my ($colors, $func) = @_;\n"
" if (exists $palette_map{$func}) {\n"
" return $palette_map{$func};\n"
" } else {\n"
" $palette_map{$func} = color($colors, $hash, $func);\n"
" return $palette_map{$func};\n"
" }\n"
"}\n"
"\n"
"sub write_palette {\n"
" open(FILE, \">$pal_file\");\n"
" foreach my $key (sort keys %palette_map) {\n"
" print FILE $key.\"->\".$palette_map{$key}.\"\\n\";\n"
" }\n"
" close(FILE);\n"
"}\n"
"\n"
"sub read_palette {\n"
" if (-e $pal_file) {\n"
" open(FILE, $pal_file) or die \"can't open file $pal_file: $!\";\n"
" while ( my $line = <FILE>) {\n"
" chomp($line);\n"
" (my $key, my $value) = split(\"->\",$line);\n"
" $palette_map{$key}=$value;\n"
" }\n"
" close(FILE)\n"
" }\n"
"}\n"
"\n"
"my %Node; # Hash of merged frame data\n"
"my %Tmp;\n"
"\n"
"# flow() merges two stacks, storing the merged frames and value data in %Node.\n"
"sub flow {\n"
" my ($last, $this, $v, $d) = @_;\n"
"\n"
" my $len_a = @$last - 1;\n"
" my $len_b = @$this - 1;\n"
"\n"
" my $i = 0;\n"
" my $len_same;\n"
" for (; $i <= $len_a; $i++) {\n"
" last if $i > $len_b;\n"
" last if $last->[$i] ne $this->[$i];\n"
" }\n"
" $len_same = $i;\n"
"\n"
" for ($i = $len_a; $i >= $len_same; $i--) {\n"
" my $k = \"$last->[$i];$i\";\n"
" # a unique ID is constructed from \"func;depth;etime\";\n"
" # func-depth isn't unique, it may be repeated later.\n"
" $Node{\"$k;$v\"}->{stime} = delete $Tmp{$k}->{stime};\n"
" if (defined $Tmp{$k}->{delta}) {\n"
" $Node{\"$k;$v\"}->{delta} = delete $Tmp{$k}->{delta};\n"
" }\n"
" delete $Tmp{$k};\n"
" }\n"
"\n"
" for ($i = $len_same; $i <= $len_b; $i++) {\n"
" my $k = \"$this->[$i];$i\";\n"
" $Tmp{$k}->{stime} = $v;\n"
" if (defined $d) {\n"
" $Tmp{$k}->{delta} += $i == $len_b ? $d : 0;\n"
" }\n"
" }\n"
"\n"
" return $this;\n"
"}\n"
"\n"
"# parse input\n"
"my @Data;\n"
"my $last = [];\n"
"my $time = 0;\n"
"my $delta = undef;\n"
"my $ignored = 0;\n"
"my $line;\n"
"my $maxdelta = 1;\n"
"\n"
"# reverse if needed\n"
"foreach (<>) {\n"
" chomp;\n"
" $line = $_;\n"
" if ($stackreverse) {\n"
" # there may be an extra samples column for differentials\n"
" # XXX todo: redo these REs as one. It's repeated below.\n"
" my($stack, $samples) = (/^(.*)\\s+?(\\d+(?:\\.\\d*)?)$/);\n"
" my $samples2 = undef;\n"
" if ($stack =~ /^(.*)\\s+?(\\d+(?:\\.\\d*)?)$/) {\n"
" $samples2 = $samples;\n"
" ($stack, $samples) = $stack =~ (/^(.*)\\s+?(\\d+(?:\\.\\d*)?)$/);\n"
" unshift @Data, join(\";\", reverse split(\";\", $stack)) . \" "
"$samples $samples2\";\n"
" } else {\n"
" unshift @Data, join(\";\", reverse split(\";\", $stack)) . \" "
"$samples\";\n"
" }\n"
" } else {\n"
" unshift @Data, $line;\n"
" }\n"
"}\n"
"\n"
"# process and merge frames\n"
"foreach (sort @Data) {\n"
" chomp;\n"
" # process: folded_stack count\n"
" # eg: func_a;func_b;func_c 31\n"
" my ($stack, $samples) = (/^(.*)\\s+?(\\d+(?:\\.\\d*)?)$/);\n"
" unless (defined $samples and defined $stack) {\n"
" ++$ignored;\n"
" next;\n"
" }\n"
"\n"
" # there may be an extra samples column for differentials:\n"
" my $samples2 = undef;\n"
" if ($stack =~ /^(.*)\\s+?(\\d+(?:\\.\\d*)?)$/) {\n"
" $samples2 = $samples;\n"
" ($stack, $samples) = $stack =~ (/^(.*)\\s+?(\\d+(?:\\.\\d*)?)$/);\n"
" }\n"
" $delta = undef;\n"
" if (defined $samples2) {\n"
" $delta = $samples2 - $samples;\n"
" $maxdelta = abs($delta) if abs($delta) > $maxdelta;\n"
" }\n"
"\n"
" # for chain graphs, annotate waker frames with \"_[w]\", for later\n"
" # coloring. This is a hack, but has a precedent (\"_[k]\" from perf).\n"
" if ($colors eq \"chain\") {\n"
" my @parts = split \";--;\", $stack;\n"
" my @newparts = ();\n"
" $stack = shift @parts;\n"
" $stack .= \";--;\";\n"
" foreach my $part (@parts) {\n"
" $part =~ s/;/_[w];/g;\n"
" $part .= \"_[w]\";\n"
" push @newparts, $part;\n"
" }\n"
" $stack .= join \";--;\", @parts;\n"
" }\n"
"\n"
" # merge frames and populate %Node:\n"
" $last = flow($last, [ '', split \";\", $stack ], $time, $delta);\n"
"\n"
" if (defined $samples2) {\n"
" $time += $samples2;\n"
" } else {\n"
" $time += $samples;\n"
" }\n"
"}\n"
"flow($last, [], $time, $delta);\n"
"\n"
"warn \"Ignored $ignored lines with invalid format\\n\" if $ignored;\n"
"unless ($time) {\n"
" warn \"ERROR: No stack counts found\\n\";\n"
" my $im = SVG->new();\n"
" # emit an error message SVG, for tools automating flamegraph use\n"
" my $imageheight = $fontsize * 5;\n"
" $im->header($imagewidth, $imageheight);\n"
" $im->stringTTF($im->colorAllocate(0, 0, 0), $fonttype, $fontsize + 2,\n"
" 0.0, int($imagewidth / 2), $fontsize * 2,\n"
" \"ERROR: No valid input provided to flamegraph.pl.\", \"middle\");\n"
" print $im->svg;\n"
" exit 2;\n"
"}\n"
"if ($timemax and $timemax < $time) {\n"
" warn \"Specified --total $timemax is less than actual total $time, so "
"ignored\\n\"\n"
" if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc)\n"
" undef $timemax;\n"
"}\n"
"$timemax ||= $time;\n"
"\n"
"my $widthpertime = ($imagewidth - 2 * $xpad) / $timemax;\n"
"my $minwidth_time = $minwidth / $widthpertime;\n"
"\n"
"# prune blocks that are too narrow and determine max depth\n"
"while (my ($id, $node) = each %Node) {\n"
" my ($func, $depth, $etime) = split \";\", $id;\n"
" my $stime = $node->{stime};\n"
" die \"missing start for $id\" if not defined $stime;\n"
"\n"
" if (($etime-$stime) < $minwidth_time) {\n"
" delete $Node{$id};\n"
" next;\n"
" }\n"
" $depthmax = $depth if $depth > $depthmax;\n"
"}\n"
"\n"
"# draw canvas, and embed interactive JavaScript program\n"
"my $imageheight = (($depthmax + 1) * $frameheight) + $ypad1 + $ypad2;\n"
"$imageheight += $ypad3 if $subtitletext ne \"\";\n"
"my $im = SVG->new();\n"
"$im->header($imagewidth, $imageheight);\n"
"my $inc = <<INC;\n"
"<defs >\n"
" <linearGradient id=\"background\" y1=\"0\" y2=\"1\" x1=\"0\" x2=\"0\" >\n"
" <stop stop-color=\"$bgcolor1\" offset=\"5%\" />\n"
" <stop stop-color=\"$bgcolor2\" offset=\"95%\" />\n"
" </linearGradient>\n"
"</defs>\n"
"<style type=\"text/css\">\n"
" .func_g:hover { stroke:black; stroke-width:0.5; cursor:pointer; }\n"
"</style>\n"
"<script type=\"text/ecmascript\">\n"
"<![CDATA[\n"
" var details, searchbtn, matchedtxt, svg;\n"
" function init(evt) {\n"
" details = document.getElementById(\"details\").firstChild;\n"
" searchbtn = document.getElementById(\"search\");\n"
" matchedtxt = document.getElementById(\"matched\");\n"
" svg = document.getElementsByTagName(\"svg\")[0];\n"
" searching = 0;\n"
" }\n"
"\n"
" // mouse-over for info\n"
" function s(node) { // show\n"
" info = g_to_text(node);\n"
" details.nodeValue = \"$nametype \" + info;\n"
" }\n"
" function c() { // clear\n"
" details.nodeValue = ' ';\n"
" }\n"
"\n"
" // ctrl-F for search\n"
" window.addEventListener(\"keydown\",function (e) {\n"
" if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {\n"
" e.preventDefault();\n"
" search_prompt();\n"
" }\n"
" })\n"
"\n"
" // functions\n"
" function find_child(parent, name, attr) {\n"
" var children = parent.childNodes;\n"
" for (var i=0; i<children.length;i++) {\n"
" if (children[i].tagName == name)\n"
" return (attr != undefined) ? "
"children[i].attributes[attr].value : children[i];\n"
" }\n"
" return;\n"
" }\n"
" function orig_save(e, attr, val) {\n"
" if (e.attributes[\"_orig_\"+attr] != undefined) return;\n"
" if (e.attributes[attr] == undefined) return;\n"
" if (val == undefined) val = e.attributes[attr].value;\n"
" e.setAttribute(\"_orig_\"+attr, val);\n"
" }\n"
" function orig_load(e, attr) {\n"
" if (e.attributes[\"_orig_\"+attr] == undefined) return;\n"
" e.attributes[attr].value = e.attributes[\"_orig_\"+attr].value;\n"
" e.removeAttribute(\"_orig_\"+attr);\n"
" }\n"
" function g_to_text(e) {\n"
" var text = find_child(e, \"title\").firstChild.nodeValue;\n"
" return (text)\n"
" }\n"
" function g_to_func(e) {\n"
" var func = g_to_text(e);\n"
" // if there's any manipulation we want to do to the function\n"
" // name before it's searched, do it here before returning.\n"
" return (func);\n"
" }\n"
" function update_text(e) {\n"
" var r = find_child(e, \"rect\");\n"
" var t = find_child(e, \"text\");\n"
" var w = parseFloat(r.attributes[\"width\"].value) -3;\n"
" var txt = find_child(e, "
"\"title\").textContent.replace(/\\\\([^(]*\\\\)\\$/,\"\");\n"
" t.attributes[\"x\"].value = parseFloat(r.attributes[\"x\"].value) +3;\n"
"\n"
" // Smaller than this size won't fit anything\n"
" if (w < 2*$fontsize*$fontwidth) {\n"
" t.textContent = \"\";\n"
" return;\n"
" }\n"
"\n"
" t.textContent = txt;\n"
" // Fit in full text width\n"
" if (/^ *\\$/.test(txt) || t.getSubStringLength(0, txt.length) < w)\n"
" return;\n"
"\n"
" for (var x=txt.length-2; x>0; x--) {\n"
" if (t.getSubStringLength(0, x+2) <= w) {\n"
" t.textContent = txt.substring(0,x) + \"..\";\n"
" return;\n"
" }\n"
" }\n"
" t.textContent = \"\";\n"
" }\n"
"\n"
" // zoom\n"
" function zoom_reset(e) {\n"
" if (e.attributes != undefined) {\n"
" orig_load(e, \"x\");\n"
" orig_load(e, \"width\");\n"
" }\n"
" if (e.childNodes == undefined) return;\n"
" for(var i=0, c=e.childNodes; i<c.length; i++) {\n"
" zoom_reset(c[i]);\n"
" }\n"
" }\n"
" function zoom_child(e, x, ratio) {\n"
" if (e.attributes != undefined) {\n"
" if (e.attributes[\"x\"] != undefined) {\n"
" orig_save(e, \"x\");\n"
" e.attributes[\"x\"].value = "
"(parseFloat(e.attributes[\"x\"].value) - x - $xpad) * ratio + $xpad;\n"
" if(e.tagName == \"text\") e.attributes[\"x\"].value = "
"find_child(e.parentNode, \"rect\", \"x\") + 3;\n"
" }\n"
" if (e.attributes[\"width\"] != undefined) {\n"
" orig_save(e, \"width\");\n"
" e.attributes[\"width\"].value = "
"parseFloat(e.attributes[\"width\"].value) * ratio;\n"
" }\n"
" }\n"
"\n"
" if (e.childNodes == undefined) return;\n"
" for(var i=0, c=e.childNodes; i<c.length; i++) {\n"
" zoom_child(c[i], x-$xpad, ratio);\n"
" }\n"
" }\n"
" function zoom_parent(e) {\n"
" if (e.attributes) {\n"
" if (e.attributes[\"x\"] != undefined) {\n"
" orig_save(e, \"x\");\n"
" e.attributes[\"x\"].value = $xpad;\n"
" }\n"
" if (e.attributes[\"width\"] != undefined) {\n"
" orig_save(e, \"width\");\n"
" e.attributes[\"width\"].value = "
"parseInt(svg.width.baseVal.value) - ($xpad*2);\n"
" }\n"
" }\n"
" if (e.childNodes == undefined) return;\n"
" for(var i=0, c=e.childNodes; i<c.length; i++) {\n"
" zoom_parent(c[i]);\n"
" }\n"
" }\n"
" function zoom(node) {\n"
" var attr = find_child(node, \"rect\").attributes;\n"
" var width = parseFloat(attr[\"width\"].value);\n"
" var xmin = parseFloat(attr[\"x\"].value);\n"
" var xmax = parseFloat(xmin + width);\n"
" var ymin = parseFloat(attr[\"y\"].value);\n"
" var ratio = (svg.width.baseVal.value - 2*$xpad) / width;\n"
"\n"
" // XXX: Workaround for JavaScript float issues (fix me)\n"
" var fudge = 0.0001;\n"
"\n"
" var unzoombtn = document.getElementById(\"unzoom\");\n"
" unzoombtn.style[\"opacity\"] = \"1.0\";\n"
"\n"
" var el = document.getElementsByTagName(\"g\");\n"
" for(var i=0;i<el.length;i++){\n"
" var e = el[i];\n"
" var a = find_child(e, \"rect\").attributes;\n"
" var ex = parseFloat(a[\"x\"].value);\n"
" var ew = parseFloat(a[\"width\"].value);\n"
" // Is it an ancestor\n"
" if ($inverted == 0) {\n"
" var upstack = parseFloat(a[\"y\"].value) > ymin;\n"
" } else {\n"
" var upstack = parseFloat(a[\"y\"].value) < ymin;\n"
" }\n"
" if (upstack) {\n"
" // Direct ancestor\n"
" if (ex <= xmin && (ex+ew+fudge) >= xmax) {\n"
" e.style[\"opacity\"] = \"0.5\";\n"
" zoom_parent(e);\n"
" e.onclick = function(e){unzoom(); zoom(this);};\n"
" update_text(e);\n"
" }\n"
" // not in current path\n"
" else\n"
" e.style[\"display\"] = \"none\";\n"
" }\n"
" // Children maybe\n"
" else {\n"
" // no common path\n"
" if (ex < xmin || ex + fudge >= xmax) {\n"
" e.style[\"display\"] = \"none\";\n"
" }\n"
" else {\n"
" zoom_child(e, xmin, ratio);\n"
" e.onclick = function(e){zoom(this);};\n"
" update_text(e);\n"
" }\n"
" }\n"
" }\n"
" }\n"
" function unzoom() {\n"
" var unzoombtn = document.getElementById(\"unzoom\");\n"
" unzoombtn.style[\"opacity\"] = \"0.0\";\n"
"\n"
" var el = document.getElementsByTagName(\"g\");\n"
" for(i=0;i<el.length;i++) {\n"
" el[i].style[\"display\"] = \"block\";\n"
" el[i].style[\"opacity\"] = \"1\";\n"
" zoom_reset(el[i]);\n"
" update_text(el[i]);\n"
" }\n"
" }\n"
"\n"
" // search\n"
" function reset_search() {\n"
" var el = document.getElementsByTagName(\"rect\");\n"
" for (var i=0; i < el.length; i++) {\n"
" orig_load(el[i], \"fill\")\n"
" }\n"
" }\n"
" function search_prompt() {\n"
" if (!searching) {\n"
" var term = prompt(\"Enter a search term (regexp \" +\n"
" \"allowed, eg: ^ext4_)\", \"\");\n"
" if (term != null) {\n"
" search(term)\n"
" }\n"
" } else {\n"
" reset_search();\n"
" searching = 0;\n"
" searchbtn.style[\"opacity\"] = \"0.1\";\n"
" searchbtn.firstChild.nodeValue = \"Search\"\n"
" matchedtxt.style[\"opacity\"] = \"0.0\";\n"
" matchedtxt.firstChild.nodeValue = \"\"\n"
" }\n"
" }\n"
" function search(term) {\n"
" var re = new RegExp(term);\n"
" var el = document.getElementsByTagName(\"g\");\n"
" var matches = new Object();\n"
" var maxwidth = 0;\n"
" for (var i = 0; i < el.length; i++) {\n"
" var e = el[i];\n"
" if (e.attributes[\"class\"].value != \"func_g\")\n"
" continue;\n"
" var func = g_to_func(e);\n"
" var rect = find_child(e, \"rect\");\n"
" if (rect == null) {\n"
" // the rect might be wrapped in an anchor\n"
" // if nameattr href is being used\n"
" if (rect = find_child(e, \"a\")) {\n"
" rect = find_child(r, \"rect\");\n"
" }\n"
" }\n"
" if (func == null || rect == null)\n"
" continue;\n"
"\n"
" // Save max width. Only works as we have a root frame\n"
" var w = parseFloat(rect.attributes[\"width\"].value);\n"
" if (w > maxwidth)\n"
" maxwidth = w;\n"
"\n"
" if (func.match(re)) {\n"
" // highlight\n"
" var x = parseFloat(rect.attributes[\"x\"].value);\n"
" orig_save(rect, \"fill\");\n"
" rect.attributes[\"fill\"].value =\n"
" \"$searchcolor\";\n"
"\n"
" // remember matches\n"
" if (matches[x] == undefined) {\n"
" matches[x] = w;\n"
" } else {\n"
" if (w > matches[x]) {\n"
" // overwrite with parent\n"
" matches[x] = w;\n"
" }\n"
" }\n"
" searching = 1;\n"
" }\n"
" }\n"
" if (!searching)\n"
" return;\n"
"\n"
" searchbtn.style[\"opacity\"] = \"1.0\";\n"
" searchbtn.firstChild.nodeValue = \"Reset Search\"\n"
"\n"
" // calculate percent matched, excluding vertical overlap\n"
" var count = 0;\n"
" var lastx = -1;\n"
" var lastw = 0;\n"
" var keys = Array();\n"
" for (k in matches) {\n"
" if (matches.hasOwnProperty(k))\n"
" keys.push(k);\n"
" }\n"
" // sort the matched frames by their x location\n"
" // ascending, then width descending\n"
" keys.sort(function(a, b){\n"
" return a - b;\n"
" });\n"
" // Step through frames saving only the biggest bottom-up frames\n"
" // thanks to the sort order. This relies on the tree property\n"
" // where children are always smaller than their parents.\n"
" var fudge = 0.0001; // JavaScript floating point\n"
" for (var k in keys) {\n"
" var x = parseFloat(keys[k]);\n"
" var w = matches[keys[k]];\n"
" if (x >= lastx + lastw - fudge) {\n"
" count += w;\n"
" lastx = x;\n"
" lastw = w;\n"
" }\n"
" }\n"
" // display matched percent\n"
" matchedtxt.style[\"opacity\"] = \"1.0\";\n"
" pct = 100 * count / maxwidth;\n"
" if (pct == 100)\n"
" pct = \"100\"\n"
" else\n"
" pct = pct.toFixed(1)\n"
" matchedtxt.firstChild.nodeValue = \"Matched: \" + pct + \"%\";\n"
" }\n"
" function searchover(e) {\n"
" searchbtn.style[\"opacity\"] = \"1.0\";\n"
" }\n"
" function searchout(e) {\n"
" if (searching) {\n"
" searchbtn.style[\"opacity\"] = \"1.0\";\n"
" } else {\n"
" searchbtn.style[\"opacity\"] = \"0.1\";\n"
" }\n"
" }\n"
"]]>\n"
"</script>\n"
"INC\n"
"$im->include($inc);\n"
"$im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)');\n"
"my ($white, $black, $vvdgrey, $vdgrey, $dgrey) = (\n"
" $im->colorAllocate(255, 255, 255),\n"
" $im->colorAllocate(0, 0, 0),\n"
" $im->colorAllocate(40, 40, 40),\n"
" $im->colorAllocate(160, 160, 160),\n"
" $im->colorAllocate(200, 200, 200),\n"
" );\n"
"$im->stringTTF($black, $fonttype, $fontsize + 5, 0.0, int($imagewidth / 2), $fontsize "
"* 2, $titletext, \"middle\");\n"
"if ($subtitletext ne \"\") {\n"
" $im->stringTTF($vdgrey, $fonttype, $fontsize, 0.0, int($imagewidth / 2), $fontsize "
"* 4, $subtitletext, \"middle\");\n"
"}\n"
"$im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $imageheight - ($ypad2 / 2), "
"\" \", \"\", 'id=\"details\"');\n"
"$im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $fontsize * 2,\n"
" \"Reset Zoom\", \"\", 'id=\"unzoom\" onclick=\"unzoom()\" "
"style=\"opacity:0.0;cursor:pointer\"');\n"
"$im->stringTTF($black, $fonttype, $fontsize, 0.0, $imagewidth - $xpad - 100,\n"
" $fontsize * 2, \"Search\", \"\", 'id=\"search\" onmouseover=\"searchover()\" "
"onmouseout=\"searchout()\" onclick=\"search_prompt()\" "
"style=\"opacity:0.1;cursor:pointer\"');\n"
"$im->stringTTF($black, $fonttype, $fontsize, 0.0, $imagewidth - $xpad - 100, "
"$imageheight - ($ypad2 / 2), \" \", \"\", 'id=\"matched\"');\n"
"\n"
"if ($palette) {\n"
" read_palette();\n"
"}\n"
"\n"
"# draw frames\n"
"while (my ($id, $node) = each %Node) {\n"
" my ($func, $depth, $etime) = split \";\", $id;\n"
" my $stime = $node->{stime};\n"
" my $delta = $node->{delta};\n"
"\n"
" $etime = $timemax if $func eq \"\" and $depth == 0;\n"
"\n"
" my $x1 = $xpad + $stime * $widthpertime;\n"
" my $x2 = $xpad + $etime * $widthpertime;\n"
" my ($y1, $y2);\n"
" unless ($inverted) {\n"
" $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad;\n"
" $y2 = $imageheight - $ypad2 - $depth * $frameheight;\n"
" } else {\n"
" $y1 = $ypad1 + $depth * $frameheight;\n"
" $y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad;\n"
" }\n"
"\n"
" my $samples = sprintf \"%.0f\", ($etime - $stime) * $factor;\n"
" (my $samples_txt = $samples) # add commas per perlfaq5\n"
" =~ s/(^[-+]?\\d+?(?=(?>(?:\\d{3})+)(?!\\d))|\\G\\d{3}(?=\\d))/$1,/g;\n"
"\n"
" my $info;\n"
" if ($func eq \"\" and $depth == 0) {\n"
" $info = \"all ($samples_txt $countname, 100%)\";\n"
" } else {\n"
" my $pct = sprintf \"%.2f\", ((100 * $samples) / ($timemax * $factor));\n"
" my $escaped_func = $func;\n"
" # clean up SVG breaking characters:\n"
" $escaped_func =~ s/&/&amp;/g;\n"
" $escaped_func =~ s/</&lt;/g;\n"
" $escaped_func =~ s/>/&gt;/g;\n"
" $escaped_func =~ s/\"/&quot;/g;\n"
" $escaped_func =~ s/_\\[[kwij]\\]$//; # strip any annotation\n"
" unless (defined $delta) {\n"
" $info = \"$escaped_func ($samples_txt $countname, $pct%)\";\n"
" } else {\n"
" my $d = $negate ? -$delta : $delta;\n"
" my $deltapct = sprintf \"%.2f\", ((100 * $d) / ($timemax * "
"$factor));\n"
" $deltapct = $d > 0 ? \"+$deltapct\" : $deltapct;\n"
" $info = \"$escaped_func ($samples_txt $countname, $pct%; "
"$deltapct%)\";\n"
" }\n"
" }\n"
"\n"
" my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone\n"
" $nameattr->{class} ||= \"func_g\";\n"
" $nameattr->{onmouseover} ||= \"s(this)\";\n"
" $nameattr->{onmouseout} ||= \"c()\";\n"
" $nameattr->{onclick} ||= \"zoom(this)\";\n"
" $nameattr->{title} ||= $info;\n"
" $im->group_start($nameattr);\n"
"\n"
" my $color;\n"
" if ($func eq \"--\") {\n"
" $color = $vdgrey;\n"
" } elsif ($func eq \"-\") {\n"
" $color = $dgrey;\n"
" } elsif (defined $delta) {\n"
" $color = color_scale($delta, $maxdelta);\n"
" } elsif ($palette) {\n"
" $color = color_map($colors, $func);\n"
" } else {\n"
" $color = color($colors, $hash, $func);\n"
" }\n"
" $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx=\"2\" ry=\"2\"');\n"
"\n"
" my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth));\n"
" my $text = \"\";\n"
" if ($chars >= 3) { # room for one char plus two dots\n"
" $func =~ s/_\\[[kwij]\\]$//; # strip any annotation\n"
" $text = substr $func, 0, $chars;\n"
" substr($text, -2, 2) = \"..\" if $chars < length $func;\n"
" $text =~ s/&/&amp;/g;\n"
" $text =~ s/</&lt;/g;\n"
" $text =~ s/>/&gt;/g;\n"
" }\n"
" $im->stringTTF($black, $fonttype, $fontsize, 0.0, $x1 + 3, 3 + ($y1 + $y2) / 2, "
"$text, \"\");\n"
"\n"
" $im->group_end($nameattr);\n"
"}\n"
"\n"
"print $im->svg;\n"
"\n"
"if ($palette) {\n"
" write_palette();\n"
"}\n";
}
} // namespace brpc
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
// Authors: Tian,Ye (tianye15@baidu.com)
#pragma once
namespace brpc {
const char* flamegraph_perl();
} // namespace brpc
......@@ -16,8 +16,10 @@
// under the License.
// Authors: Ge,Jun (gejun@baidu.com)
// Tian,Ye(tianye15@baidu.com)
#include <stdio.h>
#include <thread>
#include <gflags/gflags.h>
#include "butil/files/file_enumerator.h"
#include "butil/file_util.h" // butil::FilePath
......@@ -28,6 +30,7 @@
#include "brpc/server.h"
#include "brpc/reloadable_flags.h"
#include "brpc/builtin/pprof_perl.h"
#include "brpc/builtin/flamegraph_perl.h"
#include "brpc/builtin/hotspots_service.h"
#include "brpc/details/tcmalloc_extension.h"
......@@ -43,6 +46,67 @@ void ContentionProfilerStop();
namespace brpc {
enum class DisplayType{
kUnknown,
#if defined(OS_LINUX)
kFlameGraph,
#endif
kDot,
kText
};
static const char* DisplayTypeToString(DisplayType type) {
switch (type) {
#if defined(OS_LINUX)
case DisplayType::kFlameGraph: return "flame";
#endif
case DisplayType::kDot: return "dot";
case DisplayType::kText: return "text";
default: return "unknown";
}
}
static DisplayType StringToDisplayType(const std::string& val) {
static butil::CaseIgnoredFlatMap<DisplayType>* display_type_map;
static std::once_flag flag;
std::call_once(flag, []() {
display_type_map = new butil::CaseIgnoredFlatMap<DisplayType>;
display_type_map->init(10);
#if defined(OS_LINUX)
(*display_type_map)["flame"] = DisplayType::kFlameGraph;
#endif
(*display_type_map)["dot"] = DisplayType::kDot;
(*display_type_map)["text"] = DisplayType::kText;
});
auto type = display_type_map->seek(val);
if (type == nullptr) {
return DisplayType::kUnknown;
}
return *type;
}
static std::string DisplayTypeToPProfArgument(DisplayType type) {
switch (type) {
#if defined(OS_LINUX)
case DisplayType::kFlameGraph: return " --collapsed ";
case DisplayType::kDot: return " --dot ";
case DisplayType::kText: return " --text ";
#elif defined(OS_MACOSX)
case DisplayType::kDot: return " -dot ";
case DisplayType::kText: return " -text ";
#endif
default: return " unknown type ";
}
}
static std::string GeneratePerlScriptPath(const std::string& filename) {
std::string path;
path.reserve(FLAGS_rpc_profiling_dir.size() + 1 + filename.size());
path += FLAGS_rpc_profiling_dir;
path.push_back('/');
path += filename;
return std::move(path);
}
extern bool cpu_profiler_enabled;
......@@ -54,6 +118,7 @@ DEFINE_int32(max_profiles_kept, 32,
BRPC_VALIDATE_GFLAG(max_profiles_kept, PassValidate);
static const char* const PPROF_FILENAME = "pprof.pl";
static const char* const FLAMEGRAPH_FILENAME = "flamegraph.pl";
static int DEFAULT_PROFILING_SECONDS = 10;
static size_t CONCURRENT_PROFILING_LIMIT = 256;
......@@ -228,16 +293,16 @@ static bool ValidProfilePath(const butil::StringPiece& path) {
static int MakeCacheName(char* cache_name, size_t len,
const char* prof_name,
const char* base_name,
bool use_text,
DisplayType display_type,
bool show_ccount) {
if (base_name) {
return snprintf(cache_name, len, "%s.cache/base_%s%s%s", prof_name,
return snprintf(cache_name, len, "%s.cache/base_%s.%s%s", prof_name,
base_name,
(use_text ? ".text" : ".dot"),
DisplayTypeToString(display_type),
(show_ccount ? ".ccount" : ""));
} else {
return snprintf(cache_name, len, "%s.cache/%s%s", prof_name,
(use_text ? "text" : "dot"),
DisplayTypeToString(display_type),
(show_ccount ? ".ccount" : ""));
}
......@@ -344,9 +409,16 @@ static void DisplayResult(Controller* cntl,
}
butil::IOBuf& resp = cntl->response_attachment();
const bool use_html = UseHTML(cntl->http_request());
const bool use_text = cntl->http_request().uri().GetQuery("text");
const bool show_ccount = cntl->http_request().uri().GetQuery("ccount");
const std::string* base_name = cntl->http_request().uri().GetQuery("base");
const std::string* display_type_query = cntl->http_request().uri().GetQuery("display_type");
DisplayType display_type = DisplayType::kFlameGraph;
if (display_type_query) {
display_type = StringToDisplayType(*display_type_query);
if (display_type == DisplayType::kUnknown) {
return cntl->SetFailed(EINVAL, "Invalid display_type=%s", display_type_query->c_str());
}
}
if (base_name != NULL) {
if (!ValidProfilePath(*base_name)) {
return cntl->SetFailed(EINVAL, "Invalid query `base'");
......@@ -361,7 +433,7 @@ static void DisplayResult(Controller* cntl,
char expected_result_name[256];
MakeCacheName(expected_result_name, sizeof(expected_result_name),
prof_name, GetBaseName(base_name),
use_text, show_ccount);
display_type, show_ccount);
// Try to read cache first.
FILE* fp = fopen(expected_result_name, "r");
if (fp != NULL) {
......@@ -400,23 +472,29 @@ static void DisplayResult(Controller* cntl,
}
std::ostringstream cmd_builder;
std::string pprof_tool;
pprof_tool.reserve(FLAGS_rpc_profiling_dir.size() + 1 + strlen(PPROF_FILENAME));
pprof_tool += FLAGS_rpc_profiling_dir;
pprof_tool.push_back('/');
pprof_tool += PPROF_FILENAME;
std::string pprof_tool{GeneratePerlScriptPath(PPROF_FILENAME)};
std::string flamegraph_tool{GeneratePerlScriptPath(FLAMEGRAPH_FILENAME)};
#if defined(OS_LINUX)
cmd_builder << "perl " << pprof_tool
<< (use_text ? " --text " : " --dot ")
<< DisplayTypeToPProfArgument(display_type)
<< (show_ccount ? " --contention " : "");
if (base_name) {
cmd_builder << "--base " << *base_name << ' ';
}
cmd_builder << GetProgramName() << " " << prof_name << " 2>&1 ";
cmd_builder << GetProgramName() << " " << prof_name;
if (display_type == DisplayType::kFlameGraph) {
// For flamegraph, we don't care about pprof error msg,
// which will cause confusing messages in the final result.
cmd_builder << " 2>/dev/null " << " | " << "perl " << flamegraph_tool;
}
cmd_builder << " 2>&1 ";
#elif defined(OS_MACOSX)
cmd_builder << getenv("GOOGLE_PPROF_BINARY_PATH") << " "
<< (use_text ? " -text " : " -dot ")
<< DisplayTypeToPProfArgument(display_type)
<< (show_ccount ? " -contentions " : "");
if (base_name) {
cmd_builder << "-base " << *base_name << ' ';
......@@ -427,7 +505,8 @@ static void DisplayResult(Controller* cntl,
const std::string cmd = cmd_builder.str();
for (int ntry = 0; ntry < 2; ++ntry) {
if (!g_written_pprof_perl) {
if (!WriteSmallFile(pprof_tool.c_str(), pprof_perl())) {
if (!WriteSmallFile(pprof_tool.c_str(), pprof_perl()) ||
!WriteSmallFile(flamegraph_tool.c_str(), flamegraph_perl())) {
os << "Fail to write " << pprof_tool
<< (use_html ? "</body></html>" : "\n");
os.move_to(resp);
......@@ -442,12 +521,20 @@ static void DisplayResult(Controller* cntl,
butil::IOBufBuilder pprof_output;
const int rc = butil::read_command_output(pprof_output, cmd.c_str());
if (rc != 0) {
butil::FilePath path(pprof_tool);
if (!butil::PathExists(path)) {
butil::FilePath pprof_path(pprof_tool);
if (!butil::PathExists(pprof_path)) {
// Write the script again.
g_written_pprof_perl = false;
// tell user.
os << path.value() << " was removed, recreate ...\n\n";
os << pprof_path.value() << " was removed, recreate ...\n\n";
continue;
}
butil::FilePath flamegraph_path(flamegraph_tool);
if (!butil::PathExists(flamegraph_path)) {
// Write the script again.
g_written_pprof_perl = false;
// tell user.
os << flamegraph_path.value() << " was removed, recreate ...\n\n";
continue;
}
if (rc < 0) {
......@@ -464,7 +551,7 @@ static void DisplayResult(Controller* cntl,
// Cache result in file.
char result_name[256];
MakeCacheName(result_name, sizeof(result_name), prof_name,
GetBaseName(base_name), use_text, show_ccount);
GetBaseName(base_name), display_type, show_ccount);
// Append the profile name as the visual reminder for what
// current profile is.
......@@ -757,7 +844,6 @@ static void StartProfiling(ProfilingType type,
butil::IOBufBuilder os;
bool enabled = false;
const char* extra_desc = "";
if (type == PROFILING_CPU) {
enabled = cpu_profiler_enabled;
} else if (type == PROFILING_CONTENTION) {
......@@ -796,9 +882,16 @@ static void StartProfiling(ProfilingType type,
const int seconds = ReadSeconds(cntl);
const std::string* view = cntl->http_request().uri().GetQuery("view");
const bool use_text = cntl->http_request().uri().GetQuery("text");
const bool show_ccount = cntl->http_request().uri().GetQuery("ccount");
const std::string* base_name = cntl->http_request().uri().GetQuery("base");
const std::string* display_type_query = cntl->http_request().uri().GetQuery("display_type");
DisplayType display_type = DisplayType::kFlameGraph;
if (display_type_query) {
display_type = StringToDisplayType(*display_type_query);
if (display_type == DisplayType::kUnknown) {
return cntl->SetFailed(EINVAL, "Invalid display_type=%s", display_type_query->c_str());
}
}
ProfilingClient profiling_client;
size_t nwaiters = 0;
......@@ -824,38 +917,19 @@ static void StartProfiling(ProfilingType type,
"function generateURL() {\n"
" var past_prof = document.getElementById('view_prof').value;\n"
" var base_prof = document.getElementById('base_prof').value;\n"
" var use_text = document.getElementById('text_cb').checked;\n";
" var display_type = document.getElementById('display_type').value;\n";
if (type == PROFILING_CONTENTION) {
os << " var show_ccount = document.getElementById('ccount_cb').checked;\n";
}
os << " var targetURL = '/hotspots/" << type_str << "';\n"
" var first = true;\n"
" targetURL += '?' + 'display_type=' + display_type;\n"
" if (past_prof != '') {\n"
" if (first) {\n"
" targetURL += '?';\n"
" first = false;\n"
" } else {\n"
" targetURL += '&';\n"
" }\n"
" targetURL += '&';\n"
" targetURL += 'view=' + past_prof;\n"
" }\n"
" if (base_prof != '') {\n"
" if (first) {\n"
" targetURL += '?';\n"
" first = false;\n"
" } else {\n"
" targetURL += '&';\n"
" }\n"
" targetURL += '&';\n"
" targetURL += 'base=' + base_prof;\n"
" }\n"
" if (use_text) {\n"
" if (first) {\n"
" targetURL += '?';\n"
" first = false;\n"
" } else {\n"
" targetURL += '&';\n"
" }\n"
" targetURL += 'text';\n"
" }\n";
if (type == PROFILING_CONTENTION) {
os <<
......@@ -904,6 +978,7 @@ static void StartProfiling(ProfilingType type,
" data = data.substring(selEnd + '[addToProfEnd]'.length);\n"
" }\n"
" $(\"#profiling-result\").html('<pre>' + data + '</pre>');\n"
" if (data.indexOf('FlameGraph') != -1) { init(); }"
" } else {\n"
" $(\"#profiling-result\").html('Plotting ...');\n"
" var svg = Viz(data.substring(index), \"svg\");\n"
......@@ -921,9 +996,7 @@ static void StartProfiling(ProfilingType type,
if (profiling_client.id != 0) {
os << "&profiling_id=" << profiling_client.id;
}
if (use_text) {
os << "&text";
}
os << "&display_type=" << DisplayTypeToString(display_type);
if (show_ccount) {
os << "&ccount";
}
......@@ -1004,18 +1077,21 @@ static void StartProfiling(ProfilingType type,
}
os << '>' << GetBaseName(&past_profs[i]);
}
os << "</select>"
"&nbsp;&nbsp;&nbsp;<label for='text_cb'>"
"<input id='text_cb' type='checkbox'"
<< (use_text ? " checked=''" : "") <<
" onclick='onChangedCB(this);'>text</label>";
os << "</select>";
os << "<div><pre style='display:inline'>Display Type: </pre>"
"<select id='display_type' onchange='onSelectProf()'>"
#if defined(OS_LINUX)
"<option value=flame" << (display_type == DisplayType::kFlameGraph ? " selected" : "") << ">flame</option>"
#endif
"<option value=dot" << (display_type == DisplayType::kDot ? " selected" : "") << ">dot</option>"
"<option value=text" << (display_type == DisplayType::kText ? " selected" : "") << ">text</option></select>";
if (type == PROFILING_CONTENTION) {
os << "&nbsp;&nbsp;&nbsp;<label for='ccount_cb'>"
"<input id='ccount_cb' type='checkbox'"
<< (show_ccount ? " checked=''" : "") <<
" onclick='onChangedCB(this);'>count</label>";
}
os << "<br><pre style='display:inline'>Diff: </pre>"
os << "</div><div><pre style='display:inline'>Diff: </pre>"
"<select id='base_prof' onchange='onSelectProf()'>"
"<option value=''>&lt;none&gt;</option>";
for (size_t i = 0; i < past_profs.size(); ++i) {
......@@ -1025,7 +1101,7 @@ static void StartProfiling(ProfilingType type,
}
os << '>' << GetBaseName(&past_profs[i]);
}
os << "</select>";
os << "</select></div>";
if (!enabled && view == NULL) {
os << "<p><span style='color:red'>Error:</span> "
......@@ -1076,7 +1152,7 @@ static void StartProfiling(ProfilingType type,
}
os << "</div><pre class='logo'><span class='logo_text'>" << logo()
<< "</span></pre></body>\n";
if (!use_text) {
if (display_type == DisplayType::kDot) {
// don't need viz.js in text mode.
os << "<script language=\"javascript\" type=\"text/javascript\""
" src=\"/js/viz_min\"></script>\n";
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment