diff --git a/.vscode/settings.json b/.vscode/settings.json index 4f1df64..1c22ffa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -102,6 +102,8 @@ "hash_set": "cpp", "regex": "cpp", "stack": "cpp", - "__memory": "cpp" + "__memory": "cpp", + "__verbose_abort": "cpp", + "print": "cpp" } } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 26932af..096f9ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required (VERSION 3.6) +INCLUDE_DIRECTORIES(include . ${Boost_INCLUDE_DIR}) + list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) set(BOT_NAME "BumbleCee") @@ -18,8 +20,11 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(THREADS_PREFER_PTHREAD_FLAG TRUE) +add_definitions( -DBOOST_ALL_NO_LIB ) +set( Boost_USE_STATIC_LIBS ON ) find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) +find_package(Boost REQUIRED) target_include_directories(${BOT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include @@ -35,6 +40,7 @@ target_link_libraries(${BOT_NAME} ${CMAKE_THREAD_LIBS_INIT} ${OPENSSL_CRYPTO_LIBRARY} ${OPENSSL_SSL_LIBRARY} + ${Boost_LIBRARIES} ) link_directories(/usr/lib) \ No newline at end of file diff --git a/flamegraph.pl b/flamegraph.pl new file mode 100755 index 0000000..ad35f6f --- /dev/null +++ b/flamegraph.pl @@ -0,0 +1,1303 @@ +#!/usr/bin/perl -w +# +# flamegraph.pl flame stack grapher. +# +# This takes stack samples and renders a call graph, allowing hot functions +# and codepaths to be quickly identified. Stack samples can be generated using +# tools such as DTrace, perf, SystemTap, and Instruments. +# +# USAGE: ./flamegraph.pl [options] input.txt > graph.svg +# +# grep funcA input.txt | ./flamegraph.pl [options] > graph.svg +# +# Then open the resulting .svg in a web browser, for interactivity: mouse-over +# frames for info, click to zoom, and ctrl-F to search. +# +# Options are listed in the usage message (--help). +# +# The input is stack frames and sample counts formatted as single lines. Each +# frame in the stack is semicolon separated, with a space and count at the end +# of the line. These can be generated for Linux perf script output using +# stackcollapse-perf.pl, for DTrace using stackcollapse.pl, and for other tools +# using the other stackcollapse programs. Example input: +# +# swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 1 +# +# An optional extra column of counts can be provided to generate a differential +# flame graph of the counts, colored red for more, and blue for less. This +# can be useful when using flame graphs for non-regression testing. +# See the header comment in the difffolded.pl program for instructions. +# +# The input functions can optionally have annotations at the end of each +# function name, following a precedent by some tools (Linux perf's _[k]): +# _[k] for kernel +# _[i] for inlined +# _[j] for jit +# _[w] for waker +# Some of the stackcollapse programs support adding these annotations, eg, +# stackcollapse-perf.pl --kernel --jit. They are used merely for colors by +# some palettes, eg, flamegraph.pl --color=java. +# +# The output flame graph shows relative presence of functions in stack samples. +# The ordering on the x-axis has no meaning; since the data is samples, time +# order of events is not known. The order used sorts function names +# alphabetically. +# +# While intended to process stack samples, this can also process stack traces. +# For example, tracing stacks for memory allocation, or resource usage. You +# can use --title to set the title to reflect the content, and --countname +# to change "samples" to "bytes" etc. +# +# There are a few different palettes, selectable using --color. By default, +# the colors are selected at random (except for differentials). Functions +# called "-" will be printed gray, which can be used for stack separators (eg, +# between user and kernel stacks). +# +# HISTORY +# +# This was inspired by Neelakanth Nadgir's excellent function_call_graph.rb +# program, which visualized function entry and return trace events. As Neel +# wrote: "The output displayed is inspired by Roch's CallStackAnalyzer which +# was in turn inspired by the work on vftrace by Jan Boerhout". See: +# https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and +# +# 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. + +use strict; + +use Getopt::Long; + +use open qw(:std :utf8); + +# tunables +my $encoding; +my $fonttype = "Verdana"; +my $imagewidth = 1200; # max width, pixels +my $frameheight = 16; # max height is dynamic +my $fontsize = 12; # base text size +my $fontwidth = 0.59; # avg width relative to fontsize +my $minwidth = 0.1; # min function width, pixels or percentage of time +my $nametype = "Function:"; # what are the names in the data? +my $countname = "samples"; # what are the counts in the data? +my $colors = "hot"; # color theme +my $bgcolors = ""; # background color theme +my $nameattrfile; # file holding function attributes +my $timemax; # (override the) sum of the counts +my $factor = 1; # factor to scale counts by +my $hash = 0; # color by function name +my $rand = 0; # color randomly +my $palette = 0; # if we use consistent palettes (default off) +my %palette_map; # palette map hash +my $pal_file = "palette.map"; # palette map file name +my $stackreverse = 0; # reverse stack order, switching merge end +my $inverted = 0; # icicle graph +my $flamechart = 0; # produce a flame chart (sort by time, do not merge stacks) +my $negate = 0; # switch differential hues +my $titletext = ""; # centered heading +my $titledefault = "Flame Graph"; # overwritten by --title +my $titleinverted = "Icicle Graph"; # " " +my $searchcolor = "rgb(230,0,230)"; # color for search highlighting +my $notestext = ""; # embedded notes in SVG +my $subtitletext = ""; # second level title (optional) +my $help = 0; + +sub usage { + die < outfile.svg\n + --title TEXT # change title text + --subtitle TEXT # second level title (optional) + --width NUM # width of image (default 1200) + --height NUM # height of each frame (default 16) + --minwidth NUM # omit smaller functions. In pixels or use "%" for + # percentage of time (default 0.1 pixels) + --fonttype FONT # font type (default "Verdana") + --fontsize NUM # font size (default 12) + --countname TEXT # count type label (default "samples") + --nametype TEXT # name type label (default "Function:") + --colors PALETTE # set color palette. choices are: hot (default), mem, + # io, wakeup, chain, java, js, perl, red, green, blue, + # aqua, yellow, purple, orange + --bgcolors COLOR # set background colors. gradient choices are yellow + # (default), blue, green, grey; flat colors use "#rrggbb" + --hash # colors are keyed by function name hash + --random # colors are randomly generated + --cp # use consistent palette (palette.map) + --reverse # generate stack-reversed flame graph + --inverted # icicle graph + --flamechart # produce a flame chart (sort by time, do not merge stacks) + --negate # switch differential hues (blue<->red) + --notes TEXT # add notes comment in SVG (for debugging) + --help # this message + + eg, + $0 --title="Flame Graph: malloc()" trace.txt > graph.svg +USAGE_END +} + +GetOptions( + 'fonttype=s' => \$fonttype, + 'width=i' => \$imagewidth, + 'height=i' => \$frameheight, + 'encoding=s' => \$encoding, + 'fontsize=f' => \$fontsize, + 'fontwidth=f' => \$fontwidth, + 'minwidth=s' => \$minwidth, + 'title=s' => \$titletext, + 'subtitle=s' => \$subtitletext, + 'nametype=s' => \$nametype, + 'countname=s' => \$countname, + 'nameattr=s' => \$nameattrfile, + 'total=s' => \$timemax, + 'factor=f' => \$factor, + 'colors=s' => \$colors, + 'bgcolors=s' => \$bgcolors, + 'hash' => \$hash, + 'random' => \$rand, + 'cp' => \$palette, + 'reverse' => \$stackreverse, + 'inverted' => \$inverted, + 'flamechart' => \$flamechart, + 'negate' => \$negate, + 'notes=s' => \$notestext, + 'help' => \$help, +) or usage(); +$help && usage(); + +# internals +my $ypad1 = $fontsize * 3; # pad top, include title +my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels +my $ypad3 = $fontsize * 2; # pad top, include subtitle (optional) +my $xpad = 10; # pad lefm and right +my $framepad = 1; # vertical padding for frames +my $depthmax = 0; +my %Events; +my %nameattr; + +if ($flamechart && $titletext eq "") { + $titletext = "Flame Chart"; +} + +if ($titletext eq "") { + unless ($inverted) { + $titletext = $titledefault; + } else { + $titletext = $titleinverted; + } +} + +if ($nameattrfile) { + # The name-attribute file format is a function name followed by a tab then + # a sequence of tab separated name=value pairs. + open my $attrfh, $nameattrfile or die "Can't read $nameattrfile: $!\n"; + while (<$attrfh>) { + chomp; + my ($funcname, $attrstr) = split /\t/, $_, 2; + die "Invalid format in $nameattrfile" unless defined $attrstr; + $nameattr{$funcname} = { map { split /=/, $_, 2 } split /\t/, $attrstr }; + } +} + +if ($notestext =~ /[<>]/) { + die "Notes string can't contain < or >" +} + +# Ensure minwidth is a valid floating-point number, +# print usage string if not +my $minwidth_f; +if ($minwidth =~ /^([0-9.]+)%?$/) { + $minwidth_f = $1; +} else { + warn "Value '$minwidth' is invalid for minwidth, expected a float.\n"; + usage(); +} + +# background colors: +# - yellow gradient: default (hot, java, js, perl) +# - green gradient: mem +# - blue gradient: io, wakeup, chain +# - gray gradient: flat colors (red, green, blue, ...) +if ($bgcolors eq "") { + # choose a default + if ($colors eq "mem") { + $bgcolors = "green"; + } elsif ($colors =~ /^(io|wakeup|chain)$/) { + $bgcolors = "blue"; + } elsif ($colors =~ /^(red|green|blue|aqua|yellow|purple|orange)$/) { + $bgcolors = "grey"; + } else { + $bgcolors = "yellow"; + } +} +my ($bgcolor1, $bgcolor2); +if ($bgcolors eq "yellow") { + $bgcolor1 = "#eeeeee"; # background color gradient start + $bgcolor2 = "#eeeeb0"; # background color gradient stop +} elsif ($bgcolors eq "blue") { + $bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff"; +} elsif ($bgcolors eq "green") { + $bgcolor1 = "#eef2ee"; $bgcolor2 = "#e0ffe0"; +} elsif ($bgcolors eq "grey") { + $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8"; +} elsif ($bgcolors =~ /^#......$/) { + $bgcolor1 = $bgcolor2 = $bgcolors; +} else { + die "Unrecognized bgcolor option \"$bgcolors\"" +} + +# SVG functions +{ package SVG; + sub new { + my $class = shift; + my $self = {}; + bless ($self, $class); + return $self; + } + + sub header { + my ($self, $w, $h) = @_; + my $enc_attr = ''; + if (defined $encoding) { + $enc_attr = qq{ encoding="$encoding"}; + } + $self->{svg} .= < + + + + +SVG + } + + sub include { + my ($self, $content) = @_; + $self->{svg} .= $content; + } + + sub colorAllocate { + my ($self, $r, $g, $b) = @_; + return "rgb($r,$g,$b)"; + } + + sub group_start { + my ($self, $attr) = @_; + + my @g_attr = map { + exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : () + } qw(id class); + push @g_attr, $attr->{g_extra} if $attr->{g_extra}; + if ($attr->{href}) { + my @a_attr; + push @a_attr, sprintf qq/xlink:href="%s"/, $attr->{href} if $attr->{href}; + # default target=_top else links will open within SVG + push @a_attr, sprintf qq/target="%s"/, $attr->{target} || "_top"; + push @a_attr, $attr->{a_extra} if $attr->{a_extra}; + $self->{svg} .= sprintf qq/\n/, join(' ', (@a_attr, @g_attr)); + } else { + $self->{svg} .= sprintf qq/\n/, join(' ', @g_attr); + } + + $self->{svg} .= sprintf qq/%s<\/title>/, $attr->{title} + if $attr->{title}; # should be first element within g container + } + + sub group_end { + my ($self, $attr) = @_; + $self->{svg} .= $attr->{href} ? qq/<\/a>\n/ : qq/<\/g>\n/; + } + + sub filledRectangle { + my ($self, $x1, $y1, $x2, $y2, $fill, $extra) = @_; + $x1 = sprintf "%0.1f", $x1; + $x2 = sprintf "%0.1f", $x2; + my $w = sprintf "%0.1f", $x2 - $x1; + my $h = sprintf "%0.1f", $y2 - $y1; + $extra = defined $extra ? $extra : ""; + $self->{svg} .= qq/\n/; + } + + sub stringTTF { + my ($self, $id, $x, $y, $str, $extra) = @_; + $x = sprintf "%0.2f", $x; + $id = defined $id ? qq/id="$id"/ : ""; + $extra ||= ""; + $self->{svg} .= qq/$str<\/text>\n/; + } + + sub svg { + my $self = shift; + return "$self->{svg}\n"; + } + 1; +} + +sub namehash { + # Generate a vector hash for the name string, weighting early over + # later characters. We want to pick the same colors for function + # names across different flame graphs. + my $name = shift; + my $vector = 0; + my $weight = 1; + my $max = 1; + my $mod = 10; + # if module name present, trunc to 1st char + $name =~ s/.(.*?)`//; + foreach my $c (split //, $name) { + my $i = (ord $c) % $mod; + $vector += ($i / ($mod++ - 1)) * $weight; + $max += 1 * $weight; + $weight *= 0.70; + last if $mod > 12; + } + return (1 - $vector / $max) +} + +sub sum_namehash { + my $name = shift; + return unpack("%32W*", $name); +} + +sub random_namehash { + # Generate a random hash for the name string. + # This ensures that functions with the same name have the same color, + # both within a flamegraph and across multiple flamegraphs without + # needing to set a palette and while preserving the original flamegraph + # optic, unlike what happens with --hash. + my $name = shift; + my $hash = sum_namehash($name); + srand($hash); + return rand(1) +} + +sub color { + my ($type, $hash, $name) = @_; + my ($v1, $v2, $v3); + + if ($hash) { + $v1 = namehash($name); + $v2 = $v3 = namehash(scalar reverse $name); + } elsif ($rand) { + $v1 = rand(1); + $v2 = rand(1); + $v3 = rand(1); + } else { + $v1 = random_namehash($name); + $v2 = random_namehash($name); + $v3 = random_namehash($name); + } + + # theme palettes + if (defined $type and $type eq "hot") { + my $r = 205 + int(50 * $v3); + my $g = 0 + int(230 * $v1); + my $b = 0 + int(55 * $v2); + return "rgb($r,$g,$b)"; + } + if (defined $type and $type eq "mem") { + my $r = 0; + my $g = 190 + int(50 * $v2); + my $b = 0 + int(210 * $v1); + return "rgb($r,$g,$b)"; + } + if (defined $type and $type eq "io") { + my $r = 80 + int(60 * $v1); + my $g = $r; + my $b = 190 + int(55 * $v2); + return "rgb($r,$g,$b)"; + } + + # multi palettes + if (defined $type and $type eq "java") { + # Handle both annotations (_[j], _[i], ...; which are + # accurate), as well as input that lacks any annotations, as + # best as possible. Without annotations, we get a little hacky + # and match on java|org|com, etc. + if ($name =~ m:_\[j\]$:) { # jit annotation + $type = "green"; + } elsif ($name =~ m:_\[i\]$:) { # inline annotation + $type = "aqua"; + } elsif ($name =~ m:^L?(java|javax|jdk|net|org|com|io|sun)/:) { # Java + $type = "green"; + } elsif ($name =~ /:::/) { # Java, typical perf-map-agent method separator + $type = "green"; + } elsif ($name =~ /::/) { # C++ + $type = "yellow"; + } elsif ($name =~ m:_\[k\]$:) { # kernel annotation + $type = "orange"; + } elsif ($name =~ /::/) { # C++ + $type = "yellow"; + } else { # system + $type = "red"; + } + # fall-through to color palettes + } + if (defined $type and $type eq "perl") { + if ($name =~ /::/) { # C++ + $type = "yellow"; + } elsif ($name =~ m:Perl: or $name =~ m:\.pl:) { # Perl + $type = "green"; + } elsif ($name =~ m:_\[k\]$:) { # kernel + $type = "orange"; + } else { # system + $type = "red"; + } + # fall-through to color palettes + } + if (defined $type and $type eq "js") { + # Handle both annotations (_[j], _[i], ...; which are + # accurate), as well as input that lacks any annotations, as + # best as possible. Without annotations, we get a little hacky, + # and match on a "/" with a ".js", etc. + if ($name =~ m:_\[j\]$:) { # jit annotation + if ($name =~ m:/:) { + $type = "green"; # source + } else { + $type = "aqua"; # builtin + } + } elsif ($name =~ /::/) { # C++ + $type = "yellow"; + } elsif ($name =~ m:/.*\.js:) { # JavaScript (match "/" in path) + $type = "green"; + } elsif ($name =~ m/:/) { # JavaScript (match ":" in builtin) + $type = "aqua"; + } elsif ($name =~ m/^ $/) { # Missing symbol + $type = "green"; + } elsif ($name =~ m:_\[k\]:) { # kernel + $type = "orange"; + } else { # system + $type = "red"; + } + # fall-through to color palettes + } + if (defined $type and $type eq "wakeup") { + $type = "aqua"; + # fall-through to color palettes + } + if (defined $type and $type eq "chain") { + if ($name =~ m:_\[w\]:) { # waker + $type = "aqua" + } else { # off-CPU + $type = "blue"; + } + # fall-through to color palettes + } + + # color palettes + if (defined $type and $type eq "red") { + my $r = 200 + int(55 * $v1); + my $x = 50 + int(80 * $v1); + return "rgb($r,$x,$x)"; + } + if (defined $type and $type eq "green") { + my $g = 200 + int(55 * $v1); + my $x = 50 + int(60 * $v1); + return "rgb($x,$g,$x)"; + } + if (defined $type and $type eq "blue") { + my $b = 205 + int(50 * $v1); + my $x = 80 + int(60 * $v1); + return "rgb($x,$x,$b)"; + } + if (defined $type and $type eq "yellow") { + my $x = 175 + int(55 * $v1); + my $b = 50 + int(20 * $v1); + return "rgb($x,$x,$b)"; + } + if (defined $type and $type eq "purple") { + my $x = 190 + int(65 * $v1); + my $g = 80 + int(60 * $v1); + return "rgb($x,$g,$x)"; + } + if (defined $type and $type eq "aqua") { + my $r = 50 + int(60 * $v1); + my $g = 165 + int(55 * $v1); + my $b = 165 + int(55 * $v1); + return "rgb($r,$g,$b)"; + } + if (defined $type and $type eq "orange") { + my $r = 190 + int(65 * $v1); + my $g = 90 + int(65 * $v1); + return "rgb($r,$g,0)"; + } + + return "rgb(0,0,0)"; +} + +sub color_scale { + my ($value, $max) = @_; + my ($r, $g, $b) = (255, 255, 255); + $value = -$value if $negate; + if ($value > 0) { + $g = $b = int(210 * ($max - $value) / $max); + } elsif ($value < 0) { + $r = $g = int(210 * ($max + $value) / $max); + } + return "rgb($r,$g,$b)"; +} + +sub color_map { + my ($colors, $func) = @_; + if (exists $palette_map{$func}) { + return $palette_map{$func}; + } else { + $palette_map{$func} = color($colors, $hash, $func); + return $palette_map{$func}; + } +} + +sub write_palette { + open(FILE, ">$pal_file"); + foreach my $key (sort keys %palette_map) { + print FILE $key."->".$palette_map{$key}."\n"; + } + close(FILE); +} + +sub read_palette { + if (-e $pal_file) { + open(FILE, $pal_file) or die "can't open file $pal_file: $!"; + while ( my $line = ) { + chomp($line); + (my $key, my $value) = split("->",$line); + $palette_map{$key}=$value; + } + close(FILE) + } +} + +my %Node; # Hash of merged frame data +my %Tmp; + +# flow() merges two stacks, storing the merged frames and value data in %Node. +sub flow { + my ($last, $this, $v, $d) = @_; + + my $len_a = @$last - 1; + my $len_b = @$this - 1; + + my $i = 0; + my $len_same; + for (; $i <= $len_a; $i++) { + last if $i > $len_b; + last if $last->[$i] ne $this->[$i]; + } + $len_same = $i; + + for ($i = $len_a; $i >= $len_same; $i--) { + my $k = "$last->[$i];$i"; + # a unique ID is constructed from "func;depth;etime"; + # func-depth isn't unique, it may be repeated later. + $Node{"$k;$v"}->{stime} = delete $Tmp{$k}->{stime}; + if (defined $Tmp{$k}->{delta}) { + $Node{"$k;$v"}->{delta} = delete $Tmp{$k}->{delta}; + } + delete $Tmp{$k}; + } + + for ($i = $len_same; $i <= $len_b; $i++) { + my $k = "$this->[$i];$i"; + $Tmp{$k}->{stime} = $v; + if (defined $d) { + $Tmp{$k}->{delta} += $i == $len_b ? $d : 0; + } + } + + return $this; +} + +# parse input +my @Data; +my @SortedData; +my $last = []; +my $time = 0; +my $delta = undef; +my $ignored = 0; +my $line; +my $maxdelta = 1; + +# reverse if needed +foreach (<>) { + chomp; + $line = $_; + if ($stackreverse) { + # there may be an extra samples column for differentials + # XXX todo: redo these REs as one. It's repeated below. + my($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); + my $samples2 = undef; + if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { + $samples2 = $samples; + ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); + unshift @Data, join(";", reverse split(";", $stack)) . " $samples $samples2"; + } else { + unshift @Data, join(";", reverse split(";", $stack)) . " $samples"; + } + } else { + unshift @Data, $line; + } +} + +if ($flamechart) { + # In flame chart mode, just reverse the data so time moves from left to right. + @SortedData = reverse @Data; +} else { + @SortedData = sort @Data; +} + +# process and merge frames +foreach (@SortedData) { + chomp; + # process: folded_stack count + # eg: func_a;func_b;func_c 31 + my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); + unless (defined $samples and defined $stack) { + ++$ignored; + next; + } + + # there may be an extra samples column for differentials: + my $samples2 = undef; + if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { + $samples2 = $samples; + ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); + } + $delta = undef; + if (defined $samples2) { + $delta = $samples2 - $samples; + $maxdelta = abs($delta) if abs($delta) > $maxdelta; + } + + # for chain graphs, annotate waker frames with "_[w]", for later + # coloring. This is a hack, but has a precedent ("_[k]" from perf). + if ($colors eq "chain") { + my @parts = split ";--;", $stack; + my @newparts = (); + $stack = shift @parts; + $stack .= ";--;"; + foreach my $part (@parts) { + $part =~ s/;/_[w];/g; + $part .= "_[w]"; + push @newparts, $part; + } + $stack .= join ";--;", @parts; + } + + # merge frames and populate %Node: + $last = flow($last, [ '', split ";", $stack ], $time, $delta); + + if (defined $samples2) { + $time += $samples2; + } else { + $time += $samples; + } +} +flow($last, [], $time, $delta); + +if ($countname eq "samples") { + # If $countname is used, it's likely that we're not measuring in stack samples + # (e.g. time could be the unit), so don't warn. + warn "Stack count is low ($time). Did something go wrong?\n" if $time < 100; +} + +warn "Ignored $ignored lines with invalid format\n" if $ignored; +unless ($time) { + warn "ERROR: No stack counts found\n"; + my $im = SVG->new(); + # emit an error message SVG, for tools automating flamegraph use + my $imageheight = $fontsize * 5; + $im->header($imagewidth, $imageheight); + $im->stringTTF(undef, int($imagewidth / 2), $fontsize * 2, + "ERROR: No valid input provided to flamegraph.pl."); + print $im->svg; + exit 2; +} +if ($timemax and $timemax < $time) { + warn "Specified --total $timemax is less than actual total $time, so ignored\n" + if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc) + undef $timemax; +} +$timemax ||= $time; + +my $widthpertime = ($imagewidth - 2 * $xpad) / $timemax; + +# Treat as a percentage of time if the string ends in a "%". +my $minwidth_time; +if ($minwidth =~ /%$/) { + $minwidth_time = $timemax * $minwidth_f / 100; +} else { + $minwidth_time = $minwidth_f / $widthpertime; +} + +# prune blocks that are too narrow and determine max depth +while (my ($id, $node) = each %Node) { + my ($func, $depth, $etime) = split ";", $id; + my $stime = $node->{stime}; + die "missing start for $id" if not defined $stime; + + if (($etime-$stime) < $minwidth_time) { + delete $Node{$id}; + next; + } + $depthmax = $depth if $depth > $depthmax; +} + +# draw canvas, and embed interactive JavaScript program +my $imageheight = (($depthmax + 1) * $frameheight) + $ypad1 + $ypad2; +$imageheight += $ypad3 if $subtitletext ne ""; +my $titlesize = $fontsize + 5; +my $im = SVG->new(); +my ($black, $vdgrey, $dgrey) = ( + $im->colorAllocate(0, 0, 0), + $im->colorAllocate(160, 160, 160), + $im->colorAllocate(200, 200, 200), + ); +$im->header($imagewidth, $imageheight); +my $inc = < + + + + + + + +INC +$im->include($inc); +$im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)'); +$im->stringTTF("title", int($imagewidth / 2), $fontsize * 2, $titletext); +$im->stringTTF("subtitle", int($imagewidth / 2), $fontsize * 4, $subtitletext) if $subtitletext ne ""; +$im->stringTTF("details", $xpad, $imageheight - ($ypad2 / 2), " "); +$im->stringTTF("unzoom", $xpad, $fontsize * 2, "Reset Zoom", 'class="hide"'); +$im->stringTTF("search", $imagewidth - $xpad - 100, $fontsize * 2, "Search"); +$im->stringTTF("ignorecase", $imagewidth - $xpad - 16, $fontsize * 2, "ic"); +$im->stringTTF("matched", $imagewidth - $xpad - 100, $imageheight - ($ypad2 / 2), " "); + +if ($palette) { + read_palette(); +} + +# draw frames +$im->group_start({id => "frames"}); +while (my ($id, $node) = each %Node) { + my ($func, $depth, $etime) = split ";", $id; + my $stime = $node->{stime}; + my $delta = $node->{delta}; + + $etime = $timemax if $func eq "" and $depth == 0; + + my $x1 = $xpad + $stime * $widthpertime; + my $x2 = $xpad + $etime * $widthpertime; + my ($y1, $y2); + unless ($inverted) { + $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad; + $y2 = $imageheight - $ypad2 - $depth * $frameheight; + } else { + $y1 = $ypad1 + $depth * $frameheight; + $y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad; + } + + # Add commas per perlfaq5: + # https://perldoc.perl.org/perlfaq5#How-can-I-output-my-numbers-with-commas-added? + my $samples = sprintf "%.0f", ($etime - $stime) * $factor; + (my $samples_txt = $samples) + =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g; + + my $info; + if ($func eq "" and $depth == 0) { + $info = "all ($samples_txt $countname, 100%)"; + } else { + my $pct = sprintf "%.2f", ((100 * $samples) / ($timemax * $factor)); + my $escaped_func = $func; + # clean up SVG breaking characters: + $escaped_func =~ s/&/&/g; + $escaped_func =~ s//>/g; + $escaped_func =~ s/"/"/g; + $escaped_func =~ s/_\[[kwij]\]$//; # strip any annotation + unless (defined $delta) { + $info = "$escaped_func ($samples_txt $countname, $pct%)"; + } else { + my $d = $negate ? -$delta : $delta; + my $deltapct = sprintf "%.2f", ((100 * $d) / ($timemax * $factor)); + $deltapct = $d > 0 ? "+$deltapct" : $deltapct; + $info = "$escaped_func ($samples_txt $countname, $pct%; $deltapct%)"; + } + } + + my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone + $nameattr->{title} ||= $info; + $im->group_start($nameattr); + + my $color; + if ($func eq "--") { + $color = $vdgrey; + } elsif ($func eq "-") { + $color = $dgrey; + } elsif (defined $delta) { + $color = color_scale($delta, $maxdelta); + } elsif ($palette) { + $color = color_map($colors, $func); + } else { + $color = color($colors, $hash, $func); + } + $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"'); + + my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth)); + my $text = ""; + if ($chars >= 3) { # room for one char plus two dots + $func =~ s/_\[[kwij]\]$//; # strip any annotation + $text = substr $func, 0, $chars; + substr($text, -2, 2) = ".." if $chars < length $func; + $text =~ s/&/&/g; + $text =~ s//>/g; + } + $im->stringTTF(undef, $x1 + 3, 3 + ($y1 + $y2) / 2, $text); + + $im->group_end($nameattr); +} +$im->group_end(); + +print $im->svg; + +if ($palette) { + write_palette(); +} + +# vim: ts=8 sts=8 sw=8 noexpandtab diff --git a/generateStackFlame.sh b/generateStackFlame.sh new file mode 100755 index 0000000..2b11c1a --- /dev/null +++ b/generateStackFlame.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# 기본값 설정 +n_flag=20 +i_flag=0 +process_args=() + +# 입력값 파싱 +while [[ $# -gt 0 ]]; do + case "$1" in + -n) + shift + if [[ ! "$1" =~ ^[0-9]+$ ]]; then + echo "오류: -n 옵션 뒤에는 숫자가 와야 합니다." >&2 + exit 1 + fi + n_flag="$1" + ;; + -i) + shift + if [[ ! "$1" =~ ^[0-9]+$ ]]; then + echo "오류: -i 옵션 뒤에는 숫자가 와야 합니다." >&2 + exit 1 + fi + i_flag="$1" + ;; + *) + # 프로세스 명 또는 PID 검증 + if [[ "$1" =~ ^[0-9]+$ ]]; then + # 숫자인 경우, PID 존재 여부 확인 + if ! ps -p "$1" > /dev/null 2>&1; then + echo "오류: PID $1 에 해당하는 프로세스가 존재하지 않습니다." >&2 + exit 1 + fi + else + # 문자열인 경우, 프로세스 이름 존재 여부 확인 + if ! pgrep -x "$1" > /dev/null 2>&1; then + echo "오류: 프로세스 '$1' 가 실행 중이지 않습니다." >&2 + exit 1 + fi + fi + process_args+=("$1") + ;; + esac + shift +done + +# 프로세스 인자 검증 +if [[ ${#process_args[@]} -eq 0 ]]; then + echo "오류: 최소한 하나 이상의 프로세스 이름 또는 PID를 입력해야 합니다." >&2 + exit 1 +fi + +pid=$(pidof $process_args) + +for x in $(seq 1 $n_flag) + do + sudo gdb -ex "set pagination 0" \ + -ex "thread apply all bt" \ + -batch -p $pid >> out.gdb 2> /dev/null + sleep $i_flag + done + +./stackcollapse-gdb.pl out.gdb > out.folded +rm out.gdb +./flamegraph.pl out.folded > stackflame.svg +rm out.folded \ No newline at end of file diff --git a/include/Utils/ConsoleUtils.hpp b/include/Utils/ConsoleUtils.hpp index 43434c0..65c161f 100644 --- a/include/Utils/ConsoleUtils.hpp +++ b/include/Utils/ConsoleUtils.hpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include namespace bumbleBee { class ConsoleUtils { @@ -9,28 +11,36 @@ public: /** * @brief 명령어를 쉘에서 실행하고 결과를 EOF 전까지 읽어 \n을 구분자로 토큰화하여 반환합니다 * @param cmd 실행할 명령 + * @param args 아규먼트 * @return std::queue tokens */ - static std::queue getResultFromCommand(std::string cmd) { - std::string result, token; + static std::queue safe_execute_command(const std::string& cmd, const std::vector& args) { std::queue tokens; - FILE* stream; - const int maxBuffer = 12; // 적당한 크기 - char buffer[maxBuffer]; - cmd.append(" 2>&1"); // 표준에러를 표준출력으로 redirect + try { + boost::process::ipstream output; // 명령의 출력을 받을 스트림 + boost::process::child c(cmd, boost::process::args(args), boost::process::std_out > output); - stream = popen(cmd.c_str(), "r"); // 주어진 command를 shell로 실행하고 파이프 연결 (fd 반환) - if (stream) { - while (fgets(buffer, maxBuffer, stream) != NULL) result.append(buffer); // fgets: fd (stream)를 길이 (maxBuffer)만큼 읽어 버퍼 (buffer)에 저장 - pclose(stream); - } + std::string line; + while (!output.eof() && std::getline(output, line)) + tokens.push(line); - std::stringstream ss(result); - while (std::getline(ss, token, '\n')) { - tokens.push(token); + c.wait(); // 프로세스가 종료될 때까지 대기 + return tokens; + } catch (const std::exception& e) { + return tokens; } + } + /** + * @brief 명령어를 쉘에서 실행하고 결과를 파이프로 연결하여 반환합니다 + * @param cmd 실행할 명령 + * @param args 아규먼트 + * @return FILE* fd + */ + static FILE* safe_open_pipe(const std::string& cmd, const std::vector& args) { + boost::process::pipe pipe; + boost::process::child c(cmd, boost::process::args(args), boost::process::std_out > pipe); - return tokens; + return fdopen(pipe.native_source(), "r"); } }; } \ No newline at end of file diff --git a/include/Utils/VersionsCheckUtils.hpp b/include/Utils/VersionsCheckUtils.hpp index c6bd973..2be15fa 100644 --- a/include/Utils/VersionsCheckUtils.hpp +++ b/include/Utils/VersionsCheckUtils.hpp @@ -7,7 +7,7 @@ namespace bumbleBee { class VersionsCheckUtils { public: static bool isThereCMD(std::shared_ptr cluster, std::string cmd) { - if (ConsoleUtils::getResultFromCommand("which " + cmd).size() == 0) { + if (ConsoleUtils::safe_execute_command("which", {cmd}).size() == 0) { cluster->log(dpp::ll_error, cmd + " is unavaliable. unresolable error please install " + cmd); return false; } @@ -16,7 +16,7 @@ public: } static void validateFFMPEG(std::shared_ptr cluster) { - std::queue result = ConsoleUtils::getResultFromCommand(SettingsManager::getFFMPEG_CMD() + " -version"); + std::queue result = ConsoleUtils::safe_execute_command(SettingsManager::getFFMPEG_CMD(), {"-version"}); std::string front = result.front(); if (front[0] != 'f' || front[1] != 'f' || @@ -43,7 +43,7 @@ public: } static void validateYTDLP(std::shared_ptr cluster) { - std::queue result = ConsoleUtils::getResultFromCommand(SettingsManager::getYTDLP_CMD() + " --version"); + std::queue result = ConsoleUtils::safe_execute_command(SettingsManager::getYTDLP_CMD(), {"--version"}); std::string front = result.front(); if ((front[0]-'0' < 0 || front[0]-'0' > 9) || (front[1]-'0' < 0 || front[1]-'0' > 9) || @@ -72,7 +72,7 @@ public: static void updateytdlp(std::shared_ptr cluster) { cluster->log(dpp::ll_info, "Checking if yt-dlp update is available..."); - std::queue result = ConsoleUtils::getResultFromCommand("./yt-dlp -U"); + std::queue result = ConsoleUtils::safe_execute_command("./yt-dlp", {"-U"}); while(!result.empty()) { std::string front = result.front(); result.pop(); diff --git a/src/Commands/Play.cpp b/src/Commands/Play.cpp index 2eb04eb..19198ce 100644 --- a/src/Commands/Play.cpp +++ b/src/Commands/Play.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace bumbleBee::commands { @@ -23,12 +24,20 @@ namespace bumbleBee::commands { return; } std::string query = std::get(event.get_parameter("query")); - query = "\"" + query + "\""; + // query = "\"" + query + "\""; std::queue ids = - ConsoleUtils::getResultFromCommand( - SettingsManager::getYTDLP_CMD() + - " --default-search ytsearch --flat-playlist --skip-download --quiet --ignore-errors --print id " + query); + ConsoleUtils::safe_execute_command( + SettingsManager::getYTDLP_CMD(), { + "--default-search", + "ytsearch", + "--flat-playlist", + "--skip-download", + "--quiet", + "--ignore-errors", + "--print", + "id", + query}); std::queue> musics; @@ -46,19 +55,18 @@ namespace bumbleBee::commands { ids.pop(); } - FILE* file = popen((SettingsManager::getYTDLP_CMD() + - " --default-search ytsearch --flat-playlist --skip-download --quiet --ignore-errors -J http://youtu.be/" + ids.front()).c_str(), "r"); - - std::ostringstream oss; - char buffer[1024]; - size_t bytesRead; - - while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) { - oss.write(buffer, bytesRead); - } - pclose(file); + std::string jsonData = ConsoleUtils::safe_execute_command(SettingsManager::getYTDLP_CMD(), { + "--default-search", + "ytsearch", + "--flat-playlist", + "--skip-download", + "--quiet", + "--ignore-errors", + "-J", + "http://youtu.be/" + ids.front() + }).front(); - std::istringstream iss(oss.str()); + std::istringstream iss(jsonData); nlohmann::json videoDataJson; iss >> videoDataJson; @@ -108,19 +116,18 @@ namespace bumbleBee::commands { continue; } - FILE* file = popen((SettingsManager::getYTDLP_CMD() + - " --default-search ytsearch --flat-playlist --skip-download --quiet --ignore-errors -J http://youtu.be/" + ids.front()).c_str(), "r"); - - std::ostringstream oss; - char buffer[1024]; - size_t bytesRead; - - while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) { - oss.write(buffer, bytesRead); - } - pclose(file); - - std::istringstream iss(oss.str()); + std::string jsonData = ConsoleUtils::safe_execute_command(SettingsManager::getYTDLP_CMD(), { + "--default-search", + "ytsearch", + "--flat-playlist", + "--skip-download", + "--quiet", + "--ignore-errors", + "-J", + "http://youtu.be/" + ids.front() + }).front(); + + std::istringstream iss(jsonData); nlohmann::json videoDataJson; iss >> videoDataJson; diff --git a/src/Settings/SettingsManager.cpp b/src/Settings/SettingsManager.cpp index e26f936..073b37f 100644 --- a/src/Settings/SettingsManager.cpp +++ b/src/Settings/SettingsManager.cpp @@ -14,13 +14,19 @@ bool SettingsManager::REGISTER_COMMAND = false; bool SettingsManager::validateToken() { nlohmann::json response; - if (ConsoleUtils::getResultFromCommand("which curl").size() == 0) { + std::string curl = ConsoleUtils::safe_execute_command("/usr/bin/which", {"curl"}).front(); + if (curl == "") { std::cout << "curl is unavaliable. unresolable error please install curl." << std::endl; return false; } - std::string stresult = ConsoleUtils::getResultFromCommand("curl -sX GET \"https://discord.com/api/v10/users/@me\" -H \"Authorization: Bot " + - TOKEN + "\"").front(); + std::string stresult = ConsoleUtils::safe_execute_command(curl, { + "-sX", + "GET", + "https://discord.com/api/v10/users/@me", + "-H", + "Authorization: Bot " + TOKEN + "" + }).front(); std::stringstream ss(stresult); ss >> response; diff --git a/src/Utils/AsyncDownloadManager.cpp b/src/Utils/AsyncDownloadManager.cpp index 056eff2..530aa32 100644 --- a/src/Utils/AsyncDownloadManager.cpp +++ b/src/Utils/AsyncDownloadManager.cpp @@ -38,8 +38,17 @@ void AsyncDownloadManager::downloadWorker() { cluster->log(dpp::ll_info, "AsyncDownloadManager: " + query + " accepted."); std::queue ids = - ConsoleUtils::getResultFromCommand(SettingsManager::getYTDLP_CMD() + - " --default-search ytsearch --flat-playlist --skip-download --quiet --ignore-errors --print id " + query); + ConsoleUtils::safe_execute_command( + SettingsManager::getYTDLP_CMD(), { + "--default-search", + "ytsearch", + "--flat-playlist", + "--skip-download", + "--quiet", + "--ignore-errors", + "--print", + "id", + query}); if (ids.size() >= 2) { cluster->log(dpp::ll_info, query + " is playlist"); @@ -56,8 +65,13 @@ void AsyncDownloadManager::downloadWorker() { } std::queue urls = - ConsoleUtils::getResultFromCommand(SettingsManager::getYTDLP_CMD() + - " -f ba* --print urls https://youtu.be/" + ids.front()); + ConsoleUtils::safe_execute_command(SettingsManager::getYTDLP_CMD(), { + "-f", + "ba*", + "--print", + "urls", + "https://youtu.be/" + ids.front() + }); cluster->log(dpp::ll_debug, "url: " + urls.front()); @@ -75,8 +89,11 @@ void AsyncDownloadManager::downloadWorker() { cluster->log(dpp::ll_info, "Thread id: " + tid.str() + ": " + downloadID + " accepted."); - std::string command = std::string("./streamOpus.sh " + SettingsManager::getYTDLP_CMD() + " " + downloadID + " " + SettingsManager::getFFMPEG_CMD()); - stream = popen(command.c_str(), "r"); + stream = ConsoleUtils::safe_open_pipe("./streamOpus.sh", { + SettingsManager::getYTDLP_CMD(), + downloadID, + SettingsManager::getFFMPEG_CMD() + }); cluster->log(dpp::ll_info, "Thread id: " + tid.str() + " Opened stream: " + downloadID); }); diff --git a/stackcollapse-gdb.pl b/stackcollapse-gdb.pl new file mode 100755 index 0000000..8e9831b --- /dev/null +++ b/stackcollapse-gdb.pl @@ -0,0 +1,72 @@ +#!/usr/bin/perl -ws +# +# stackcollapse-gdb Collapse GDB backtraces +# +# Parse a list of GDB backtraces as generated with the poor man's +# profiler [1]: +# +# for x in $(seq 1 500); do +# gdb -ex "set pagination 0" -ex "thread apply all bt" -batch -p $pid 2> /dev/null +# sleep 0.01 +# done +# +# [1] http://poormansprofiler.org/ +# +# Copyright 2014 Gabriel Corona. 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 + +use strict; + +my $current = ""; +my $previous_function = ""; + +my %stacks; + +while(<>) { + chomp; + if (m/^Thread/) { + $current="" + } + elsif(m/^#[0-9]* *([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)/) { + my $function = $3; + my $alt = $1; + if(not($1 =~ /0x[a-zA-Z0-9]*/)) { + $function = $alt; + } + if ($current eq "") { + $current = $function; + } else { + $current = $function . ";" . $current; + } + } elsif(!($current eq "")) { + $stacks{$current} += 1; + $current = ""; + } +} + +if(!($current eq "")) { + $stacks{$current} += 1; + $current = ""; +} + +foreach my $k (sort { $a cmp $b } keys %stacks) { + print "$k $stacks{$k}\n"; +} diff --git a/stackcollapse-perf.pl b/stackcollapse-perf.pl new file mode 100755 index 0000000..3ff39bf --- /dev/null +++ b/stackcollapse-perf.pl @@ -0,0 +1,435 @@ +#!/usr/bin/perl -w +# +# stackcollapse-perf.pl collapse perf samples into single lines. +# +# Parses a list of multiline stacks generated by "perf script", and +# outputs a semicolon separated stack followed by a space and a count. +# If memory addresses (+0xd) are present, they are stripped, and resulting +# identical stacks are colased with their counts summed. +# +# USAGE: ./stackcollapse-perf.pl [options] infile > outfile +# +# Run "./stackcollapse-perf.pl -h" to list options. +# +# Example input: +# +# swapper 0 [000] 158665.570607: cpu-clock: +# ffffffff8103ce3b native_safe_halt ([kernel.kallsyms]) +# ffffffff8101c6a3 default_idle ([kernel.kallsyms]) +# ffffffff81013236 cpu_idle ([kernel.kallsyms]) +# ffffffff815bf03e rest_init ([kernel.kallsyms]) +# ffffffff81aebbfe start_kernel ([kernel.kallsyms].init.text) +# [...] +# +# Example output: +# +# swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 1 +# +# Input may be created and processed using: +# +# perf record -a -g -F 997 sleep 60 +# perf script | ./stackcollapse-perf.pl > out.stacks-folded +# +# The output of "perf script" should include stack traces. If these are missing +# for you, try manually selecting the perf script output; eg: +# +# perf script -f comm,pid,tid,cpu,time,event,ip,sym,dso,trace | ... +# +# This is also required for the --pid or --tid options, so that the output has +# both the PID and TID. +# +# Copyright 2012 Joyent, Inc. All rights reserved. +# Copyright 2012 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 +# +# 02-Mar-2012 Brendan Gregg Created this. +# 02-Jul-2014 " " Added process name to stacks. + +use strict; +use Getopt::Long; + +my %collapsed; + +sub remember_stack { + my ($stack, $count) = @_; + $collapsed{$stack} += $count; +} +my $annotate_kernel = 0; # put an annotation on kernel function +my $annotate_jit = 0; # put an annotation on jit symbols +my $annotate_all = 0; # enale all annotations +my $include_pname = 1; # include process names in stacks +my $include_pid = 0; # include process ID with process name +my $include_tid = 0; # include process & thread ID with process name +my $include_addrs = 0; # include raw address where a symbol can't be found +my $tidy_java = 1; # condense Java signatures +my $tidy_generic = 1; # clean up function names a little +my $target_pname; # target process name from perf invocation +my $event_filter = ""; # event type filter, defaults to first encountered event +my $event_defaulted = 0; # whether we defaulted to an event (none provided) +my $event_warning = 0; # if we printed a warning for the event + +my $show_inline = 0; +my $show_context = 0; + +my $srcline_in_input = 0; # if there are extra lines with source location (perf script -F+srcline) +GetOptions('inline' => \$show_inline, + 'context' => \$show_context, + 'srcline' => \$srcline_in_input, + 'pid' => \$include_pid, + 'kernel' => \$annotate_kernel, + 'jit' => \$annotate_jit, + 'all' => \$annotate_all, + 'tid' => \$include_tid, + 'addrs' => \$include_addrs, + 'event-filter=s' => \$event_filter) +or die < outfile\n + --pid # include PID with process names [1] + --tid # include TID and PID with process names [1] + --inline # un-inline using addr2line + --all # all annotations (--kernel --jit) + --kernel # annotate kernel functions with a _[k] + --jit # annotate jit functions with a _[j] + --context # adds source context to --inline + --srcline # parses output of 'perf script -F+srcline' and adds source context + --addrs # include raw addresses where symbols can't be found + --event-filter=EVENT # event name filter\n +[1] perf script must emit both PID and TIDs for these to work; eg, Linux < 4.1: + perf script -f comm,pid,tid,cpu,time,event,ip,sym,dso,trace + for Linux >= 4.1: + perf script -F comm,pid,tid,cpu,time,event,ip,sym,dso,trace + If you save this output add --header on Linux >= 3.14 to include perf info. +USAGE_END + +if ($annotate_all) { + $annotate_kernel = $annotate_jit = 1; +} + +my %inlineCache; + +my %nmCache; + +sub inlineCacheAdd { + my ($pc, $mod, $result) = @_; + if (defined($inlineCache{$pc})) { + $inlineCache{$pc}{$mod} = $result; + } else { + $inlineCache{$pc} = {$mod => $result}; + } +} + +# for the --inline option +sub inline { + my ($pc, $rawfunc, $mod) = @_; + + return $inlineCache{$pc}{$mod} if defined($inlineCache{$pc}{$mod}); + + # capture addr2line output + my $a2l_output = `addr2line -a $pc -e $mod -i -f -s -C`; + + # remove first line + $a2l_output =~ s/^(.*\n){1}//; + + if ($a2l_output =~ /\?\?\n\?\?:0/) { + # if addr2line fails and rawfunc is func+offset, then fall back to it + if ($rawfunc =~ /^(.+)\+0x([0-9a-f]+)$/) { + my $func = $1; + my $addr = hex $2; + + $nmCache{$mod}=`nm $mod` unless defined $nmCache{$mod}; + + if ($nmCache{$mod} =~ /^([0-9a-f]+) . \Q$func\E$/m) { + my $base = hex $1; + my $newPc = sprintf "0x%x", $base+$addr; + my $result = inline($newPc, '', $mod); + inlineCacheAdd($pc, $mod, $result); + return $result; + } + } + } + + my @fullfunc; + my $one_item = ""; + for (split /^/, $a2l_output) { + chomp $_; + + # remove discriminator info if exists + $_ =~ s/ \(discriminator \S+\)//; + + if ($one_item eq "") { + $one_item = $_; + } else { + if ($show_context == 1) { + unshift @fullfunc, $one_item . ":$_"; + } else { + unshift @fullfunc, $one_item; + } + $one_item = ""; + } + } + + my $result = join ";" , @fullfunc; + + inlineCacheAdd($pc, $mod, $result); + + return $result; +} + +my @stack; +my $pname; +my $m_pid; +my $m_tid; +my $m_period; + +# +# Main loop +# +while (defined($_ = <>)) { + + # find the name of the process launched by perf, by stepping backwards + # over the args to find the first non-option (no dash): + if (/^# cmdline/) { + my @args = split ' ', $_; + foreach my $arg (reverse @args) { + if ($arg !~ /^-/) { + $target_pname = $arg; + $target_pname =~ s:.*/::; # strip pathname + last; + } + } + } + + # skip remaining comments + next if m/^#/; + chomp; + + # end of stack. save cached data. + if (m/^$/) { + # ignore filtered samples + next if not $pname; + + if ($include_pname) { + if (defined $pname) { + unshift @stack, $pname; + } else { + unshift @stack, ""; + } + } + remember_stack(join(";", @stack), $m_period) if @stack; + undef @stack; + undef $pname; + next; + } + + # + # event record start + # + if (/^(\S.+?)\s+(\d+)\/*(\d+)*\s+/) { + # default "perf script" output has TID but not PID + # eg, "java 25607 4794564.109216: 1 cycles:" + # eg, "java 12688 [002] 6544038.708352: 235 cpu-clock:" + # eg, "V8 WorkerThread 25607 4794564.109216: 104345 cycles:" + # eg, "java 24636/25607 [000] 4794564.109216: 1 cycles:" + # eg, "java 12688/12764 6544038.708352: 10309278 cpu-clock:" + # eg, "V8 WorkerThread 24636/25607 [000] 94564.109216: 100 cycles:" + # other combinations possible + my ($comm, $pid, $tid, $period) = ($1, $2, $3, ""); + if (not $tid) { + $tid = $pid; + $pid = "?"; + } + + if (/:\s*(\d+)*\s+(\S+):\s*$/) { + $period = $1; + my $event = $2; + + if ($event_filter eq "") { + # By default only show events of the first encountered + # event type. Merging together different types, such as + # instructions and cycles, produces misleading results. + $event_filter = $event; + $event_defaulted = 1; + } elsif ($event ne $event_filter) { + if ($event_defaulted and $event_warning == 0) { + # only print this warning if necessary: + # when we defaulted and there was + # multiple event types. + print STDERR "Filtering for events of type: $event\n"; + $event_warning = 1; + } + next; + } + } + + if (not $period) { + $period = 1 + } + ($m_pid, $m_tid, $m_period) = ($pid, $tid, $period); + + if ($include_tid) { + $pname = "$comm-$m_pid/$m_tid"; + } elsif ($include_pid) { + $pname = "$comm-$m_pid"; + } else { + $pname = "$comm"; + } + $pname =~ tr/ /_/; + + # + # stack line + # + } elsif (/^\s*(\w+)\s*(.+) \((.*)\)/) { + # ignore filtered samples + next if not $pname; + + my ($pc, $rawfunc, $mod) = ($1, $2, $3); + + if ($show_inline == 1 && $mod !~ m/(perf-\d+.map|kernel\.|\[[^\]]+\])/) { + my $inlineRes = inline($pc, $rawfunc, $mod); + # - empty result this happens e.g., when $mod does not exist or is a path to a compressed kernel module + # if this happens, the user will see error message from addr2line written to stderr + # - if addr2line results in "??" , then it's much more sane to fall back than produce a '??' in graph + if($inlineRes ne "" and $inlineRes ne "??" and $inlineRes ne "??:??:0" ) { + unshift @stack, $inlineRes; + next; + } + } + + # Linux 4.8 included symbol offsets in perf script output by default, eg: + # 7fffb84c9afc cpu_startup_entry+0x800047c022ec ([kernel.kallsyms]) + # strip these off: + $rawfunc =~ s/\+0x[\da-f]+$//; + + next if $rawfunc =~ /^\(/; # skip process names + + my $is_unknown=0; + my @inline; + for (split /\->/, $rawfunc) { + my $func = $_; + + if ($func eq "[unknown]") { + if ($mod ne "[unknown]") { # use module name instead, if known + $func = $mod; + $func =~ s/.*\///; + } else { + $func = "unknown"; + $is_unknown=1; + } + + if ($include_addrs) { + $func = "\[$func \<$pc\>\]"; + } else { + $func = "\[$func\]"; + } + } + + if ($tidy_generic) { + $func =~ s/;/:/g; + if ($func !~ m/\.\(.*\)\./) { + # This doesn't look like a Go method name (such as + # "net/http.(*Client).Do"), so everything after the first open + # paren (that is not part of an "(anonymous namespace)") is + # just noise. + $func =~ s/\((?!anonymous namespace\)).*//; + } + # now tidy this horrible thing: + # 13a80b608e0a RegExp:[&<>\"\'] (/tmp/perf-7539.map) + $func =~ tr/"\'//d; + # fall through to $tidy_java + } + + if ($tidy_java and $pname =~ m/^java/) { + # along with $tidy_generic, converts the following: + # Lorg/mozilla/javascript/ContextFactory;.call(Lorg/mozilla/javascript/ContextAction;)Ljava/lang/Object; + # Lorg/mozilla/javascript/ContextFactory;.call(Lorg/mozilla/javascript/C + # Lorg/mozilla/javascript/MemberBox;.(Ljava/lang/reflect/Method;)V + # into: + # org/mozilla/javascript/ContextFactory:.call + # org/mozilla/javascript/ContextFactory:.call + # org/mozilla/javascript/MemberBox:.init + $func =~ s/^L// if $func =~ m:/:; + } + + # + # Annotations + # + # detect inlined from the @inline array + # detect kernel from the module name; eg, frames to parse include: + # ffffffff8103ce3b native_safe_halt ([kernel.kallsyms]) + # 8c3453 tcp_sendmsg (/lib/modules/4.3.0-rc1-virtual/build/vmlinux) + # 7d8 ipv4_conntrack_local+0x7f8f80b8 ([nf_conntrack_ipv4]) + # detect jit from the module name; eg: + # 7f722d142778 Ljava/io/PrintStream;::print (/tmp/perf-19982.map) + if (scalar(@inline) > 0) { + $func .= "_[i]" unless $func =~ m/\_\[i\]/; # inlined + } elsif ($annotate_kernel == 1 && $mod =~ m/(^\[|vmlinux$)/ && $mod !~ /unknown/) { + $func .= "_[k]"; # kernel + } elsif ($annotate_jit == 1 && $mod =~ m:/tmp/perf-\d+\.map:) { + $func .= "_[j]" unless $func =~ m/\_\[j\]/; # jitted + } + + # + # Source lines + # + # + # Sample outputs: + # | a.out 35081 252436.005167: 667783 cycles: + # | 408ebb some_method_name+0x8b (/full/path/to/a.out) + # | uniform_int_dist.h:300 + # | 4069f5 main+0x935 (/full/path/to/a.out) + # | file.cpp:137 + # | 7f6d2148eb25 __libc_start_main+0xd5 (/lib64/libc-2.33.so) + # | libc-2.33.so[27b25] + # + # | a.out 35081 252435.738165: 306459 cycles: + # | 7f6d213c2750 [unknown] (/usr/lib64/libkmod.so.2.3.6) + # | libkmod.so.2.3.6[6750] + # + # | a.out 35081 252435.738373: 315813 cycles: + # | 7f6d215ca51b __strlen_avx2+0x4b (/lib64/libc-2.33.so) + # | libc-2.33.so[16351b] + # | 7ffc71ee9580 [unknown] ([unknown]) + # | + # + # | a.out 35081 252435.718940: 247984 cycles: + # | ffffffff814f9302 up_write+0x32 ([kernel.kallsyms]) + # | [kernel.kallsyms][ffffffff814f9302] + if($srcline_in_input and not $is_unknown){ + $_ = <>; + chomp; + s/\[.*?\]//g; + s/^\s*//g; + s/\s*$//g; + $func.=':'.$_ unless $_ eq ""; + } + + push @inline, $func; + } + + unshift @stack, @inline; + } else { + warn "Unrecognized line: $_"; + } +} + +foreach my $k (sort { $a cmp $b } keys %collapsed) { + print "$k $collapsed{$k}\n"; +} diff --git a/stackflame.svg b/stackflame.svg new file mode 100644 index 0000000..414e43f --- /dev/null +++ b/stackflame.svg @@ -0,0 +1,862 @@ + + + + + + + + + + + + + + +Flame Graph + +Reset Zoom +Search +ic + + + +_IO_new_file_underflow (2 samples, 0.52%) + + + +dpp::ssl_client::read_loop() (4 samples, 1.04%) + + + +lll_mutex_lock_optimized (1 samples, 0.26%) + + + +dpp::https_client::connect() (4 samples, 1.04%) + + + +___pthread_cond_wait (20 samples, 5.21%) +___pth.. + + +___pthread_mutex_lock (1 samples, 0.26%) + + + +all (384 samples, 100%) + + + +__GI___underflow (2 samples, 0.52%) + + + +nlohmann::json_abi_v3_11_2::detail::parser<nlohmann::json_abi_v3_11_2::basic_json<std::map, (1 samples, 0.26%) + + + +__futex_abstimed_wait_common (256 samples, 66.67%) +__futex_abstimed_wait_common + + +___pthread_cond_clockwait64 (40 samples, 10.42%) +___pthread_cond.. + + +?? (2 samples, 0.52%) + + + +__GI___libc_free (1 samples, 0.26%) + + + +_IO_new_file_underflow (3 samples, 0.78%) + + + +std::thread::_Invoker<std::tuple<bumbleBee::MusicPlayManager::play(dpp::discord_voice_client*)::<lambda(dpp::discord_voice_client*)>, (13 samples, 3.39%) +std.. + + +__GI__IO_fread (3 samples, 0.78%) + + + +bumbleBee::MusicPlayManager::remove (1 samples, 0.26%) + + + +__GI__IO_default_xsgetn (3 samples, 0.78%) + + + +___pthread_cond_clockwait64 (40 samples, 10.42%) +___pthread_cond.. + + +__GI__IO_getline (4 samples, 1.04%) + + + +_int_free (1 samples, 0.26%) + + + +__GI___libc_read (2 samples, 0.52%) + + + +dpp::in_thread::in_loop(unsigned (260 samples, 67.71%) +dpp::in_thread::in_loop(unsigned + + +bumbleBee::BumbleBee::start (20 samples, 5.21%) +bumble.. + + +__futex_abstimed_wait_common64 (13 samples, 3.39%) +__f.. + + +dpp::discord_voice_client::write_ready() (4 samples, 1.04%) + + + +operator() (2 samples, 0.52%) + + + +__pthread_cond_wait_common (40 samples, 10.42%) +__pthread_cond_.. + + +__GI__IO_default_xsgetn (2 samples, 0.52%) + + + +nlohmann::json_abi_v3_11_2::operator>> (1 samples, 0.26%) + + + +__futex_abstimed_wait_common (40 samples, 10.42%) +__futex_abstime.. + + +___pthread_cond_wait (13 samples, 3.39%) +___.. + + +std::__invoke_impl<void, (13 samples, 3.39%) +std.. + + +main (20 samples, 5.21%) +main + + +std::thread::_Invoker<std::tuple<bumbleBee::BumbleBee::on_slashcommand(const (9 samples, 2.34%) +s.. + + +std::__invoke<bumbleBee::MusicPlayManager::send_audio_to_voice(std::shared_ptr<bumbleBee::MusicQueueElement>, (2 samples, 0.52%) + + + +dpp::request_queue::out_loop() (40 samples, 10.42%) +dpp::request_qu.. + + +std::thread::_Invoker<std::tuple<bumbleBee::MusicPlayManager::send_audio_to_voice(std::shared_ptr<bumbleBee::MusicQueueElement>, (2 samples, 0.52%) + + + +std::__invoke_impl<void, (9 samples, 2.34%) +s.. + + +std::thread::_Invoker<std::tuple<bumbleBee::BumbleBee::on_slashcommand(const (9 samples, 2.34%) +s.. + + +std::thread::_State_impl<std::thread::_Invoker<std::tuple<bumbleBee::MusicPlayManager::play(dpp::discord_voice_client*)::<lambda(dpp::discord_voice_client*)>, (13 samples, 3.39%) +std.. + + +bumbleBee::commands::Play::execute (8 samples, 2.08%) +b.. + + +__GI___poll (20 samples, 5.21%) +__GI__.. + + +_IO_fgets (4 samples, 1.04%) + + + +std::__invoke<bumbleBee::MusicPlayManager::play(dpp::discord_voice_client*)::<lambda(dpp::discord_voice_client*)>, (13 samples, 3.39%) +std.. + + +std::thread::_State_impl<std::thread::_Invoker<std::tuple<bumbleBee::MusicPlayManager::send_audio_to_voice(std::shared_ptr<bumbleBee::MusicQueueElement>, (2 samples, 0.52%) + + + +__GI___libc_read (2 samples, 0.52%) + + + +___pthread_cond_clockwait64 (256 samples, 66.67%) +___pthread_cond_clockwait64 + + +operator() (9 samples, 2.34%) +o.. + + +__GI___clock_nanosleep (1 samples, 0.26%) + + + +__futex_abstimed_wait_common (13 samples, 3.39%) +__f.. + + +__GI___poll (4 samples, 1.04%) + + + +__pthread_cond_wait_common (20 samples, 5.21%) +__pthr.. + + +nlohmann::json_abi_v3_11_2::detail::lexer<nlohmann::json_abi_v3_11_2::basic_json<std::map, (1 samples, 0.26%) + + + +nlohmann::json_abi_v3_11_2::detail::parser<nlohmann::json_abi_v3_11_2::basic_json<std::map, (1 samples, 0.26%) + + + +dpp::discord_voice_client::thread_run() (20 samples, 5.21%) +dpp::d.. + + +dpp::discord_client::thread_run() (20 samples, 5.21%) +dpp::d.. + + +nlohmann::json_abi_v3_11_2::detail::lexer<nlohmann::json_abi_v3_11_2::basic_json<std::map, (1 samples, 0.26%) + + + +__GI___poll (15 samples, 3.91%) +__GI.. + + +__GI___underflow (3 samples, 0.78%) + + + +std::thread::_State_impl<std::thread::_Invoker<std::tuple<bumbleBee::BumbleBee::on_slashcommand(const (9 samples, 2.34%) +s.. + + +__GI___futex_abstimed_wait_cancelable64 (13 samples, 3.39%) +__G.. + + +___pthread_cond_clockwait64 (256 samples, 66.67%) +___pthread_cond_clockwait64 + + +dpp::https_client::https_client(std::__cxx11::basic_string<char, (4 samples, 1.04%) + + + +nlohmann::json_abi_v3_11_2::detail::lexer<nlohmann::json_abi_v3_11_2::basic_json<std::map, (1 samples, 0.26%) + + + +dpp::ssl_client::read_loop() (20 samples, 5.21%) +dpp::s.. + + +__GI___futex_abstimed_wait_cancelable64 (40 samples, 10.42%) +__GI___futex_ab.. + + +__futex_abstimed_wait_common64 (40 samples, 10.42%) +__futex_abstime.. + + +futex_wait (1 samples, 0.26%) + + + +__pthread_cond_wait_common (256 samples, 66.67%) +__pthread_cond_wait_common + + +std::vector<char, (1 samples, 0.26%) + + + +std::condition_variable::wait<bumbleBee::MusicPlayManager::play(dpp::discord_voice_client*)::<lambda(dpp::discord_voice_client*)>::<lambda()> (13 samples, 3.39%) +std.. + + +__GI___libc_read (3 samples, 0.78%) + + + +__GI__IO_fread (2 samples, 0.52%) + + + +?? (364 samples, 94.79%) +?? + + +__futex_abstimed_wait_common (20 samples, 5.21%) +__fute.. + + +start_thread (364 samples, 94.79%) +start_thread + + +__GI___lll_lock_wait (1 samples, 0.26%) + + + +clone3 (364 samples, 94.79%) +clone3 + + +dpp::http_request::run(dpp::cluster*) (4 samples, 1.04%) + + + +__GI__IO_default_uflow (4 samples, 1.04%) + + + +std::thread::_Invoker<std::tuple<bumbleBee::MusicPlayManager::play(dpp::discord_voice_client*)::<lambda(dpp::discord_voice_client*)>, (13 samples, 3.39%) +std.. + + +__GI___libc_read (4 samples, 1.04%) + + + +__GI___libc_read (3 samples, 0.78%) + + + +_IO_new_file_underflow (4 samples, 1.04%) + + + +__GI___libc_read (4 samples, 1.04%) + + + +dpp::discord_voice_client::stop_audio() (1 samples, 0.26%) + + + +std::__invoke<bumbleBee::BumbleBee::on_slashcommand(const (9 samples, 2.34%) +s.. + + +dpp::ssl_client::read_loop() (20 samples, 5.21%) +dpp::s.. + + +__GI__IO_getline_info (4 samples, 1.04%) + + + +bumbleBee::commands::Delete::execute (1 samples, 0.26%) + + + +std::__invoke_impl<void, (2 samples, 0.52%) + + + +dpp::cluster::start(bool) (20 samples, 5.21%) +dpp::c.. + + +std::vector<dpp::voice_out_packet, (3 samples, 0.78%) + + + +dpp::discord_voice_client::want_write() (1 samples, 0.26%) + + + +__pthread_cond_wait_common (13 samples, 3.39%) +__p.. + + +__futex_abstimed_wait_common64 (256 samples, 66.67%) +__futex_abstimed_wait_common64 + + +__GI___nanosleep (1 samples, 0.26%) + + + +oggz_read (2 samples, 0.52%) + + + +operator() (13 samples, 3.39%) +ope.. + + +std::thread::_Invoker<std::tuple<bumbleBee::MusicPlayManager::send_audio_to_voice(std::shared_ptr<bumbleBee::MusicQueueElement>, (2 samples, 0.52%) + + + +__GI___futex_abstimed_wait_cancelable64 (256 samples, 66.67%) +__GI___futex_abstimed_wait_cancelable64 + + +nlohmann::json_abi_v3_11_2::detail::parser<nlohmann::json_abi_v3_11_2::basic_json<std::map, (1 samples, 0.26%) + + + +std::move<char&> (1 samples, 0.26%) + + + +bumbleBee::ConsoleUtils::getResultFromCommand (4 samples, 1.04%) + + + +__futex_abstimed_wait_common64 (20 samples, 5.21%) +__fute.. + + +__GI___futex_abstimed_wait_cancelable64 (20 samples, 5.21%) +__GI__.. + + + diff --git a/systemStackCapture.sh b/systemStackCapture.sh new file mode 100755 index 0000000..a5ef1a7 --- /dev/null +++ b/systemStackCapture.sh @@ -0,0 +1,4 @@ +sudo echo recording... +sudo perf record -F 99 -a -g -- sleep 60 +sudo perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > stackflame.svg +sudo rm perf.data \ No newline at end of file diff --git a/yt-dlp b/yt-dlp index fde5494..ae873b3 100755 Binary files a/yt-dlp and b/yt-dlp differ