stackprof 0.2.14 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +43 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +13 -6
- data/README.md +57 -51
- data/Rakefile +11 -25
- data/ext/stackprof/stackprof.c +206 -70
- data/lib/stackprof/report.rb +28 -23
- data/lib/stackprof.rb +1 -1
- data/stackprof.gemspec +8 -1
- data/test/test_stackprof.rb +65 -11
- data/vendor/FlameGraph/flamegraph.pl +751 -85
- metadata +9 -7
- data/.travis.yml +0 -21
- data/Dockerfile +0 -21
- data/Gemfile.lock +0 -27
| @@ -10,23 +10,46 @@ | |
| 10 10 | 
             
            #
         | 
| 11 11 | 
             
            #        grep funcA input.txt | ./flamegraph.pl [options] > graph.svg
         | 
| 12 12 | 
             
            #
         | 
| 13 | 
            +
            # Then open the resulting .svg in a web browser, for interactivity: mouse-over
         | 
| 14 | 
            +
            # frames for info, click to zoom, and ctrl-F to search.
         | 
| 15 | 
            +
            #
         | 
| 13 16 | 
             
            # Options are listed in the usage message (--help).
         | 
| 14 17 | 
             
            #
         | 
| 15 18 | 
             
            # The input is stack frames and sample counts formatted as single lines.  Each
         | 
| 16 19 | 
             
            # frame in the stack is semicolon separated, with a space and count at the end
         | 
| 17 | 
            -
            # of the line.  These can be generated  | 
| 18 | 
            -
            #  | 
| 20 | 
            +
            # of the line.  These can be generated for Linux perf script output using
         | 
| 21 | 
            +
            # stackcollapse-perf.pl, for DTrace using stackcollapse.pl, and for other tools
         | 
| 22 | 
            +
            # using the other stackcollapse programs.  Example input:
         | 
| 23 | 
            +
            #
         | 
| 24 | 
            +
            #  swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 1
         | 
| 25 | 
            +
            #
         | 
| 26 | 
            +
            # An optional extra column of counts can be provided to generate a differential
         | 
| 27 | 
            +
            # flame graph of the counts, colored red for more, and blue for less.  This
         | 
| 28 | 
            +
            # can be useful when using flame graphs for non-regression testing.
         | 
| 29 | 
            +
            # See the header comment in the difffolded.pl program for instructions.
         | 
| 19 30 | 
             
            #
         | 
| 20 | 
            -
            # The  | 
| 21 | 
            -
            #  | 
| 22 | 
            -
            #  | 
| 31 | 
            +
            # The input functions can optionally have annotations at the end of each
         | 
| 32 | 
            +
            # function name, following a precedent by some tools (Linux perf's _[k]):
         | 
| 33 | 
            +
            # 	_[k] for kernel
         | 
| 34 | 
            +
            #	_[i] for inlined
         | 
| 35 | 
            +
            #	_[j] for jit
         | 
| 36 | 
            +
            #	_[w] for waker
         | 
| 37 | 
            +
            # Some of the stackcollapse programs support adding these annotations, eg,
         | 
| 38 | 
            +
            # stackcollapse-perf.pl --kernel --jit. They are used merely for colors by
         | 
| 39 | 
            +
            # some palettes, eg, flamegraph.pl --color=java.
         | 
| 40 | 
            +
            #
         | 
| 41 | 
            +
            # The output flame graph shows relative presence of functions in stack samples.
         | 
| 42 | 
            +
            # The ordering on the x-axis has no meaning; since the data is samples, time
         | 
| 43 | 
            +
            # order of events is not known.  The order used sorts function names
         | 
| 44 | 
            +
            # alphabetically.
         | 
| 23 45 | 
             
            #
         | 
| 24 46 | 
             
            # While intended to process stack samples, this can also process stack traces.
         | 
| 25 47 | 
             
            # For example, tracing stacks for memory allocation, or resource usage.  You
         | 
| 26 48 | 
             
            # can use --title to set the title to reflect the content, and --countname
         | 
| 27 49 | 
             
            # to change "samples" to "bytes" etc.
         | 
| 28 50 | 
             
            #
         | 
| 29 | 
            -
            # There are a few different palettes, selectable using --color.   | 
| 51 | 
            +
            # There are a few different palettes, selectable using --color.  By default,
         | 
| 52 | 
            +
            # the colors are selected at random (except for differentials).  Functions
         | 
| 30 53 | 
             
            # called "-" will be printed gray, which can be used for stack separators (eg,
         | 
| 31 54 | 
             
            # between user and kernel stacks).
         | 
| 32 55 | 
             
            #
         | 
| @@ -38,6 +61,7 @@ | |
| 38 61 | 
             
            # was in turn inspired by the work on vftrace by Jan Boerhout".  See:
         | 
| 39 62 | 
             
            # https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and
         | 
| 40 63 | 
             
            #
         | 
| 64 | 
            +
            # Copyright 2016 Netflix, Inc.
         | 
| 41 65 | 
             
            # Copyright 2011 Joyent, Inc.  All rights reserved.
         | 
| 42 66 | 
             
            # Copyright 2011 Brendan Gregg.  All rights reserved.
         | 
| 43 67 | 
             
            #
         | 
| @@ -60,6 +84,7 @@ | |
| 60 84 | 
             
            #
         | 
| 61 85 | 
             
            # CDDL HEADER END
         | 
| 62 86 | 
             
            #
         | 
| 87 | 
            +
            # 11-Oct-2014	Adrien Mahieux	Added zoom.
         | 
| 63 88 | 
             
            # 21-Nov-2013   Shawn Sterling  Added consistent palette file option
         | 
| 64 89 | 
             
            # 17-Mar-2013   Tim Bunce       Added options and more tunables.
         | 
| 65 90 | 
             
            # 15-Dec-2011	Dave Pacheco	Support for frames with whitespace.
         | 
| @@ -69,6 +94,8 @@ use strict; | |
| 69 94 |  | 
| 70 95 | 
             
            use Getopt::Long;
         | 
| 71 96 |  | 
| 97 | 
            +
            use open qw(:std :utf8);
         | 
| 98 | 
            +
             | 
| 72 99 | 
             
            # tunables
         | 
| 73 100 | 
             
            my $encoding;
         | 
| 74 101 | 
             
            my $fonttype = "Verdana";
         | 
| @@ -77,12 +104,10 @@ my $frameheight = 16;           # max height is dynamic | |
| 77 104 | 
             
            my $fontsize = 12;              # base text size
         | 
| 78 105 | 
             
            my $fontwidth = 0.59;           # avg width relative to fontsize
         | 
| 79 106 | 
             
            my $minwidth = 0.1;             # min function width, pixels
         | 
| 80 | 
            -
            my $titletext = "Flame Graph";  # centered heading
         | 
| 81 107 | 
             
            my $nametype = "Function:";     # what are the names in the data?
         | 
| 82 108 | 
             
            my $countname = "samples";      # what are the counts in the data?
         | 
| 83 109 | 
             
            my $colors = "hot";             # color theme
         | 
| 84 | 
            -
            my $ | 
| 85 | 
            -
            my $bgcolor2 = "#eeeeb0";       # background color gradient stop
         | 
| 110 | 
            +
            my $bgcolors = "";              # background color theme
         | 
| 86 111 | 
             
            my $nameattrfile;               # file holding function attributes
         | 
| 87 112 | 
             
            my $timemax;                    # (override the) sum of the counts
         | 
| 88 113 | 
             
            my $factor = 1;                 # factor to scale counts by
         | 
| @@ -90,6 +115,48 @@ my $hash = 0;                   # color by function name | |
| 90 115 | 
             
            my $palette = 0;                # if we use consistent palettes (default off)
         | 
| 91 116 | 
             
            my %palette_map;                # palette map hash
         | 
| 92 117 | 
             
            my $pal_file = "palette.map";   # palette map file name
         | 
| 118 | 
            +
            my $stackreverse = 0;           # reverse stack order, switching merge end
         | 
| 119 | 
            +
            my $inverted = 0;               # icicle graph
         | 
| 120 | 
            +
            my $flamechart = 0;             # produce a flame chart (sort by time, do not merge stacks)
         | 
| 121 | 
            +
            my $negate = 0;                 # switch differential hues
         | 
| 122 | 
            +
            my $titletext = "";             # centered heading
         | 
| 123 | 
            +
            my $titledefault = "Flame Graph";	# overwritten by --title
         | 
| 124 | 
            +
            my $titleinverted = "Icicle Graph";	#   "    "
         | 
| 125 | 
            +
            my $searchcolor = "rgb(230,0,230)";	# color for search highlighting
         | 
| 126 | 
            +
            my $notestext = "";		# embedded notes in SVG
         | 
| 127 | 
            +
            my $subtitletext = "";		# second level title (optional)
         | 
| 128 | 
            +
            my $help = 0;
         | 
| 129 | 
            +
             | 
| 130 | 
            +
            sub usage {
         | 
| 131 | 
            +
            	die <<USAGE_END;
         | 
| 132 | 
            +
            USAGE: $0 [options] infile > outfile.svg\n
         | 
| 133 | 
            +
            	--title TEXT     # change title text
         | 
| 134 | 
            +
            	--subtitle TEXT  # second level title (optional)
         | 
| 135 | 
            +
            	--width NUM      # width of image (default 1200)
         | 
| 136 | 
            +
            	--height NUM     # height of each frame (default 16)
         | 
| 137 | 
            +
            	--minwidth NUM   # omit smaller functions (default 0.1 pixels)
         | 
| 138 | 
            +
            	--fonttype FONT  # font type (default "Verdana")
         | 
| 139 | 
            +
            	--fontsize NUM   # font size (default 12)
         | 
| 140 | 
            +
            	--countname TEXT # count type label (default "samples")
         | 
| 141 | 
            +
            	--nametype TEXT  # name type label (default "Function:")
         | 
| 142 | 
            +
            	--colors PALETTE # set color palette. choices are: hot (default), mem,
         | 
| 143 | 
            +
            	                 # io, wakeup, chain, java, js, perl, red, green, blue,
         | 
| 144 | 
            +
            	                 # aqua, yellow, purple, orange
         | 
| 145 | 
            +
            	--bgcolors COLOR # set background colors. gradient choices are yellow
         | 
| 146 | 
            +
            	                 # (default), blue, green, grey; flat colors use "#rrggbb"
         | 
| 147 | 
            +
            	--hash           # colors are keyed by function name hash
         | 
| 148 | 
            +
            	--cp             # use consistent palette (palette.map)
         | 
| 149 | 
            +
            	--reverse        # generate stack-reversed flame graph
         | 
| 150 | 
            +
            	--inverted       # icicle graph
         | 
| 151 | 
            +
            	--flamechart     # produce a flame chart (sort by time, do not merge stacks)
         | 
| 152 | 
            +
            	--negate         # switch differential hues (blue<->red)
         | 
| 153 | 
            +
            	--notes TEXT     # add notes comment in SVG (for debugging)
         | 
| 154 | 
            +
            	--help           # this message
         | 
| 155 | 
            +
             | 
| 156 | 
            +
            	eg,
         | 
| 157 | 
            +
            	$0 --title="Flame Graph: malloc()" trace.txt > graph.svg
         | 
| 158 | 
            +
            USAGE_END
         | 
| 159 | 
            +
            }
         | 
| 93 160 |  | 
| 94 161 | 
             
            GetOptions(
         | 
| 95 162 | 
             
            	'fonttype=s'  => \$fonttype,
         | 
| @@ -100,40 +167,47 @@ GetOptions( | |
| 100 167 | 
             
            	'fontwidth=f' => \$fontwidth,
         | 
| 101 168 | 
             
            	'minwidth=f'  => \$minwidth,
         | 
| 102 169 | 
             
            	'title=s'     => \$titletext,
         | 
| 170 | 
            +
            	'subtitle=s'  => \$subtitletext,
         | 
| 103 171 | 
             
            	'nametype=s'  => \$nametype,
         | 
| 104 172 | 
             
            	'countname=s' => \$countname,
         | 
| 105 173 | 
             
            	'nameattr=s'  => \$nameattrfile,
         | 
| 106 174 | 
             
            	'total=s'     => \$timemax,
         | 
| 107 175 | 
             
            	'factor=f'    => \$factor,
         | 
| 108 176 | 
             
            	'colors=s'    => \$colors,
         | 
| 177 | 
            +
            	'bgcolors=s'  => \$bgcolors,
         | 
| 109 178 | 
             
            	'hash'        => \$hash,
         | 
| 110 179 | 
             
            	'cp'          => \$palette,
         | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
| 113 | 
            -
            	 | 
| 114 | 
            -
            	 | 
| 115 | 
            -
            	 | 
| 116 | 
            -
            	 | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
            	--countname   # count type label (default "samples")
         | 
| 120 | 
            -
            	--nametype    # name type label (default "Function:")
         | 
| 121 | 
            -
            	--colors      # "hot", "mem", "io" palette (default "hot")
         | 
| 122 | 
            -
            	--hash        # colors are keyed by function name hash
         | 
| 123 | 
            -
            	--cp          # use consistent palette (palette.map)
         | 
| 124 | 
            -
             | 
| 125 | 
            -
            	eg,
         | 
| 126 | 
            -
            	$0 --title="Flame Graph: malloc()" trace.txt > graph.svg
         | 
| 127 | 
            -
            USAGE_END
         | 
| 180 | 
            +
            	'reverse'     => \$stackreverse,
         | 
| 181 | 
            +
            	'inverted'    => \$inverted,
         | 
| 182 | 
            +
            	'flamechart'  => \$flamechart,
         | 
| 183 | 
            +
            	'negate'      => \$negate,
         | 
| 184 | 
            +
            	'notes=s'     => \$notestext,
         | 
| 185 | 
            +
            	'help'        => \$help,
         | 
| 186 | 
            +
            ) or usage();
         | 
| 187 | 
            +
            $help && usage();
         | 
| 128 188 |  | 
| 129 189 | 
             
            # internals
         | 
| 130 | 
            -
            my $ypad1 = $fontsize *  | 
| 190 | 
            +
            my $ypad1 = $fontsize * 3;      # pad top, include title
         | 
| 131 191 | 
             
            my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels
         | 
| 192 | 
            +
            my $ypad3 = $fontsize * 2;      # pad top, include subtitle (optional)
         | 
| 132 193 | 
             
            my $xpad = 10;                  # pad lefm and right
         | 
| 194 | 
            +
            my $framepad = 1;		# vertical padding for frames
         | 
| 133 195 | 
             
            my $depthmax = 0;
         | 
| 134 196 | 
             
            my %Events;
         | 
| 135 197 | 
             
            my %nameattr;
         | 
| 136 198 |  | 
| 199 | 
            +
            if ($flamechart && $titletext eq "") {
         | 
| 200 | 
            +
            	$titletext = "Flame Chart";
         | 
| 201 | 
            +
            }
         | 
| 202 | 
            +
             | 
| 203 | 
            +
            if ($titletext eq "") {
         | 
| 204 | 
            +
            	unless ($inverted) {
         | 
| 205 | 
            +
            		$titletext = $titledefault;
         | 
| 206 | 
            +
            	} else {
         | 
| 207 | 
            +
            		$titletext = $titleinverted;
         | 
| 208 | 
            +
            	}
         | 
| 209 | 
            +
            }
         | 
| 210 | 
            +
             | 
| 137 211 | 
             
            if ($nameattrfile) {
         | 
| 138 212 | 
             
            	# The name-attribute file format is a function name followed by a tab then
         | 
| 139 213 | 
             
            	# a sequence of tab separated name=value pairs.
         | 
| @@ -146,8 +220,42 @@ if ($nameattrfile) { | |
| 146 220 | 
             
            	}
         | 
| 147 221 | 
             
            }
         | 
| 148 222 |  | 
| 149 | 
            -
            if ($ | 
| 150 | 
            -
             | 
| 223 | 
            +
            if ($notestext =~ /[<>]/) {
         | 
| 224 | 
            +
            	die "Notes string can't contain < or >"
         | 
| 225 | 
            +
            }
         | 
| 226 | 
            +
             | 
| 227 | 
            +
            # background colors:
         | 
| 228 | 
            +
            # - yellow gradient: default (hot, java, js, perl)
         | 
| 229 | 
            +
            # - green gradient: mem
         | 
| 230 | 
            +
            # - blue gradient: io, wakeup, chain
         | 
| 231 | 
            +
            # - gray gradient: flat colors (red, green, blue, ...)
         | 
| 232 | 
            +
            if ($bgcolors eq "") {
         | 
| 233 | 
            +
            	# choose a default
         | 
| 234 | 
            +
            	if ($colors eq "mem") {
         | 
| 235 | 
            +
            		$bgcolors = "green";
         | 
| 236 | 
            +
            	} elsif ($colors =~ /^(io|wakeup|chain)$/) {
         | 
| 237 | 
            +
            		$bgcolors = "blue";
         | 
| 238 | 
            +
            	} elsif ($colors =~ /^(red|green|blue|aqua|yellow|purple|orange)$/) {
         | 
| 239 | 
            +
            		$bgcolors = "grey";
         | 
| 240 | 
            +
            	} else {
         | 
| 241 | 
            +
            		$bgcolors = "yellow";
         | 
| 242 | 
            +
            	}
         | 
| 243 | 
            +
            }
         | 
| 244 | 
            +
            my ($bgcolor1, $bgcolor2);
         | 
| 245 | 
            +
            if ($bgcolors eq "yellow") {
         | 
| 246 | 
            +
            	$bgcolor1 = "#eeeeee";       # background color gradient start
         | 
| 247 | 
            +
            	$bgcolor2 = "#eeeeb0";       # background color gradient stop
         | 
| 248 | 
            +
            } elsif ($bgcolors eq "blue") {
         | 
| 249 | 
            +
            	$bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff";
         | 
| 250 | 
            +
            } elsif ($bgcolors eq "green") {
         | 
| 251 | 
            +
            	$bgcolor1 = "#eef2ee"; $bgcolor2 = "#e0ffe0";
         | 
| 252 | 
            +
            } elsif ($bgcolors eq "grey") {
         | 
| 253 | 
            +
            	$bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8";
         | 
| 254 | 
            +
            } elsif ($bgcolors =~ /^#......$/) {
         | 
| 255 | 
            +
            	$bgcolor1 = $bgcolor2 = $bgcolors;
         | 
| 256 | 
            +
            } else {
         | 
| 257 | 
            +
            	die "Unrecognized bgcolor option \"$bgcolors\""
         | 
| 258 | 
            +
            }
         | 
| 151 259 |  | 
| 152 260 | 
             
            # SVG functions
         | 
| 153 261 | 
             
            { package SVG;
         | 
| @@ -168,6 +276,8 @@ if ($colors eq "io")  { $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8"; } | |
| 168 276 | 
             
            <?xml version="1.0"$enc_attr standalone="no"?>
         | 
| 169 277 | 
             
            <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
         | 
| 170 278 | 
             
            <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">
         | 
| 279 | 
            +
            <!-- Flame graph stack visualization. See https://github.com/brendangregg/FlameGraph for latest version, and http://www.brendangregg.com/flamegraphs.html for examples. -->
         | 
| 280 | 
            +
            <!-- NOTES: $notestext -->
         | 
| 171 281 | 
             
            SVG
         | 
| 172 282 | 
             
            	}
         | 
| 173 283 |  | 
| @@ -186,27 +296,26 @@ SVG | |
| 186 296 |  | 
| 187 297 | 
             
            		my @g_attr = map {
         | 
| 188 298 | 
             
            			exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : ()
         | 
| 189 | 
            -
            		} qw(class | 
| 299 | 
            +
            		} qw(id class);
         | 
| 190 300 | 
             
            		push @g_attr, $attr->{g_extra} if $attr->{g_extra};
         | 
| 191 | 
            -
            		$self->{svg} .= sprintf qq/<g %s>\n/, join(' ', @g_attr);
         | 
| 192 | 
            -
             | 
| 193 | 
            -
            		$self->{svg} .= sprintf qq/<title>%s<\/title>/, $attr->{title}
         | 
| 194 | 
            -
            			if $attr->{title}; # should be first element within g container
         | 
| 195 | 
            -
             | 
| 196 301 | 
             
            		if ($attr->{href}) {
         | 
| 197 302 | 
             
            			my @a_attr;
         | 
| 198 303 | 
             
            			push @a_attr, sprintf qq/xlink:href="%s"/, $attr->{href} if $attr->{href};
         | 
| 199 304 | 
             
            			# default target=_top else links will open within SVG <object>
         | 
| 200 305 | 
             
            			push @a_attr, sprintf qq/target="%s"/, $attr->{target} || "_top";
         | 
| 201 306 | 
             
            			push @a_attr, $attr->{a_extra}                           if $attr->{a_extra};
         | 
| 202 | 
            -
            			$self->{svg} .= sprintf qq/<a %s | 
| 307 | 
            +
            			$self->{svg} .= sprintf qq/<a %s>\n/, join(' ', (@a_attr, @g_attr));
         | 
| 308 | 
            +
            		} else {
         | 
| 309 | 
            +
            			$self->{svg} .= sprintf qq/<g %s>\n/, join(' ', @g_attr);
         | 
| 203 310 | 
             
            		}
         | 
| 311 | 
            +
             | 
| 312 | 
            +
            		$self->{svg} .= sprintf qq/<title>%s<\/title>/, $attr->{title}
         | 
| 313 | 
            +
            			if $attr->{title}; # should be first element within g container
         | 
| 204 314 | 
             
            	}
         | 
| 205 315 |  | 
| 206 316 | 
             
            	sub group_end {
         | 
| 207 317 | 
             
            		my ($self, $attr) = @_;
         | 
| 208 | 
            -
            		$self->{svg} .= qq/<\/a>\n/  | 
| 209 | 
            -
            		$self->{svg} .= qq/<\/g>\n/;
         | 
| 318 | 
            +
            		$self->{svg} .= $attr->{href} ? qq/<\/a>\n/ : qq/<\/g>\n/;
         | 
| 210 319 | 
             
            	}
         | 
| 211 320 |  | 
| 212 321 | 
             
            	sub filledRectangle {
         | 
| @@ -220,10 +329,11 @@ SVG | |
| 220 329 | 
             
            	}
         | 
| 221 330 |  | 
| 222 331 | 
             
            	sub stringTTF {
         | 
| 223 | 
            -
            		my ($self, $ | 
| 224 | 
            -
            		$ | 
| 225 | 
            -
            		$ | 
| 226 | 
            -
            		$ | 
| 332 | 
            +
            		my ($self, $id, $x, $y, $str, $extra) = @_;
         | 
| 333 | 
            +
            		$x = sprintf "%0.2f", $x;
         | 
| 334 | 
            +
            		$id =  defined $id ? qq/id="$id"/ : "";
         | 
| 335 | 
            +
            		$extra ||= "";
         | 
| 336 | 
            +
            		$self->{svg} .= qq/<text $id x="$x" y="$y" $extra>$str<\/text>\n/;
         | 
| 227 337 | 
             
            	}
         | 
| 228 338 |  | 
| 229 339 | 
             
            	sub svg {
         | 
| @@ -257,6 +367,7 @@ sub namehash { | |
| 257 367 | 
             
            sub color {
         | 
| 258 368 | 
             
            	my ($type, $hash, $name) = @_;
         | 
| 259 369 | 
             
            	my ($v1, $v2, $v3);
         | 
| 370 | 
            +
             | 
| 260 371 | 
             
            	if ($hash) {
         | 
| 261 372 | 
             
            		$v1 = namehash($name);
         | 
| 262 373 | 
             
            		$v2 = $v3 = namehash(scalar reverse $name);
         | 
| @@ -265,6 +376,8 @@ sub color { | |
| 265 376 | 
             
            		$v2 = rand(1);
         | 
| 266 377 | 
             
            		$v3 = rand(1);
         | 
| 267 378 | 
             
            	}
         | 
| 379 | 
            +
             | 
| 380 | 
            +
            	# theme palettes
         | 
| 268 381 | 
             
            	if (defined $type and $type eq "hot") {
         | 
| 269 382 | 
             
            		my $r = 205 + int(50 * $v3);
         | 
| 270 383 | 
             
            		my $g = 0 + int(230 * $v1);
         | 
| @@ -283,15 +396,138 @@ sub color { | |
| 283 396 | 
             
            		my $b = 190 + int(55 * $v2);
         | 
| 284 397 | 
             
            		return "rgb($r,$g,$b)";
         | 
| 285 398 | 
             
            	}
         | 
| 399 | 
            +
             | 
| 400 | 
            +
            	# multi palettes
         | 
| 401 | 
            +
            	if (defined $type and $type eq "java") {
         | 
| 402 | 
            +
            		# Handle both annotations (_[j], _[i], ...; which are
         | 
| 403 | 
            +
            		# accurate), as well as input that lacks any annotations, as
         | 
| 404 | 
            +
            		# best as possible. Without annotations, we get a little hacky
         | 
| 405 | 
            +
            		# and match on java|org|com, etc.
         | 
| 406 | 
            +
            		if ($name =~ m:_\[j\]$:) {	# jit annotation
         | 
| 407 | 
            +
            			$type = "green";
         | 
| 408 | 
            +
            		} elsif ($name =~ m:_\[i\]$:) {	# inline annotation
         | 
| 409 | 
            +
            			$type = "aqua";
         | 
| 410 | 
            +
            		} elsif ($name =~ m:^L?(java|javax|jdk|net|org|com|io|sun)/:) {	# Java
         | 
| 411 | 
            +
            			$type = "green";
         | 
| 412 | 
            +
            		} elsif ($name =~ m:_\[k\]$:) {	# kernel annotation
         | 
| 413 | 
            +
            			$type = "orange";
         | 
| 414 | 
            +
            		} elsif ($name =~ /::/) {	# C++
         | 
| 415 | 
            +
            			$type = "yellow";
         | 
| 416 | 
            +
            		} else {			# system
         | 
| 417 | 
            +
            			$type = "red";
         | 
| 418 | 
            +
            		}
         | 
| 419 | 
            +
            		# fall-through to color palettes
         | 
| 420 | 
            +
            	}
         | 
| 421 | 
            +
            	if (defined $type and $type eq "perl") {
         | 
| 422 | 
            +
            		if ($name =~ /::/) {		# C++
         | 
| 423 | 
            +
            			$type = "yellow";
         | 
| 424 | 
            +
            		} elsif ($name =~ m:Perl: or $name =~ m:\.pl:) {	# Perl
         | 
| 425 | 
            +
            			$type = "green";
         | 
| 426 | 
            +
            		} elsif ($name =~ m:_\[k\]$:) {	# kernel
         | 
| 427 | 
            +
            			$type = "orange";
         | 
| 428 | 
            +
            		} else {			# system
         | 
| 429 | 
            +
            			$type = "red";
         | 
| 430 | 
            +
            		}
         | 
| 431 | 
            +
            		# fall-through to color palettes
         | 
| 432 | 
            +
            	}
         | 
| 433 | 
            +
            	if (defined $type and $type eq "js") {
         | 
| 434 | 
            +
            		# Handle both annotations (_[j], _[i], ...; which are
         | 
| 435 | 
            +
            		# accurate), as well as input that lacks any annotations, as
         | 
| 436 | 
            +
            		# best as possible. Without annotations, we get a little hacky,
         | 
| 437 | 
            +
            		# and match on a "/" with a ".js", etc.
         | 
| 438 | 
            +
            		if ($name =~ m:_\[j\]$:) {	# jit annotation
         | 
| 439 | 
            +
            			if ($name =~ m:/:) {
         | 
| 440 | 
            +
            				$type = "green";	# source
         | 
| 441 | 
            +
            			} else {
         | 
| 442 | 
            +
            				$type = "aqua";		# builtin
         | 
| 443 | 
            +
            			}
         | 
| 444 | 
            +
            		} elsif ($name =~ /::/) {	# C++
         | 
| 445 | 
            +
            			$type = "yellow";
         | 
| 446 | 
            +
            		} elsif ($name =~ m:/.*\.js:) {	# JavaScript (match "/" in path)
         | 
| 447 | 
            +
            			$type = "green";
         | 
| 448 | 
            +
            		} elsif ($name =~ m/:/) {	# JavaScript (match ":" in builtin)
         | 
| 449 | 
            +
            			$type = "aqua";
         | 
| 450 | 
            +
            		} elsif ($name =~ m/^ $/) {	# Missing symbol
         | 
| 451 | 
            +
            			$type = "green";
         | 
| 452 | 
            +
            		} elsif ($name =~ m:_\[k\]:) {	# kernel
         | 
| 453 | 
            +
            			$type = "orange";
         | 
| 454 | 
            +
            		} else {			# system
         | 
| 455 | 
            +
            			$type = "red";
         | 
| 456 | 
            +
            		}
         | 
| 457 | 
            +
            		# fall-through to color palettes
         | 
| 458 | 
            +
            	}
         | 
| 459 | 
            +
            	if (defined $type and $type eq "wakeup") {
         | 
| 460 | 
            +
            		$type = "aqua";
         | 
| 461 | 
            +
            		# fall-through to color palettes
         | 
| 462 | 
            +
            	}
         | 
| 463 | 
            +
            	if (defined $type and $type eq "chain") {
         | 
| 464 | 
            +
            		if ($name =~ m:_\[w\]:) {	# waker
         | 
| 465 | 
            +
            			$type = "aqua"
         | 
| 466 | 
            +
            		} else {			# off-CPU
         | 
| 467 | 
            +
            			$type = "blue";
         | 
| 468 | 
            +
            		}
         | 
| 469 | 
            +
            		# fall-through to color palettes
         | 
| 470 | 
            +
            	}
         | 
| 471 | 
            +
             | 
| 472 | 
            +
            	# color palettes
         | 
| 473 | 
            +
            	if (defined $type and $type eq "red") {
         | 
| 474 | 
            +
            		my $r = 200 + int(55 * $v1);
         | 
| 475 | 
            +
            		my $x = 50 + int(80 * $v1);
         | 
| 476 | 
            +
            		return "rgb($r,$x,$x)";
         | 
| 477 | 
            +
            	}
         | 
| 478 | 
            +
            	if (defined $type and $type eq "green") {
         | 
| 479 | 
            +
            		my $g = 200 + int(55 * $v1);
         | 
| 480 | 
            +
            		my $x = 50 + int(60 * $v1);
         | 
| 481 | 
            +
            		return "rgb($x,$g,$x)";
         | 
| 482 | 
            +
            	}
         | 
| 483 | 
            +
            	if (defined $type and $type eq "blue") {
         | 
| 484 | 
            +
            		my $b = 205 + int(50 * $v1);
         | 
| 485 | 
            +
            		my $x = 80 + int(60 * $v1);
         | 
| 486 | 
            +
            		return "rgb($x,$x,$b)";
         | 
| 487 | 
            +
            	}
         | 
| 488 | 
            +
            	if (defined $type and $type eq "yellow") {
         | 
| 489 | 
            +
            		my $x = 175 + int(55 * $v1);
         | 
| 490 | 
            +
            		my $b = 50 + int(20 * $v1);
         | 
| 491 | 
            +
            		return "rgb($x,$x,$b)";
         | 
| 492 | 
            +
            	}
         | 
| 493 | 
            +
            	if (defined $type and $type eq "purple") {
         | 
| 494 | 
            +
            		my $x = 190 + int(65 * $v1);
         | 
| 495 | 
            +
            		my $g = 80 + int(60 * $v1);
         | 
| 496 | 
            +
            		return "rgb($x,$g,$x)";
         | 
| 497 | 
            +
            	}
         | 
| 498 | 
            +
            	if (defined $type and $type eq "aqua") {
         | 
| 499 | 
            +
            		my $r = 50 + int(60 * $v1);
         | 
| 500 | 
            +
            		my $g = 165 + int(55 * $v1);
         | 
| 501 | 
            +
            		my $b = 165 + int(55 * $v1);
         | 
| 502 | 
            +
            		return "rgb($r,$g,$b)";
         | 
| 503 | 
            +
            	}
         | 
| 504 | 
            +
            	if (defined $type and $type eq "orange") {
         | 
| 505 | 
            +
            		my $r = 190 + int(65 * $v1);
         | 
| 506 | 
            +
            		my $g = 90 + int(65 * $v1);
         | 
| 507 | 
            +
            		return "rgb($r,$g,0)";
         | 
| 508 | 
            +
            	}
         | 
| 509 | 
            +
             | 
| 286 510 | 
             
            	return "rgb(0,0,0)";
         | 
| 287 511 | 
             
            }
         | 
| 288 512 |  | 
| 513 | 
            +
            sub color_scale {
         | 
| 514 | 
            +
            	my ($value, $max) = @_;
         | 
| 515 | 
            +
            	my ($r, $g, $b) = (255, 255, 255);
         | 
| 516 | 
            +
            	$value = -$value if $negate;
         | 
| 517 | 
            +
            	if ($value > 0) {
         | 
| 518 | 
            +
            		$g = $b = int(210 * ($max - $value) / $max);
         | 
| 519 | 
            +
            	} elsif ($value < 0) {
         | 
| 520 | 
            +
            		$r = $g = int(210 * ($max + $value) / $max);
         | 
| 521 | 
            +
            	}
         | 
| 522 | 
            +
            	return "rgb($r,$g,$b)";
         | 
| 523 | 
            +
            }
         | 
| 524 | 
            +
             | 
| 289 525 | 
             
            sub color_map {
         | 
| 290 526 | 
             
            	my ($colors, $func) = @_;
         | 
| 291 527 | 
             
            	if (exists $palette_map{$func}) {
         | 
| 292 528 | 
             
            		return $palette_map{$func};
         | 
| 293 529 | 
             
            	} else {
         | 
| 294 | 
            -
            		$palette_map{$func} = color($colors);
         | 
| 530 | 
            +
            		$palette_map{$func} = color($colors, $hash, $func);
         | 
| 295 531 | 
             
            		return $palette_map{$func};
         | 
| 296 532 | 
             
            	}
         | 
| 297 533 | 
             
            }
         | 
| @@ -316,11 +552,12 @@ sub read_palette { | |
| 316 552 | 
             
            	}
         | 
| 317 553 | 
             
            }
         | 
| 318 554 |  | 
| 319 | 
            -
            my %Node;
         | 
| 555 | 
            +
            my %Node;	# Hash of merged frame data
         | 
| 320 556 | 
             
            my %Tmp;
         | 
| 321 557 |  | 
| 558 | 
            +
            # flow() merges two stacks, storing the merged frames and value data in %Node.
         | 
| 322 559 | 
             
            sub flow {
         | 
| 323 | 
            -
            	my ($last, $this, $v) = @_;
         | 
| 560 | 
            +
            	my ($last, $this, $v, $d) = @_;
         | 
| 324 561 |  | 
| 325 562 | 
             
            	my $len_a = @$last - 1;
         | 
| 326 563 | 
             
            	my $len_b = @$this - 1;
         | 
| @@ -338,37 +575,122 @@ sub flow { | |
| 338 575 | 
             
            		# a unique ID is constructed from "func;depth;etime";
         | 
| 339 576 | 
             
            		# func-depth isn't unique, it may be repeated later.
         | 
| 340 577 | 
             
            		$Node{"$k;$v"}->{stime} = delete $Tmp{$k}->{stime};
         | 
| 578 | 
            +
            		if (defined $Tmp{$k}->{delta}) {
         | 
| 579 | 
            +
            			$Node{"$k;$v"}->{delta} = delete $Tmp{$k}->{delta};
         | 
| 580 | 
            +
            		}
         | 
| 341 581 | 
             
            		delete $Tmp{$k};
         | 
| 342 582 | 
             
            	}
         | 
| 343 583 |  | 
| 344 584 | 
             
            	for ($i = $len_same; $i <= $len_b; $i++) {
         | 
| 345 585 | 
             
            		my $k = "$this->[$i];$i";
         | 
| 346 586 | 
             
            		$Tmp{$k}->{stime} = $v;
         | 
| 587 | 
            +
            		if (defined $d) {
         | 
| 588 | 
            +
            			$Tmp{$k}->{delta} += $i == $len_b ? $d : 0;
         | 
| 589 | 
            +
            		}
         | 
| 347 590 | 
             
            	}
         | 
| 348 591 |  | 
| 349 592 | 
             
                    return $this;
         | 
| 350 593 | 
             
            }
         | 
| 351 594 |  | 
| 352 | 
            -
            #  | 
| 353 | 
            -
            my @Data | 
| 595 | 
            +
            # parse input
         | 
| 596 | 
            +
            my @Data;
         | 
| 597 | 
            +
            my @SortedData;
         | 
| 354 598 | 
             
            my $last = [];
         | 
| 355 599 | 
             
            my $time = 0;
         | 
| 600 | 
            +
            my $delta = undef;
         | 
| 356 601 | 
             
            my $ignored = 0;
         | 
| 357 | 
            -
             | 
| 602 | 
            +
            my $line;
         | 
| 603 | 
            +
            my $maxdelta = 1;
         | 
| 604 | 
            +
             | 
| 605 | 
            +
            # reverse if needed
         | 
| 606 | 
            +
            foreach (<>) {
         | 
| 358 607 | 
             
            	chomp;
         | 
| 359 | 
            -
            	 | 
| 360 | 
            -
            	 | 
| 608 | 
            +
            	$line = $_;
         | 
| 609 | 
            +
            	if ($stackreverse) {
         | 
| 610 | 
            +
            		# there may be an extra samples column for differentials
         | 
| 611 | 
            +
            		# XXX todo: redo these REs as one. It's repeated below.
         | 
| 612 | 
            +
            		my($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
         | 
| 613 | 
            +
            		my $samples2 = undef;
         | 
| 614 | 
            +
            		if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) {
         | 
| 615 | 
            +
            			$samples2 = $samples;
         | 
| 616 | 
            +
            			($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
         | 
| 617 | 
            +
            			unshift @Data, join(";", reverse split(";", $stack)) . " $samples $samples2";
         | 
| 618 | 
            +
            		} else {
         | 
| 619 | 
            +
            			unshift @Data, join(";", reverse split(";", $stack)) . " $samples";
         | 
| 620 | 
            +
            		}
         | 
| 621 | 
            +
            	} else {
         | 
| 622 | 
            +
            		unshift @Data, $line;
         | 
| 623 | 
            +
            	}
         | 
| 624 | 
            +
            }
         | 
| 625 | 
            +
             | 
| 626 | 
            +
            if ($flamechart) {
         | 
| 627 | 
            +
            	# In flame chart mode, just reverse the data so time moves from left to right.
         | 
| 628 | 
            +
            	@SortedData = reverse @Data;
         | 
| 629 | 
            +
            } else {
         | 
| 630 | 
            +
            	@SortedData = sort @Data;
         | 
| 631 | 
            +
            }
         | 
| 632 | 
            +
             | 
| 633 | 
            +
            # process and merge frames
         | 
| 634 | 
            +
            foreach (@SortedData) {
         | 
| 635 | 
            +
            	chomp;
         | 
| 636 | 
            +
            	# process: folded_stack count
         | 
| 637 | 
            +
            	# eg: func_a;func_b;func_c 31
         | 
| 638 | 
            +
            	my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
         | 
| 639 | 
            +
            	unless (defined $samples and defined $stack) {
         | 
| 361 640 | 
             
            		++$ignored;
         | 
| 362 641 | 
             
            		next;
         | 
| 363 642 | 
             
            	}
         | 
| 364 | 
            -
             | 
| 365 | 
            -
            	 | 
| 366 | 
            -
            	$ | 
| 643 | 
            +
             | 
| 644 | 
            +
            	# there may be an extra samples column for differentials:
         | 
| 645 | 
            +
            	my $samples2 = undef;
         | 
| 646 | 
            +
            	if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) {
         | 
| 647 | 
            +
            		$samples2 = $samples;
         | 
| 648 | 
            +
            		($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
         | 
| 649 | 
            +
            	}
         | 
| 650 | 
            +
            	$delta = undef;
         | 
| 651 | 
            +
            	if (defined $samples2) {
         | 
| 652 | 
            +
            		$delta = $samples2 - $samples;
         | 
| 653 | 
            +
            		$maxdelta = abs($delta) if abs($delta) > $maxdelta;
         | 
| 654 | 
            +
            	}
         | 
| 655 | 
            +
             | 
| 656 | 
            +
            	# for chain graphs, annotate waker frames with "_[w]", for later
         | 
| 657 | 
            +
            	# coloring. This is a hack, but has a precedent ("_[k]" from perf).
         | 
| 658 | 
            +
            	if ($colors eq "chain") {
         | 
| 659 | 
            +
            		my @parts = split ";--;", $stack;
         | 
| 660 | 
            +
            		my @newparts = ();
         | 
| 661 | 
            +
            		$stack = shift @parts;
         | 
| 662 | 
            +
            		$stack .= ";--;";
         | 
| 663 | 
            +
            		foreach my $part (@parts) {
         | 
| 664 | 
            +
            			$part =~ s/;/_[w];/g;
         | 
| 665 | 
            +
            			$part .= "_[w]";
         | 
| 666 | 
            +
            			push @newparts, $part;
         | 
| 667 | 
            +
            		}
         | 
| 668 | 
            +
            		$stack .= join ";--;", @parts;
         | 
| 669 | 
            +
            	}
         | 
| 670 | 
            +
             | 
| 671 | 
            +
            	# merge frames and populate %Node:
         | 
| 672 | 
            +
            	$last = flow($last, [ '', split ";", $stack ], $time, $delta);
         | 
| 673 | 
            +
             | 
| 674 | 
            +
            	if (defined $samples2) {
         | 
| 675 | 
            +
            		$time += $samples2;
         | 
| 676 | 
            +
            	} else {
         | 
| 677 | 
            +
            		$time += $samples;
         | 
| 678 | 
            +
            	}
         | 
| 367 679 | 
             
            }
         | 
| 368 | 
            -
            flow($last, [], $time);
         | 
| 369 | 
            -
            warn "Ignored $ignored lines with invalid format\n" if $ignored;
         | 
| 370 | 
            -
            die "ERROR: No stack counts found\n" unless $time;
         | 
| 680 | 
            +
            flow($last, [], $time, $delta);
         | 
| 371 681 |  | 
| 682 | 
            +
            warn "Ignored $ignored lines with invalid format\n" if $ignored;
         | 
| 683 | 
            +
            unless ($time) {
         | 
| 684 | 
            +
            	warn "ERROR: No stack counts found\n";
         | 
| 685 | 
            +
            	my $im = SVG->new();
         | 
| 686 | 
            +
            	# emit an error message SVG, for tools automating flamegraph use
         | 
| 687 | 
            +
            	my $imageheight = $fontsize * 5;
         | 
| 688 | 
            +
            	$im->header($imagewidth, $imageheight);
         | 
| 689 | 
            +
            	$im->stringTTF(undef, int($imagewidth / 2), $fontsize * 2,
         | 
| 690 | 
            +
            	    "ERROR: No valid input provided to flamegraph.pl.");
         | 
| 691 | 
            +
            	print $im->svg;
         | 
| 692 | 
            +
            	exit 2;
         | 
| 693 | 
            +
            }
         | 
| 372 694 | 
             
            if ($timemax and $timemax < $time) {
         | 
| 373 695 | 
             
            	warn "Specified --total $timemax is less than actual total $time, so ignored\n"
         | 
| 374 696 | 
             
            	if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc)
         | 
| @@ -392,55 +714,382 @@ while (my ($id, $node) = each %Node) { | |
| 392 714 | 
             
            	$depthmax = $depth if $depth > $depthmax;
         | 
| 393 715 | 
             
            }
         | 
| 394 716 |  | 
| 395 | 
            -
            #  | 
| 396 | 
            -
            my $imageheight = ($depthmax * $frameheight) + $ypad1 + $ypad2;
         | 
| 717 | 
            +
            # draw canvas, and embed interactive JavaScript program
         | 
| 718 | 
            +
            my $imageheight = (($depthmax + 1) * $frameheight) + $ypad1 + $ypad2;
         | 
| 719 | 
            +
            $imageheight += $ypad3 if $subtitletext ne "";
         | 
| 720 | 
            +
            my $titlesize = $fontsize + 5;
         | 
| 397 721 | 
             
            my $im = SVG->new();
         | 
| 722 | 
            +
            my ($black, $vdgrey, $dgrey) = (
         | 
| 723 | 
            +
            	$im->colorAllocate(0, 0, 0),
         | 
| 724 | 
            +
            	$im->colorAllocate(160, 160, 160),
         | 
| 725 | 
            +
            	$im->colorAllocate(200, 200, 200),
         | 
| 726 | 
            +
                );
         | 
| 398 727 | 
             
            $im->header($imagewidth, $imageheight);
         | 
| 399 728 | 
             
            my $inc = <<INC;
         | 
| 400 | 
            -
            <defs | 
| 729 | 
            +
            <defs>
         | 
| 401 730 | 
             
            	<linearGradient id="background" y1="0" y2="1" x1="0" x2="0" >
         | 
| 402 731 | 
             
            		<stop stop-color="$bgcolor1" offset="5%" />
         | 
| 403 732 | 
             
            		<stop stop-color="$bgcolor2" offset="95%" />
         | 
| 404 733 | 
             
            	</linearGradient>
         | 
| 405 734 | 
             
            </defs>
         | 
| 406 735 | 
             
            <style type="text/css">
         | 
| 407 | 
            -
            	 | 
| 736 | 
            +
            	text { font-family:$fonttype; font-size:${fontsize}px; fill:$black; }
         | 
| 737 | 
            +
            	#search { opacity:0.1; cursor:pointer; }
         | 
| 738 | 
            +
            	#search:hover, #search.show { opacity:1; }
         | 
| 739 | 
            +
            	#subtitle { text-anchor:middle; font-color:$vdgrey; }
         | 
| 740 | 
            +
            	#title { text-anchor:middle; font-size:${titlesize}px}
         | 
| 741 | 
            +
            	#unzoom { cursor:pointer; }
         | 
| 742 | 
            +
            	#frames > *:hover { stroke:black; stroke-width:0.5; cursor:pointer; }
         | 
| 743 | 
            +
            	.hide { display:none; }
         | 
| 744 | 
            +
            	.parent { opacity:0.5; }
         | 
| 408 745 | 
             
            </style>
         | 
| 409 746 | 
             
            <script type="text/ecmascript">
         | 
| 410 747 | 
             
            <![CDATA[
         | 
| 411 | 
            -
            	 | 
| 412 | 
            -
            	 | 
| 413 | 
            -
            	function  | 
| 414 | 
            -
             | 
| 748 | 
            +
            	"use strict";
         | 
| 749 | 
            +
            	var details, searchbtn, unzoombtn, matchedtxt, svg, searching;
         | 
| 750 | 
            +
            	function init(evt) {
         | 
| 751 | 
            +
            		details = document.getElementById("details").firstChild;
         | 
| 752 | 
            +
            		searchbtn = document.getElementById("search");
         | 
| 753 | 
            +
            		unzoombtn = document.getElementById("unzoom");
         | 
| 754 | 
            +
            		matchedtxt = document.getElementById("matched");
         | 
| 755 | 
            +
            		svg = document.getElementsByTagName("svg")[0];
         | 
| 756 | 
            +
            		searching = 0;
         | 
| 757 | 
            +
            	}
         | 
| 758 | 
            +
             | 
| 759 | 
            +
            	window.addEventListener("click", function(e) {
         | 
| 760 | 
            +
            		var target = find_group(e.target);
         | 
| 761 | 
            +
            		if (target) {
         | 
| 762 | 
            +
            			if (target.nodeName == "a") {
         | 
| 763 | 
            +
            				if (e.ctrlKey === false) return;
         | 
| 764 | 
            +
            				e.preventDefault();
         | 
| 765 | 
            +
            			}
         | 
| 766 | 
            +
            			if (target.classList.contains("parent")) unzoom();
         | 
| 767 | 
            +
            			zoom(target);
         | 
| 768 | 
            +
            		}
         | 
| 769 | 
            +
            		else if (e.target.id == "unzoom") unzoom();
         | 
| 770 | 
            +
            		else if (e.target.id == "search") search_prompt();
         | 
| 771 | 
            +
            	}, false)
         | 
| 772 | 
            +
             | 
| 773 | 
            +
            	// mouse-over for info
         | 
| 774 | 
            +
            	// show
         | 
| 775 | 
            +
            	window.addEventListener("mouseover", function(e) {
         | 
| 776 | 
            +
            		var target = find_group(e.target);
         | 
| 777 | 
            +
            		if (target) details.nodeValue = "$nametype " + g_to_text(target);
         | 
| 778 | 
            +
            	}, false)
         | 
| 779 | 
            +
             | 
| 780 | 
            +
            	// clear
         | 
| 781 | 
            +
            	window.addEventListener("mouseout", function(e) {
         | 
| 782 | 
            +
            		var target = find_group(e.target);
         | 
| 783 | 
            +
            		if (target) details.nodeValue = ' ';
         | 
| 784 | 
            +
            	}, false)
         | 
| 785 | 
            +
             | 
| 786 | 
            +
            	// ctrl-F for search
         | 
| 787 | 
            +
            	window.addEventListener("keydown",function (e) {
         | 
| 788 | 
            +
            		if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
         | 
| 789 | 
            +
            			e.preventDefault();
         | 
| 790 | 
            +
            			search_prompt();
         | 
| 791 | 
            +
            		}
         | 
| 792 | 
            +
            	}, false)
         | 
| 793 | 
            +
             | 
| 794 | 
            +
            	// functions
         | 
| 795 | 
            +
            	function find_child(node, selector) {
         | 
| 796 | 
            +
            		var children = node.querySelectorAll(selector);
         | 
| 797 | 
            +
            		if (children.length) return children[0];
         | 
| 798 | 
            +
            		return;
         | 
| 799 | 
            +
            	}
         | 
| 800 | 
            +
            	function find_group(node) {
         | 
| 801 | 
            +
            		var parent = node.parentElement;
         | 
| 802 | 
            +
            		if (!parent) return;
         | 
| 803 | 
            +
            		if (parent.id == "frames") return node;
         | 
| 804 | 
            +
            		return find_group(parent);
         | 
| 805 | 
            +
            	}
         | 
| 806 | 
            +
            	function orig_save(e, attr, val) {
         | 
| 807 | 
            +
            		if (e.attributes["_orig_" + attr] != undefined) return;
         | 
| 808 | 
            +
            		if (e.attributes[attr] == undefined) return;
         | 
| 809 | 
            +
            		if (val == undefined) val = e.attributes[attr].value;
         | 
| 810 | 
            +
            		e.setAttribute("_orig_" + attr, val);
         | 
| 811 | 
            +
            	}
         | 
| 812 | 
            +
            	function orig_load(e, attr) {
         | 
| 813 | 
            +
            		if (e.attributes["_orig_"+attr] == undefined) return;
         | 
| 814 | 
            +
            		e.attributes[attr].value = e.attributes["_orig_" + attr].value;
         | 
| 815 | 
            +
            		e.removeAttribute("_orig_"+attr);
         | 
| 816 | 
            +
            	}
         | 
| 817 | 
            +
            	function g_to_text(e) {
         | 
| 818 | 
            +
            		var text = find_child(e, "title").firstChild.nodeValue;
         | 
| 819 | 
            +
            		return (text)
         | 
| 820 | 
            +
            	}
         | 
| 821 | 
            +
            	function g_to_func(e) {
         | 
| 822 | 
            +
            		var func = g_to_text(e);
         | 
| 823 | 
            +
            		// if there's any manipulation we want to do to the function
         | 
| 824 | 
            +
            		// name before it's searched, do it here before returning.
         | 
| 825 | 
            +
            		return (func);
         | 
| 826 | 
            +
            	}
         | 
| 827 | 
            +
            	function update_text(e) {
         | 
| 828 | 
            +
            		var r = find_child(e, "rect");
         | 
| 829 | 
            +
            		var t = find_child(e, "text");
         | 
| 830 | 
            +
            		var w = parseFloat(r.attributes.width.value) -3;
         | 
| 831 | 
            +
            		var txt = find_child(e, "title").textContent.replace(/\\([^(]*\\)\$/,"");
         | 
| 832 | 
            +
            		t.attributes.x.value = parseFloat(r.attributes.x.value) + 3;
         | 
| 833 | 
            +
             | 
| 834 | 
            +
            		// Smaller than this size won't fit anything
         | 
| 835 | 
            +
            		if (w < 2 * $fontsize * $fontwidth) {
         | 
| 836 | 
            +
            			t.textContent = "";
         | 
| 837 | 
            +
            			return;
         | 
| 838 | 
            +
            		}
         | 
| 839 | 
            +
             | 
| 840 | 
            +
            		t.textContent = txt;
         | 
| 841 | 
            +
            		// Fit in full text width
         | 
| 842 | 
            +
            		if (/^ *\$/.test(txt) || t.getSubStringLength(0, txt.length) < w)
         | 
| 843 | 
            +
            			return;
         | 
| 844 | 
            +
             | 
| 845 | 
            +
            		for (var x = txt.length - 2; x > 0; x--) {
         | 
| 846 | 
            +
            			if (t.getSubStringLength(0, x + 2) <= w) {
         | 
| 847 | 
            +
            				t.textContent = txt.substring(0, x) + "..";
         | 
| 848 | 
            +
            				return;
         | 
| 849 | 
            +
            			}
         | 
| 850 | 
            +
            		}
         | 
| 851 | 
            +
            		t.textContent = "";
         | 
| 852 | 
            +
            	}
         | 
| 853 | 
            +
             | 
| 854 | 
            +
            	// zoom
         | 
| 855 | 
            +
            	function zoom_reset(e) {
         | 
| 856 | 
            +
            		if (e.attributes != undefined) {
         | 
| 857 | 
            +
            			orig_load(e, "x");
         | 
| 858 | 
            +
            			orig_load(e, "width");
         | 
| 859 | 
            +
            		}
         | 
| 860 | 
            +
            		if (e.childNodes == undefined) return;
         | 
| 861 | 
            +
            		for (var i = 0, c = e.childNodes; i < c.length; i++) {
         | 
| 862 | 
            +
            			zoom_reset(c[i]);
         | 
| 863 | 
            +
            		}
         | 
| 864 | 
            +
            	}
         | 
| 865 | 
            +
            	function zoom_child(e, x, ratio) {
         | 
| 866 | 
            +
            		if (e.attributes != undefined) {
         | 
| 867 | 
            +
            			if (e.attributes.x != undefined) {
         | 
| 868 | 
            +
            				orig_save(e, "x");
         | 
| 869 | 
            +
            				e.attributes.x.value = (parseFloat(e.attributes.x.value) - x - $xpad) * ratio + $xpad;
         | 
| 870 | 
            +
            				if (e.tagName == "text")
         | 
| 871 | 
            +
            					e.attributes.x.value = find_child(e.parentNode, "rect[x]").attributes.x.value + 3;
         | 
| 872 | 
            +
            			}
         | 
| 873 | 
            +
            			if (e.attributes.width != undefined) {
         | 
| 874 | 
            +
            				orig_save(e, "width");
         | 
| 875 | 
            +
            				e.attributes.width.value = parseFloat(e.attributes.width.value) * ratio;
         | 
| 876 | 
            +
            			}
         | 
| 877 | 
            +
            		}
         | 
| 878 | 
            +
             | 
| 879 | 
            +
            		if (e.childNodes == undefined) return;
         | 
| 880 | 
            +
            		for (var i = 0, c = e.childNodes; i < c.length; i++) {
         | 
| 881 | 
            +
            			zoom_child(c[i], x - $xpad, ratio);
         | 
| 882 | 
            +
            		}
         | 
| 883 | 
            +
            	}
         | 
| 884 | 
            +
            	function zoom_parent(e) {
         | 
| 885 | 
            +
            		if (e.attributes) {
         | 
| 886 | 
            +
            			if (e.attributes.x != undefined) {
         | 
| 887 | 
            +
            				orig_save(e, "x");
         | 
| 888 | 
            +
            				e.attributes.x.value = $xpad;
         | 
| 889 | 
            +
            			}
         | 
| 890 | 
            +
            			if (e.attributes.width != undefined) {
         | 
| 891 | 
            +
            				orig_save(e, "width");
         | 
| 892 | 
            +
            				e.attributes.width.value = parseInt(svg.width.baseVal.value) - ($xpad * 2);
         | 
| 893 | 
            +
            			}
         | 
| 894 | 
            +
            		}
         | 
| 895 | 
            +
            		if (e.childNodes == undefined) return;
         | 
| 896 | 
            +
            		for (var i = 0, c = e.childNodes; i < c.length; i++) {
         | 
| 897 | 
            +
            			zoom_parent(c[i]);
         | 
| 898 | 
            +
            		}
         | 
| 899 | 
            +
            	}
         | 
| 900 | 
            +
            	function zoom(node) {
         | 
| 901 | 
            +
            		var attr = find_child(node, "rect").attributes;
         | 
| 902 | 
            +
            		var width = parseFloat(attr.width.value);
         | 
| 903 | 
            +
            		var xmin = parseFloat(attr.x.value);
         | 
| 904 | 
            +
            		var xmax = parseFloat(xmin + width);
         | 
| 905 | 
            +
            		var ymin = parseFloat(attr.y.value);
         | 
| 906 | 
            +
            		var ratio = (svg.width.baseVal.value - 2 * $xpad) / width;
         | 
| 907 | 
            +
             | 
| 908 | 
            +
            		// XXX: Workaround for JavaScript float issues (fix me)
         | 
| 909 | 
            +
            		var fudge = 0.0001;
         | 
| 910 | 
            +
             | 
| 911 | 
            +
            		unzoombtn.classList.remove("hide");
         | 
| 912 | 
            +
             | 
| 913 | 
            +
            		var el = document.getElementById("frames").children;
         | 
| 914 | 
            +
            		for (var i = 0; i < el.length; i++) {
         | 
| 915 | 
            +
            			var e = el[i];
         | 
| 916 | 
            +
            			var a = find_child(e, "rect").attributes;
         | 
| 917 | 
            +
            			var ex = parseFloat(a.x.value);
         | 
| 918 | 
            +
            			var ew = parseFloat(a.width.value);
         | 
| 919 | 
            +
            			var upstack;
         | 
| 920 | 
            +
            			// Is it an ancestor
         | 
| 921 | 
            +
            			if ($inverted == 0) {
         | 
| 922 | 
            +
            				upstack = parseFloat(a.y.value) > ymin;
         | 
| 923 | 
            +
            			} else {
         | 
| 924 | 
            +
            				upstack = parseFloat(a.y.value) < ymin;
         | 
| 925 | 
            +
            			}
         | 
| 926 | 
            +
            			if (upstack) {
         | 
| 927 | 
            +
            				// Direct ancestor
         | 
| 928 | 
            +
            				if (ex <= xmin && (ex+ew+fudge) >= xmax) {
         | 
| 929 | 
            +
            					e.classList.add("parent");
         | 
| 930 | 
            +
            					zoom_parent(e);
         | 
| 931 | 
            +
            					update_text(e);
         | 
| 932 | 
            +
            				}
         | 
| 933 | 
            +
            				// not in current path
         | 
| 934 | 
            +
            				else
         | 
| 935 | 
            +
            					e.classList.add("hide");
         | 
| 936 | 
            +
            			}
         | 
| 937 | 
            +
            			// Children maybe
         | 
| 938 | 
            +
            			else {
         | 
| 939 | 
            +
            				// no common path
         | 
| 940 | 
            +
            				if (ex < xmin || ex + fudge >= xmax) {
         | 
| 941 | 
            +
            					e.classList.add("hide");
         | 
| 942 | 
            +
            				}
         | 
| 943 | 
            +
            				else {
         | 
| 944 | 
            +
            					zoom_child(e, xmin, ratio);
         | 
| 945 | 
            +
            					update_text(e);
         | 
| 946 | 
            +
            				}
         | 
| 947 | 
            +
            			}
         | 
| 948 | 
            +
            		}
         | 
| 949 | 
            +
            	}
         | 
| 950 | 
            +
            	function unzoom() {
         | 
| 951 | 
            +
            		unzoombtn.classList.add("hide");
         | 
| 952 | 
            +
            		var el = document.getElementById("frames").children;
         | 
| 953 | 
            +
            		for(var i = 0; i < el.length; i++) {
         | 
| 954 | 
            +
            			el[i].classList.remove("parent");
         | 
| 955 | 
            +
            			el[i].classList.remove("hide");
         | 
| 956 | 
            +
            			zoom_reset(el[i]);
         | 
| 957 | 
            +
            			update_text(el[i]);
         | 
| 958 | 
            +
            		}
         | 
| 959 | 
            +
            	}
         | 
| 960 | 
            +
             | 
| 961 | 
            +
            	// search
         | 
| 962 | 
            +
            	function reset_search() {
         | 
| 963 | 
            +
            		var el = document.querySelectorAll("#frames rect");
         | 
| 964 | 
            +
            		for (var i = 0; i < el.length; i++) {
         | 
| 965 | 
            +
            			orig_load(el[i], "fill")
         | 
| 966 | 
            +
            		}
         | 
| 967 | 
            +
            	}
         | 
| 968 | 
            +
            	function search_prompt() {
         | 
| 969 | 
            +
            		if (!searching) {
         | 
| 970 | 
            +
            			var term = prompt("Enter a search term (regexp " +
         | 
| 971 | 
            +
            			    "allowed, eg: ^ext4_)", "");
         | 
| 972 | 
            +
            			if (term != null) {
         | 
| 973 | 
            +
            				search(term)
         | 
| 974 | 
            +
            			}
         | 
| 975 | 
            +
            		} else {
         | 
| 976 | 
            +
            			reset_search();
         | 
| 977 | 
            +
            			searching = 0;
         | 
| 978 | 
            +
            			searchbtn.classList.remove("show");
         | 
| 979 | 
            +
            			searchbtn.firstChild.nodeValue = "Search"
         | 
| 980 | 
            +
            			matchedtxt.classList.add("hide");
         | 
| 981 | 
            +
            			matchedtxt.firstChild.nodeValue = ""
         | 
| 982 | 
            +
            		}
         | 
| 983 | 
            +
            	}
         | 
| 984 | 
            +
            	function search(term) {
         | 
| 985 | 
            +
            		var re = new RegExp(term);
         | 
| 986 | 
            +
            		var el = document.getElementById("frames").children;
         | 
| 987 | 
            +
            		var matches = new Object();
         | 
| 988 | 
            +
            		var maxwidth = 0;
         | 
| 989 | 
            +
            		for (var i = 0; i < el.length; i++) {
         | 
| 990 | 
            +
            			var e = el[i];
         | 
| 991 | 
            +
            			var func = g_to_func(e);
         | 
| 992 | 
            +
            			var rect = find_child(e, "rect");
         | 
| 993 | 
            +
            			if (func == null || rect == null)
         | 
| 994 | 
            +
            				continue;
         | 
| 995 | 
            +
             | 
| 996 | 
            +
            			// Save max width. Only works as we have a root frame
         | 
| 997 | 
            +
            			var w = parseFloat(rect.attributes.width.value);
         | 
| 998 | 
            +
            			if (w > maxwidth)
         | 
| 999 | 
            +
            				maxwidth = w;
         | 
| 1000 | 
            +
             | 
| 1001 | 
            +
            			if (func.match(re)) {
         | 
| 1002 | 
            +
            				// highlight
         | 
| 1003 | 
            +
            				var x = parseFloat(rect.attributes.x.value);
         | 
| 1004 | 
            +
            				orig_save(rect, "fill");
         | 
| 1005 | 
            +
            				rect.attributes.fill.value = "$searchcolor";
         | 
| 1006 | 
            +
             | 
| 1007 | 
            +
            				// remember matches
         | 
| 1008 | 
            +
            				if (matches[x] == undefined) {
         | 
| 1009 | 
            +
            					matches[x] = w;
         | 
| 1010 | 
            +
            				} else {
         | 
| 1011 | 
            +
            					if (w > matches[x]) {
         | 
| 1012 | 
            +
            						// overwrite with parent
         | 
| 1013 | 
            +
            						matches[x] = w;
         | 
| 1014 | 
            +
            					}
         | 
| 1015 | 
            +
            				}
         | 
| 1016 | 
            +
            				searching = 1;
         | 
| 1017 | 
            +
            			}
         | 
| 1018 | 
            +
            		}
         | 
| 1019 | 
            +
            		if (!searching)
         | 
| 1020 | 
            +
            			return;
         | 
| 1021 | 
            +
             | 
| 1022 | 
            +
            		searchbtn.classList.add("show");
         | 
| 1023 | 
            +
            		searchbtn.firstChild.nodeValue = "Reset Search";
         | 
| 1024 | 
            +
             | 
| 1025 | 
            +
            		// calculate percent matched, excluding vertical overlap
         | 
| 1026 | 
            +
            		var count = 0;
         | 
| 1027 | 
            +
            		var lastx = -1;
         | 
| 1028 | 
            +
            		var lastw = 0;
         | 
| 1029 | 
            +
            		var keys = Array();
         | 
| 1030 | 
            +
            		for (k in matches) {
         | 
| 1031 | 
            +
            			if (matches.hasOwnProperty(k))
         | 
| 1032 | 
            +
            				keys.push(k);
         | 
| 1033 | 
            +
            		}
         | 
| 1034 | 
            +
            		// sort the matched frames by their x location
         | 
| 1035 | 
            +
            		// ascending, then width descending
         | 
| 1036 | 
            +
            		keys.sort(function(a, b){
         | 
| 1037 | 
            +
            			return a - b;
         | 
| 1038 | 
            +
            		});
         | 
| 1039 | 
            +
            		// Step through frames saving only the biggest bottom-up frames
         | 
| 1040 | 
            +
            		// thanks to the sort order. This relies on the tree property
         | 
| 1041 | 
            +
            		// where children are always smaller than their parents.
         | 
| 1042 | 
            +
            		var fudge = 0.0001;	// JavaScript floating point
         | 
| 1043 | 
            +
            		for (var k in keys) {
         | 
| 1044 | 
            +
            			var x = parseFloat(keys[k]);
         | 
| 1045 | 
            +
            			var w = matches[keys[k]];
         | 
| 1046 | 
            +
            			if (x >= lastx + lastw - fudge) {
         | 
| 1047 | 
            +
            				count += w;
         | 
| 1048 | 
            +
            				lastx = x;
         | 
| 1049 | 
            +
            				lastw = w;
         | 
| 1050 | 
            +
            			}
         | 
| 1051 | 
            +
            		}
         | 
| 1052 | 
            +
            		// display matched percent
         | 
| 1053 | 
            +
            		matchedtxt.classList.remove("hide");
         | 
| 1054 | 
            +
            		var pct = 100 * count / maxwidth;
         | 
| 1055 | 
            +
            		if (pct != 100) pct = pct.toFixed(1)
         | 
| 1056 | 
            +
            		matchedtxt.firstChild.nodeValue = "Matched: " + pct + "%";
         | 
| 1057 | 
            +
            	}
         | 
| 415 1058 | 
             
            ]]>
         | 
| 416 1059 | 
             
            </script>
         | 
| 417 1060 | 
             
            INC
         | 
| 418 1061 | 
             
            $im->include($inc);
         | 
| 419 1062 | 
             
            $im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)');
         | 
| 420 | 
            -
             | 
| 421 | 
            -
             | 
| 422 | 
            -
             | 
| 423 | 
            -
             | 
| 424 | 
            -
             | 
| 425 | 
            -
             | 
| 426 | 
            -
            $im->stringTTF($black, $fonttype, $fontsize + 5, 0.0, int($imagewidth / 2), $fontsize * 2, $titletext, "middle");
         | 
| 427 | 
            -
            $im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $imageheight - ($ypad2 / 2), " ", "", 'id="details"');
         | 
| 1063 | 
            +
            $im->stringTTF("title", int($imagewidth / 2), $fontsize * 2, $titletext);
         | 
| 1064 | 
            +
            $im->stringTTF("subtitle", int($imagewidth / 2), $fontsize * 4, $subtitletext) if $subtitletext ne "";
         | 
| 1065 | 
            +
            $im->stringTTF("details", $xpad, $imageheight - ($ypad2 / 2), " ");
         | 
| 1066 | 
            +
            $im->stringTTF("unzoom", $xpad, $fontsize * 2, "Reset Zoom", 'class="hide"');
         | 
| 1067 | 
            +
            $im->stringTTF("search", $imagewidth - $xpad - 100, $fontsize * 2, "Search");
         | 
| 1068 | 
            +
            $im->stringTTF("matched", $imagewidth - $xpad - 100, $imageheight - ($ypad2 / 2), " ");
         | 
| 428 1069 |  | 
| 429 1070 | 
             
            if ($palette) {
         | 
| 430 1071 | 
             
            	read_palette();
         | 
| 431 1072 | 
             
            }
         | 
| 432 | 
            -
            # Draw frames
         | 
| 433 1073 |  | 
| 1074 | 
            +
            # draw frames
         | 
| 1075 | 
            +
            $im->group_start({id => "frames"});
         | 
| 434 1076 | 
             
            while (my ($id, $node) = each %Node) {
         | 
| 435 1077 | 
             
            	my ($func, $depth, $etime) = split ";", $id;
         | 
| 436 1078 | 
             
            	my $stime = $node->{stime};
         | 
| 1079 | 
            +
            	my $delta = $node->{delta};
         | 
| 437 1080 |  | 
| 438 1081 | 
             
            	$etime = $timemax if $func eq "" and $depth == 0;
         | 
| 439 1082 |  | 
| 440 1083 | 
             
            	my $x1 = $xpad + $stime * $widthpertime;
         | 
| 441 1084 | 
             
            	my $x2 = $xpad + $etime * $widthpertime;
         | 
| 442 | 
            -
            	my $y1  | 
| 443 | 
            -
            	 | 
| 1085 | 
            +
            	my ($y1, $y2);
         | 
| 1086 | 
            +
            	unless ($inverted) {
         | 
| 1087 | 
            +
            		$y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad;
         | 
| 1088 | 
            +
            		$y2 = $imageheight - $ypad2 - $depth * $frameheight;
         | 
| 1089 | 
            +
            	} else {
         | 
| 1090 | 
            +
            		$y1 = $ypad1 + $depth * $frameheight;
         | 
| 1091 | 
            +
            		$y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad;
         | 
| 1092 | 
            +
            	}
         | 
| 444 1093 |  | 
| 445 1094 | 
             
            	my $samples = sprintf "%.0f", ($etime - $stime) * $factor;
         | 
| 446 1095 | 
             
            	(my $samples_txt = $samples) # add commas per perlfaq5
         | 
| @@ -452,38 +1101,55 @@ while (my ($id, $node) = each %Node) { | |
| 452 1101 | 
             
            	} else {
         | 
| 453 1102 | 
             
            		my $pct = sprintf "%.2f", ((100 * $samples) / ($timemax * $factor));
         | 
| 454 1103 | 
             
            		my $escaped_func = $func;
         | 
| 1104 | 
            +
            		# clean up SVG breaking characters:
         | 
| 455 1105 | 
             
            		$escaped_func =~ s/&/&/g;
         | 
| 456 1106 | 
             
            		$escaped_func =~ s/</</g;
         | 
| 457 1107 | 
             
            		$escaped_func =~ s/>/>/g;
         | 
| 458 | 
            -
            		$ | 
| 1108 | 
            +
            		$escaped_func =~ s/"/"/g;
         | 
| 1109 | 
            +
            		$escaped_func =~ s/_\[[kwij]\]$//;	# strip any annotation
         | 
| 1110 | 
            +
            		unless (defined $delta) {
         | 
| 1111 | 
            +
            			$info = "$escaped_func ($samples_txt $countname, $pct%)";
         | 
| 1112 | 
            +
            		} else {
         | 
| 1113 | 
            +
            			my $d = $negate ? -$delta : $delta;
         | 
| 1114 | 
            +
            			my $deltapct = sprintf "%.2f", ((100 * $d) / ($timemax * $factor));
         | 
| 1115 | 
            +
            			$deltapct = $d > 0 ? "+$deltapct" : $deltapct;
         | 
| 1116 | 
            +
            			$info = "$escaped_func ($samples_txt $countname, $pct%; $deltapct%)";
         | 
| 1117 | 
            +
            		}
         | 
| 459 1118 | 
             
            	}
         | 
| 460 1119 |  | 
| 461 1120 | 
             
            	my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone
         | 
| 462 | 
            -
            	$nameattr->{class}       ||= "func_g";
         | 
| 463 | 
            -
            	$nameattr->{onmouseover} ||= "s('".$info."')";
         | 
| 464 | 
            -
            	$nameattr->{onmouseout}  ||= "c()";
         | 
| 465 1121 | 
             
            	$nameattr->{title}       ||= $info;
         | 
| 466 1122 | 
             
            	$im->group_start($nameattr);
         | 
| 467 1123 |  | 
| 468 | 
            -
            	 | 
| 469 | 
            -
             | 
| 1124 | 
            +
            	my $color;
         | 
| 1125 | 
            +
            	if ($func eq "--") {
         | 
| 1126 | 
            +
            		$color = $vdgrey;
         | 
| 1127 | 
            +
            	} elsif ($func eq "-") {
         | 
| 1128 | 
            +
            		$color = $dgrey;
         | 
| 1129 | 
            +
            	} elsif (defined $delta) {
         | 
| 1130 | 
            +
            		$color = color_scale($delta, $maxdelta);
         | 
| 1131 | 
            +
            	} elsif ($palette) {
         | 
| 1132 | 
            +
            		$color = color_map($colors, $func);
         | 
| 470 1133 | 
             
            	} else {
         | 
| 471 | 
            -
            		 | 
| 472 | 
            -
            		$im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"');
         | 
| 1134 | 
            +
            		$color = color($colors, $hash, $func);
         | 
| 473 1135 | 
             
            	}
         | 
| 1136 | 
            +
            	$im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"');
         | 
| 474 1137 |  | 
| 475 1138 | 
             
            	my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth));
         | 
| 1139 | 
            +
            	my $text = "";
         | 
| 476 1140 | 
             
            	if ($chars >= 3) { # room for one char plus two dots
         | 
| 477 | 
            -
            		 | 
| 1141 | 
            +
            		$func =~ s/_\[[kwij]\]$//;	# strip any annotation
         | 
| 1142 | 
            +
            		$text = substr $func, 0, $chars;
         | 
| 478 1143 | 
             
            		substr($text, -2, 2) = ".." if $chars < length $func;
         | 
| 479 1144 | 
             
            		$text =~ s/&/&/g;
         | 
| 480 1145 | 
             
            		$text =~ s/</</g;
         | 
| 481 1146 | 
             
            		$text =~ s/>/>/g;
         | 
| 482 | 
            -
            		$im->stringTTF($black, $fonttype, $fontsize, 0.0, $x1 + 3, 3 + ($y1 + $y2) / 2, $text, "");
         | 
| 483 1147 | 
             
            	}
         | 
| 1148 | 
            +
            	$im->stringTTF(undef, $x1 + 3, 3 + ($y1 + $y2) / 2, $text);
         | 
| 484 1149 |  | 
| 485 1150 | 
             
            	$im->group_end($nameattr);
         | 
| 486 1151 | 
             
            }
         | 
| 1152 | 
            +
            $im->group_end();
         | 
| 487 1153 |  | 
| 488 1154 | 
             
            print $im->svg;
         | 
| 489 1155 |  |