├── img └── demo.png ├── MANIFEST.in ├── profiler_online ├── log.py ├── __init__.py └── tools │ ├── difffolded.pl │ └── flamegraph.pl ├── test.py ├── LICENSE.txt ├── setup.py └── README.md /img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfyiamcool/profiler_online/HEAD/img/demo.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md profiler_online/tools/*.pl 2 | recursive-include profiler_online tools/*.pl 3 | -------------------------------------------------------------------------------- /profiler_online/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | def init_logger(logfile): 4 | logger = logging.getLogger() 5 | logger.setLevel(logging.INFO) 6 | fmt = '%(asctime)s - %(process)s - %(levelname)s: - %(message)s' 7 | formatter = logging.Formatter(fmt) 8 | handler = logging.FileHandler(logfile) 9 | handler.setFormatter(formatter) 10 | logger.addHandler(handler) 11 | return logger 12 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #cofing:utf-8 2 | import gevent 3 | from gevent import monkey 4 | monkey.patch_all() 5 | from gevent.pool import Pool 6 | import requests 7 | from profiler_online import run_profiler 8 | 9 | run_profiler() 10 | 11 | p = Pool(100) 12 | 13 | def handler_request(url): 14 | print len(requests.get(url).content) 15 | 16 | def fetch_data(): 17 | urls = ['http://www.baidu.com','http://www.sina.com.cn','http://www.163.com','http://www.oschina.net'] * 100 18 | for url in urls: 19 | p.spawn(handler_request, url) 20 | p.join() 21 | 22 | if __name__ == "__main__": 23 | while 1: 24 | fetch_data() 25 | gevent.sleep(3) 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 rui fengyun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | #from distutils.core import setup, Command 4 | from setuptools import setup,find_packages 5 | import os 6 | import os.path 7 | 8 | def read(fname): 9 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 10 | 11 | setup( 12 | name='profiler_online', 13 | version='2.3', 14 | description='profile python app online, Display FlameGraph in a browser', 15 | long_description=open('README.md').read(), 16 | keywords = ["profiler_online","fengyun"], 17 | url='http://xiaorui.cc', 18 | author='ruifengyun', 19 | author_email='rfyiamcool@163.com', 20 | install_requires=['gevent','flask'], 21 | package_data={'profiler_online': ['tools/*', 22 | 'tools/flamegraph.pl' 23 | ]}, 24 | packages=['profiler_online'], 25 | include_package_data=True, 26 | # zip_safe=False, 27 | license = "MIT", 28 | classifiers = [ 29 | 'Development Status :: 2 - Pre-Alpha', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Programming Language :: Python :: 2.6', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Programming Language :: Python :: 3.0', 35 | 'Topic :: Software Development :: Libraries :: Python Modules', 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目名 2 | 3 | `profiler_online` 4 | 5 | 更多profiler_online相关信息,转到这个链接 [xiaorui.cc](http://xiaorui.cc/2015/10/22/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E4%B9%8B%E8%B0%83%E8%AF%95python%E5%BA%94%E7%94%A8%E7%94%9F%E6%88%90%E6%80%A7%E8%83%BDcpu%E7%81%AB%E7%84%B0%E5%9B%BE/) 6 | 7 | ### 介绍: 8 | 用来解析查找python程序可能存在的各方面性能或者奇葩问题的工具,通过web访问访问可以直接拿到火焰图. 9 | 10 | 本项目是基于FlameGraph封装的,他本身是perl开发的,我就地封了一层调用,外加了web展现.这样对于python工程师来说,可以方便的把调试功能加入应用里面. 11 | 12 | *Gregg开发的FlameGraph源码* 13 | 14 | [https://github.com/brendangregg/FlameGraph](https://github.com/brendangregg/FlameGraph) 15 | 16 | *Python systemTap参考文档* 17 | 18 | [https://github.com/nylas/nylas-perftools](https://github.com/nylas/nylas-perftools) 19 | 20 | Will Add Future: 21 | 22 | * 加入更完善的信号控制 23 | * 加入内存的相关信息 24 | * 查询时间范围 25 | 26 | ### 安装: 27 | 28 | **pypi** 29 | 30 | ``` 31 | pip install profiler_online 32 | ``` 33 | 34 | **源码安装** 35 | 36 | ``` 37 | git clone https://github.com/rfyiamcool/profiler_online.git 38 | cd profiler_online 39 | python setup.py install 40 | ``` 41 | 42 | ### 用法: 43 | 44 | 这边已经封装好了,你需要做的只是把性能分析模块引入到你的应用里面. 45 | 46 | ``` 47 | from profiler_online import run_profiler 48 | run_profiler() 49 | ``` 50 | 51 | run_profiler支持三个参数: 52 | ``` 53 | debug_config = { 54 | 'host': '127.0.0.1', 55 | 'port': 8080, 56 | 'tmp_path: '/tmp/debug' 57 | } 58 | run_profiler(**debug_config) 59 | ``` 60 | 61 | ### 测试: 62 | 63 | 打开浏览器 http://127.0.0.1:8080 这样就可以显示正在运行服务的性能火焰图了. 64 | 65 | ![image](https://github.com/rfyiamcool/profiler_online/raw/master/img/demo.png) 66 | 67 | ### 问题: 68 | 69 | 下面是以前创建火焰图的方法. 70 | 71 | ``` 72 | python test.py 73 | curl "127.0.0.1:8080" | profiler_online/tools/flamegraph.pl > flame.html 74 | ``` 75 | 76 | 改进的方法: 77 | ``` 78 | 直接浏览器打开,地址栏 --> 127.0.0.1:8080 79 | ``` 80 | 81 | 在开发过程中,遇到了python系统调用时不能正常捕获输出. 现在已经改为临时文件的方式. 82 | ``` 83 | cmdstr = './profiler_online/tools/flamegraph.pl' 84 | p = subprocess.Popen(cmdstr, stdin = subprocess.PIPE, stderr = subprocess.PIPE, shell = True) 85 | p.stdin.write(stats) 86 | p.stdin.flush() 87 | try: 88 | if p.stderr: 89 | stats = p.stderr.read() 90 | p.stderr.flush() 91 | if p.stdout: 92 | stats = p.stdout.read() 93 | except Exception, e: 94 | print e,Exception 95 | ``` 96 | 97 | 98 | -------------------------------------------------------------------------------- /profiler_online/__init__.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | import collections 3 | import signal 4 | import time 5 | import gevent 6 | import threading 7 | import subprocess 8 | from werkzeug.serving import BaseWSGIServer, WSGIRequestHandler 9 | from werkzeug.wrappers import Request, Response 10 | from profiler_online.log import init_logger 11 | import os 12 | 13 | logger = init_logger('debug.log') 14 | 15 | class Sampler(object): 16 | def __init__(self, interval=0.005): 17 | self.interval = interval 18 | self._started = None 19 | self._stack_counts = collections.defaultdict(int) 20 | 21 | def start(self): 22 | self._started = time.time() 23 | try: 24 | signal.signal(signal.SIGVTALRM, self._sample) 25 | except ValueError: 26 | raise ValueError('Can only sample on the main thread') 27 | 28 | signal.setitimer(signal.ITIMER_VIRTUAL, self.interval, 0) 29 | 30 | def _sample(self, signum, frame): 31 | stack = [] 32 | while frame is not None: 33 | stack.append(self._format_frame(frame)) 34 | frame = frame.f_back 35 | 36 | stack = ';'.join(reversed(stack)) 37 | self._stack_counts[stack] += 1 38 | signal.setitimer(signal.ITIMER_VIRTUAL, self.interval, 0) 39 | 40 | def _format_frame(self, frame): 41 | return '{}({})'.format(frame.f_code.co_name, 42 | frame.f_globals.get('__name__')) 43 | 44 | def output_stats(self): 45 | if self._started is None: 46 | return '' 47 | elapsed = time.time() - self._started 48 | lines = ['elapsed {}'.format(elapsed), 49 | 'granularity {}'.format(self.interval)] 50 | ordered_stacks = sorted(self._stack_counts.items(), 51 | key=lambda kv: kv[1], reverse=True) 52 | lines.extend(['{} {}'.format(frame, count) 53 | for frame, count in ordered_stacks]) 54 | return '\n'.join(lines) + '\n' 55 | 56 | def reset(self): 57 | self._started = time.time() 58 | self._stack_counts = collections.defaultdict(int) 59 | 60 | 61 | class Emitter(object): 62 | def __init__(self, sampler, host, port, tmp_path): 63 | self.sampler = sampler 64 | self.host = host 65 | self.port = port 66 | self.tmp_path = tmp_path 67 | 68 | def handle_request(self, environ, start_response): 69 | stats = self.sampler.output_stats() 70 | request = Request(environ) 71 | if request.args.get('reset') in ('1', 'true'): 72 | self.sampler.reset() 73 | with open('debug.out','w') as f: 74 | f.write(stats) 75 | 76 | pl = "%s/tools/flamegraph.pl"%os.path.dirname(__file__) 77 | cmdstr = 'cat %s/debug.out | perl %s'%(self.tmp_path,pl) 78 | p = subprocess.Popen(cmdstr, stdin = subprocess.PIPE,stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell = True) 79 | stats = p.stdout.read() 80 | response = Response(stats,mimetype='text/html') 81 | return response(environ, start_response) 82 | 83 | def run(self): 84 | server = BaseWSGIServer(self.host, self.port, self.handle_request, 85 | _QuietHandler) 86 | logger.info('Serving profiles on port {}'.format(self.port)) 87 | server.serve_forever() 88 | 89 | 90 | class _QuietHandler(WSGIRequestHandler): 91 | def log_request(self, *args, **kwargs): 92 | """Suppress request logging so as not to pollute application logs.""" 93 | pass 94 | 95 | def run_profiler(host='0.0.0.0', port=8080, tmp_path=os.getcwd()): 96 | try: 97 | gevent.spawn(run_worker,host,port,tmp_path) 98 | except e: 99 | t = threading.Thread(target=run_worker,args=(host,port,tmp_path)) 100 | t.start() 101 | 102 | def run_worker(host,port,tmp_path): 103 | sampler = Sampler() 104 | sampler.start() 105 | e = Emitter(sampler, host, port, tmp_path) 106 | e.run() 107 | 108 | -------------------------------------------------------------------------------- /profiler_online/tools/difffolded.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # difffolded.pl diff two folded stack files. Use this for generating 4 | # flame graph differentials. 5 | # 6 | # USAGE: ./difffolded.pl [-hns] folded1 folded2 | ./flamegraph.pl > diff2.svg 7 | # 8 | # Options are described in the usage message (-h). 9 | # 10 | # The flamegraph will be colored based on higher samples (red) and smaller 11 | # samples (blue). The frame widths will be based on the 2nd folded file. 12 | # This might be confusing if stack frames disappear entirely; it will make 13 | # the most sense to ALSO create a differential based on the 1st file widths, 14 | # while switching the hues; eg: 15 | # 16 | # ./difffolded.pl folded2 folded1 | ./flamegraph.pl --negate > diff1.svg 17 | # 18 | # Here's what they mean when comparing a before and after profile: 19 | # 20 | # diff1.svg: widths show the before profile, colored by what WILL happen 21 | # diff2.svg: widths show the after profile, colored by what DID happen 22 | # 23 | # INPUT: See stackcollapse* programs. 24 | # 25 | # OUTPUT: The full list of stacks, with two columns, one from each file. 26 | # If a stack wasn't present in a file, the column value is zero. 27 | # 28 | # folded_stack_trace count_from_folded1 count_from_folded2 29 | # 30 | # eg: 31 | # 32 | # funca;funcb;funcc 31 33 33 | # ... 34 | # 35 | # COPYRIGHT: Copyright (c) 2014 Brendan Gregg. 36 | # 37 | # This program is free software; you can redistribute it and/or 38 | # modify it under the terms of the GNU General Public License 39 | # as published by the Free Software Foundation; either version 2 40 | # of the License, or (at your option) any later version. 41 | # 42 | # This program is distributed in the hope that it will be useful, 43 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 44 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 45 | # GNU General Public License for more details. 46 | # 47 | # You should have received a copy of the GNU General Public License 48 | # along with this program; if not, write to the Free Software Foundation, 49 | # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 50 | # 51 | # (http://www.gnu.org/copyleft/gpl.html) 52 | # 53 | # 28-Oct-2014 Brendan Gregg Created this. 54 | 55 | use strict; 56 | use Getopt::Std; 57 | 58 | # defaults 59 | my $normalize = 0; # make sample counts equal 60 | my $striphex = 0; # strip hex numbers 61 | 62 | sub usage { 63 | print STDERR < diff2.svg 65 | -h # help message 66 | -n # normalize sample counts 67 | -s # strip hex numbers (addresses) 68 | See stackcollapse scripts for generating folded files. 69 | Also consider flipping the files and hues to highlight reduced paths: 70 | $0 folded2 folded1 | ./flamegraph.pl --negate > diff1.svg 71 | USAGE_END 72 | exit 2; 73 | } 74 | 75 | usage() if @ARGV < 2; 76 | our($opt_h, $opt_n, $opt_s); 77 | getopts('ns') or usage(); 78 | usage() if $opt_h; 79 | $normalize = 1 if defined $opt_n; 80 | $striphex = 1 if defined $opt_s; 81 | 82 | my ($total1, $total2) = (0, 0); 83 | my %Folded; 84 | 85 | my $file1 = $ARGV[0]; 86 | my $file2 = $ARGV[1]; 87 | 88 | open FILE, $file1 or die "ERROR: Can't read $file1\n"; 89 | while () { 90 | chomp; 91 | my ($stack, $count) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 92 | $stack =~ s/0x[0-9a-fA-F]+/0x.../g if $striphex; 93 | $Folded{$stack}{1} += $count; 94 | $total1 += $count; 95 | } 96 | close FILE; 97 | 98 | open FILE, $file2 or die "ERROR: Can't read $file2\n"; 99 | while () { 100 | chomp; 101 | my ($stack, $count) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 102 | $stack =~ s/0x[0-9a-fA-F]+/0x.../g if $striphex; 103 | $Folded{$stack}{2} += $count; 104 | $total2 += $count; 105 | } 106 | close FILE; 107 | 108 | foreach my $stack (keys %Folded) { 109 | $Folded{$stack}{1} = 0 unless defined $Folded{$stack}{1}; 110 | $Folded{$stack}{2} = 0 unless defined $Folded{$stack}{2}; 111 | if ($normalize && $total1 != $total2) { 112 | $Folded{$stack}{1} = int($Folded{$stack}{1} * $total2 / $total1); 113 | } 114 | print "$stack $Folded{$stack}{1} $Folded{$stack}{2}\n"; 115 | } 116 | -------------------------------------------------------------------------------- /profiler_online/tools/flamegraph.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # flamegraph.pl flame stack grapher. 4 | # 5 | # This takes stack samples and renders a call graph, allowing hot functions 6 | # and codepaths to be quickly identified. Stack samples can be generated using 7 | # tools such as DTrace, perf, SystemTap, and Instruments. 8 | # 9 | # USAGE: ./flamegraph.pl [options] input.txt > graph.svg 10 | # 11 | # grep funcA input.txt | ./flamegraph.pl [options] > graph.svg 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 | # 16 | # Options are listed in the usage message (--help). 17 | # 18 | # The input is stack frames and sample counts formatted as single lines. Each 19 | # frame in the stack is semicolon separated, with a space and count at the end 20 | # of the line. These can be generated using DTrace with stackcollapse.pl, 21 | # and other tools using the stackcollapse variants. 22 | # 23 | # An optional extra column of counts can be provided to generate a differential 24 | # flame graph of the counts, colored red for more, and blue for less. This 25 | # can be useful when using flame graphs for non-regression testing. 26 | # See the header comment in the difffolded.pl program for instructions. 27 | # 28 | # The output graph shows relative presence of functions in stack samples. The 29 | # ordering on the x-axis has no meaning; since the data is samples, time order 30 | # of events is not known. The order used sorts function names alphabetically. 31 | # 32 | # While intended to process stack samples, this can also process stack traces. 33 | # For example, tracing stacks for memory allocation, or resource usage. You 34 | # can use --title to set the title to reflect the content, and --countname 35 | # to change "samples" to "bytes" etc. 36 | # 37 | # There are a few different palettes, selectable using --color. By default, 38 | # the colors are selected at random (except for differentials). Functions 39 | # called "-" will be printed gray, which can be used for stack separators (eg, 40 | # between user and kernel stacks). 41 | # 42 | # HISTORY 43 | # 44 | # This was inspired by Neelakanth Nadgir's excellent function_call_graph.rb 45 | # program, which visualized function entry and return trace events. As Neel 46 | # wrote: "The output displayed is inspired by Roch's CallStackAnalyzer which 47 | # was in turn inspired by the work on vftrace by Jan Boerhout". See: 48 | # https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and 49 | # 50 | # Copyright 2011 Joyent, Inc. All rights reserved. 51 | # Copyright 2011 Brendan Gregg. All rights reserved. 52 | # 53 | # CDDL HEADER START 54 | # 55 | # The contents of this file are subject to the terms of the 56 | # Common Development and Distribution License (the "License"). 57 | # You may not use this file except in compliance with the License. 58 | # 59 | # You can obtain a copy of the license at docs/cddl1.txt or 60 | # http://opensource.org/licenses/CDDL-1.0. 61 | # See the License for the specific language governing permissions 62 | # and limitations under the License. 63 | # 64 | # When distributing Covered Code, include this CDDL HEADER in each 65 | # file and include the License file at docs/cddl1.txt. 66 | # If applicable, add the following below this CDDL HEADER, with the 67 | # fields enclosed by brackets "[]" replaced with your own identifying 68 | # information: Portions Copyright [yyyy] [name of copyright owner] 69 | # 70 | # CDDL HEADER END 71 | # 72 | # 11-Oct-2014 Adrien Mahieux Added zoom. 73 | # 21-Nov-2013 Shawn Sterling Added consistent palette file option 74 | # 17-Mar-2013 Tim Bunce Added options and more tunables. 75 | # 15-Dec-2011 Dave Pacheco Support for frames with whitespace. 76 | # 10-Sep-2011 Brendan Gregg Created this. 77 | 78 | use strict; 79 | 80 | use Getopt::Long; 81 | 82 | # tunables 83 | my $encoding; 84 | my $fonttype = "Verdana"; 85 | my $imagewidth = 1200; # max width, pixels 86 | my $frameheight = 16; # max height is dynamic 87 | my $fontsize = 12; # base text size 88 | my $fontwidth = 0.59; # avg width relative to fontsize 89 | my $minwidth = 0.1; # min function width, pixels 90 | my $nametype = "Function:"; # what are the names in the data? 91 | my $countname = "samples"; # what are the counts in the data? 92 | my $colors = "hot"; # color theme 93 | my $bgcolor1 = "#eeeeee"; # background color gradient start 94 | my $bgcolor2 = "#eeeeb0"; # background color gradient stop 95 | my $nameattrfile; # file holding function attributes 96 | my $timemax; # (override the) sum of the counts 97 | my $factor = 1; # factor to scale counts by 98 | my $hash = 0; # color by function name 99 | my $palette = 0; # if we use consistent palettes (default off) 100 | my %palette_map; # palette map hash 101 | my $pal_file = "palette.map"; # palette map file name 102 | my $stackreverse = 0; # reverse stack order, switching merge end 103 | my $inverted = 0; # icicle graph 104 | my $negate = 0; # switch differential hues 105 | my $titletext = ""; # centered heading 106 | my $titledefault = "Flame Graph"; # overwritten by --title 107 | my $titleinverted = "Icicle Graph"; # " " 108 | my $searchcolor = "rgb(230,0,230)"; # color for search highlighting 109 | my $help = 0; 110 | 111 | sub usage { 112 | die < outfile.svg\n 114 | --title # change title text 115 | --width # width of image (default 1200) 116 | --height # height of each frame (default 16) 117 | --minwidth # omit smaller functions (default 0.1 pixels) 118 | --fonttype # font type (default "Verdana") 119 | --fontsize # font size (default 12) 120 | --countname # count type label (default "samples") 121 | --nametype # name type label (default "Function:") 122 | --colors # set color palette. choices are: hot (default), mem, io, 123 | # java, js, red, green, blue, yellow, purple, orange 124 | --hash # colors are keyed by function name hash 125 | --cp # use consistent palette (palette.map) 126 | --reverse # generate stack-reversed flame graph 127 | --inverted # icicle graph 128 | --negate # switch differential hues (blue<->red) 129 | --help # this message 130 | 131 | eg, 132 | $0 --title="Flame Graph: malloc()" trace.txt > graph.svg 133 | USAGE_END 134 | } 135 | 136 | GetOptions( 137 | 'fonttype=s' => \$fonttype, 138 | 'width=i' => \$imagewidth, 139 | 'height=i' => \$frameheight, 140 | 'encoding=s' => \$encoding, 141 | 'fontsize=f' => \$fontsize, 142 | 'fontwidth=f' => \$fontwidth, 143 | 'minwidth=f' => \$minwidth, 144 | 'title=s' => \$titletext, 145 | 'nametype=s' => \$nametype, 146 | 'countname=s' => \$countname, 147 | 'nameattr=s' => \$nameattrfile, 148 | 'total=s' => \$timemax, 149 | 'factor=f' => \$factor, 150 | 'colors=s' => \$colors, 151 | 'hash' => \$hash, 152 | 'cp' => \$palette, 153 | 'reverse' => \$stackreverse, 154 | 'inverted' => \$inverted, 155 | 'negate' => \$negate, 156 | 'help' => \$help, 157 | ) or usage(); 158 | $help && usage(); 159 | 160 | # internals 161 | my $ypad1 = $fontsize * 4; # pad top, include title 162 | my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels 163 | my $xpad = 10; # pad lefm and right 164 | my $framepad = 1; # vertical padding for frames 165 | my $depthmax = 0; 166 | my %Events; 167 | my %nameattr; 168 | 169 | if ($titletext eq "") { 170 | unless ($inverted) { 171 | $titletext = $titledefault; 172 | } else { 173 | $titletext = $titleinverted; 174 | } 175 | } 176 | 177 | if ($nameattrfile) { 178 | # The name-attribute file format is a function name followed by a tab then 179 | # a sequence of tab separated name=value pairs. 180 | open my $attrfh, $nameattrfile or die "Can't read $nameattrfile: $!\n"; 181 | while (<$attrfh>) { 182 | chomp; 183 | my ($funcname, $attrstr) = split /\t/, $_, 2; 184 | die "Invalid format in $nameattrfile" unless defined $attrstr; 185 | $nameattr{$funcname} = { map { split /=/, $_, 2 } split /\t/, $attrstr }; 186 | } 187 | } 188 | 189 | if ($colors eq "mem") { $bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff"; } 190 | if ($colors eq "io") { $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8"; } 191 | 192 | # SVG functions 193 | { package SVG; 194 | sub new { 195 | my $class = shift; 196 | my $self = {}; 197 | bless ($self, $class); 198 | return $self; 199 | } 200 | 201 | sub header { 202 | my ($self, $w, $h) = @_; 203 | my $enc_attr = ''; 204 | if (defined $encoding) { 205 | $enc_attr = qq{ encoding="$encoding"}; 206 | } 207 | $self->{svg} .= < 209 | 210 | 211 | 212 | SVG 213 | } 214 | 215 | sub include { 216 | my ($self, $content) = @_; 217 | $self->{svg} .= $content; 218 | } 219 | 220 | sub colorAllocate { 221 | my ($self, $r, $g, $b) = @_; 222 | return "rgb($r,$g,$b)"; 223 | } 224 | 225 | sub group_start { 226 | my ($self, $attr) = @_; 227 | 228 | my @g_attr = map { 229 | exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : () 230 | } qw(class style onmouseover onmouseout onclick); 231 | push @g_attr, $attr->{g_extra} if $attr->{g_extra}; 232 | $self->{svg} .= sprintf qq/\n/, join(' ', @g_attr); 233 | 234 | $self->{svg} .= sprintf qq/%s<\/title>/, $attr->{title} 235 | if $attr->{title}; # should be first element within g container 236 | 237 | if ($attr->{href}) { 238 | my @a_attr; 239 | push @a_attr, sprintf qq/xlink:href="%s"/, $attr->{href} if $attr->{href}; 240 | # default target=_top else links will open within SVG 241 | push @a_attr, sprintf qq/target="%s"/, $attr->{target} || "_top"; 242 | push @a_attr, $attr->{a_extra} if $attr->{a_extra}; 243 | $self->{svg} .= sprintf qq//, join(' ', @a_attr); 244 | } 245 | } 246 | 247 | sub group_end { 248 | my ($self, $attr) = @_; 249 | $self->{svg} .= qq/<\/a>\n/ if $attr->{href}; 250 | $self->{svg} .= qq/<\/g>\n/; 251 | } 252 | 253 | sub filledRectangle { 254 | my ($self, $x1, $y1, $x2, $y2, $fill, $extra) = @_; 255 | $x1 = sprintf "%0.1f", $x1; 256 | $x2 = sprintf "%0.1f", $x2; 257 | my $w = sprintf "%0.1f", $x2 - $x1; 258 | my $h = sprintf "%0.1f", $y2 - $y1; 259 | $extra = defined $extra ? $extra : ""; 260 | $self->{svg} .= qq/\n/; 261 | } 262 | 263 | sub stringTTF { 264 | my ($self, $color, $font, $size, $angle, $x, $y, $str, $loc, $extra) = @_; 265 | $x = sprintf "%0.2f", $x; 266 | $loc = defined $loc ? $loc : "left"; 267 | $extra = defined $extra ? $extra : ""; 268 | $self->{svg} .= qq/$str<\/text>\n/; 269 | } 270 | 271 | sub svg { 272 | my $self = shift; 273 | return "$self->{svg}\n"; 274 | } 275 | 1; 276 | } 277 | 278 | sub namehash { 279 | # Generate a vector hash for the name string, weighting early over 280 | # later characters. We want to pick the same colors for function 281 | # names across different flame graphs. 282 | my $name = shift; 283 | my $vector = 0; 284 | my $weight = 1; 285 | my $max = 1; 286 | my $mod = 10; 287 | # if module name present, trunc to 1st char 288 | $name =~ s/.(.*?)`//; 289 | foreach my $c (split //, $name) { 290 | my $i = (ord $c) % $mod; 291 | $vector += ($i / ($mod++ - 1)) * $weight; 292 | $max += 1 * $weight; 293 | $weight *= 0.70; 294 | last if $mod > 12; 295 | } 296 | return (1 - $vector / $max) 297 | } 298 | 299 | sub color { 300 | my ($type, $hash, $name) = @_; 301 | my ($v1, $v2, $v3); 302 | 303 | if ($hash) { 304 | $v1 = namehash($name); 305 | $v2 = $v3 = namehash(scalar reverse $name); 306 | } else { 307 | $v1 = rand(1); 308 | $v2 = rand(1); 309 | $v3 = rand(1); 310 | } 311 | 312 | # theme palettes 313 | if (defined $type and $type eq "hot") { 314 | my $r = 205 + int(50 * $v3); 315 | my $g = 0 + int(230 * $v1); 316 | my $b = 0 + int(55 * $v2); 317 | return "rgb($r,$g,$b)"; 318 | } 319 | if (defined $type and $type eq "mem") { 320 | my $r = 0; 321 | my $g = 190 + int(50 * $v2); 322 | my $b = 0 + int(210 * $v1); 323 | return "rgb($r,$g,$b)"; 324 | } 325 | if (defined $type and $type eq "io") { 326 | my $r = 80 + int(60 * $v1); 327 | my $g = $r; 328 | my $b = 190 + int(55 * $v2); 329 | return "rgb($r,$g,$b)"; 330 | } 331 | 332 | # multi palettes 333 | if (defined $type and $type eq "java") { 334 | if ($name =~ /::/) { # C++ 335 | $type = "yellow"; 336 | } elsif ($name =~ m:/:) { # Java (match "/" in path) 337 | $type = "green" 338 | } else { # system 339 | $type = "red"; 340 | } 341 | # fall-through to color palettes 342 | } 343 | if (defined $type and $type eq "js") { 344 | if ($name =~ /::/) { # C++ 345 | $type = "yellow"; 346 | } elsif ($name =~ m:/:) { # JavaScript (match "/" in path) 347 | $type = "green" 348 | } elsif ($name =~ m/:/) { # JavaScript (match ":" in builtin) 349 | $type = "aqua" 350 | } elsif ($name =~ m/^ $/) { # Missing symbol 351 | $type = "green" 352 | } else { # system 353 | $type = "red"; 354 | } 355 | # fall-through to color palettes 356 | } 357 | 358 | # color palettes 359 | if (defined $type and $type eq "red") { 360 | my $r = 200 + int(55 * $v1); 361 | my $x = 50 + int(80 * $v1); 362 | return "rgb($r,$x,$x)"; 363 | } 364 | if (defined $type and $type eq "green") { 365 | my $g = 200 + int(55 * $v1); 366 | my $x = 50 + int(60 * $v1); 367 | return "rgb($x,$g,$x)"; 368 | } 369 | if (defined $type and $type eq "blue") { 370 | my $b = 205 + int(50 * $v1); 371 | my $x = 80 + int(60 * $v1); 372 | return "rgb($x,$x,$b)"; 373 | } 374 | if (defined $type and $type eq "yellow") { 375 | my $x = 175 + int(55 * $v1); 376 | my $b = 50 + int(20 * $v1); 377 | return "rgb($x,$x,$b)"; 378 | } 379 | if (defined $type and $type eq "purple") { 380 | my $x = 190 + int(65 * $v1); 381 | my $g = 80 + int(60 * $v1); 382 | return "rgb($x,$g,$x)"; 383 | } 384 | if (defined $type and $type eq "aqua") { 385 | my $r = 50 + int(60 * $v1); 386 | my $g = 165 + int(55 * $v1); 387 | my $b = 165 + int(55 * $v1); 388 | return "rgb($r,$g,$b)"; 389 | } 390 | if (defined $type and $type eq "orange") { 391 | my $r = 190 + int(65 * $v1); 392 | my $g = 90 + int(65 * $v1); 393 | return "rgb($r,$g,0)"; 394 | } 395 | 396 | return "rgb(0,0,0)"; 397 | } 398 | 399 | sub color_scale { 400 | my ($value, $max) = @_; 401 | my ($r, $g, $b) = (255, 255, 255); 402 | $value = -$value if $negate; 403 | if ($value > 0) { 404 | $g = $b = int(210 * ($max - $value) / $max); 405 | } elsif ($value < 0) { 406 | $r = $g = int(210 * ($max + $value) / $max); 407 | } 408 | return "rgb($r,$g,$b)"; 409 | } 410 | 411 | sub color_map { 412 | my ($colors, $func) = @_; 413 | if (exists $palette_map{$func}) { 414 | return $palette_map{$func}; 415 | } else { 416 | $palette_map{$func} = color($colors); 417 | return $palette_map{$func}; 418 | } 419 | } 420 | 421 | sub write_palette { 422 | open(FILE, ">$pal_file"); 423 | foreach my $key (sort keys %palette_map) { 424 | print FILE $key."->".$palette_map{$key}."\n"; 425 | } 426 | close(FILE); 427 | } 428 | 429 | sub read_palette { 430 | if (-e $pal_file) { 431 | open(FILE, $pal_file) or die "can't open file $pal_file: $!"; 432 | while ( my $line = ) { 433 | chomp($line); 434 | (my $key, my $value) = split("->",$line); 435 | $palette_map{$key}=$value; 436 | } 437 | close(FILE) 438 | } 439 | } 440 | 441 | my %Node; # Hash of merged frame data 442 | my %Tmp; 443 | 444 | # flow() merges two stacks, storing the merged frames and value data in %Node. 445 | sub flow { 446 | my ($last, $this, $v, $d) = @_; 447 | 448 | my $len_a = @$last - 1; 449 | my $len_b = @$this - 1; 450 | 451 | my $i = 0; 452 | my $len_same; 453 | for (; $i <= $len_a; $i++) { 454 | last if $i > $len_b; 455 | last if $last->[$i] ne $this->[$i]; 456 | } 457 | $len_same = $i; 458 | 459 | for ($i = $len_a; $i >= $len_same; $i--) { 460 | my $k = "$last->[$i];$i"; 461 | # a unique ID is constructed from "func;depth;etime"; 462 | # func-depth isn't unique, it may be repeated later. 463 | $Node{"$k;$v"}->{stime} = delete $Tmp{$k}->{stime}; 464 | if (defined $Tmp{$k}->{delta}) { 465 | $Node{"$k;$v"}->{delta} = delete $Tmp{$k}->{delta}; 466 | } 467 | delete $Tmp{$k}; 468 | } 469 | 470 | for ($i = $len_same; $i <= $len_b; $i++) { 471 | my $k = "$this->[$i];$i"; 472 | $Tmp{$k}->{stime} = $v; 473 | if (defined $d) { 474 | $Tmp{$k}->{delta} += $i == $len_b ? $d : 0; 475 | } 476 | } 477 | 478 | return $this; 479 | } 480 | 481 | # parse input 482 | my @Data; 483 | my $last = []; 484 | my $time = 0; 485 | my $delta = undef; 486 | my $ignored = 0; 487 | my $line; 488 | my $maxdelta = 1; 489 | 490 | # reverse if needed 491 | foreach (<>) { 492 | chomp; 493 | $line = $_; 494 | if ($stackreverse) { 495 | # there may be an extra samples column for differentials 496 | # XXX todo: redo these REs as one. It's repeated below. 497 | my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 498 | my $samples2 = undef; 499 | if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { 500 | $samples2 = $samples; 501 | ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 502 | unshift @Data, join(";", reverse split(";", $stack)) . " $samples $samples2"; 503 | } else { 504 | unshift @Data, join(";", reverse split(";", $stack)) . " $samples"; 505 | } 506 | } else { 507 | unshift @Data, $line; 508 | } 509 | } 510 | 511 | # process and merge frames 512 | foreach (sort @Data) { 513 | chomp; 514 | # process: folded_stack count 515 | # eg: func_a;func_b;func_c 31 516 | my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 517 | unless (defined $samples and defined $stack) { 518 | ++$ignored; 519 | next; 520 | } 521 | 522 | # there may be an extra samples column for differentials: 523 | my $samples2 = undef; 524 | if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { 525 | $samples2 = $samples; 526 | ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 527 | } 528 | $delta = undef; 529 | if (defined $samples2) { 530 | $delta = $samples2 - $samples; 531 | $maxdelta = abs($delta) if abs($delta) > $maxdelta; 532 | } 533 | 534 | $stack =~ tr/<>/()/; 535 | 536 | # merge frames and populate %Node: 537 | $last = flow($last, [ '', split ";", $stack ], $time, $delta); 538 | 539 | if (defined $samples2) { 540 | $time += $samples2; 541 | } else { 542 | $time += $samples; 543 | } 544 | } 545 | flow($last, [], $time, $delta); 546 | 547 | warn "Ignored $ignored lines with invalid format\n" if $ignored; 548 | unless ($time) { 549 | warn "ERROR: No stack counts found\n"; 550 | my $im = SVG->new(); 551 | # emit an error message SVG, for tools automating flamegraph use 552 | my $imageheight = $fontsize * 5; 553 | $im->header($imagewidth, $imageheight); 554 | $im->stringTTF($im->colorAllocate(0, 0, 0), $fonttype, $fontsize + 2, 555 | 0.0, int($imagewidth / 2), $fontsize * 2, 556 | "ERROR: No valid input provided to flamegraph.pl.", "middle"); 557 | print $im->svg; 558 | exit 2; 559 | } 560 | if ($timemax and $timemax < $time) { 561 | warn "Specified --total $timemax is less than actual total $time, so ignored\n" 562 | if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc) 563 | undef $timemax; 564 | } 565 | $timemax ||= $time; 566 | 567 | my $widthpertime = ($imagewidth - 2 * $xpad) / $timemax; 568 | my $minwidth_time = $minwidth / $widthpertime; 569 | 570 | # prune blocks that are too narrow and determine max depth 571 | while (my ($id, $node) = each %Node) { 572 | my ($func, $depth, $etime) = split ";", $id; 573 | my $stime = $node->{stime}; 574 | die "missing start for $id" if not defined $stime; 575 | 576 | if (($etime-$stime) < $minwidth_time) { 577 | delete $Node{$id}; 578 | next; 579 | } 580 | $depthmax = $depth if $depth > $depthmax; 581 | } 582 | 583 | # draw canvas, and embed interactive JavaScript program 584 | my $imageheight = ($depthmax * $frameheight) + $ypad1 + $ypad2; 585 | my $im = SVG->new(); 586 | $im->header($imagewidth, $imageheight); 587 | my $inc = < 589 | 590 | 591 | 592 | 593 | 594 | 597 | 916 | INC 917 | $im->include($inc); 918 | $im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)'); 919 | my ($white, $black, $vvdgrey, $vdgrey) = ( 920 | $im->colorAllocate(255, 255, 255), 921 | $im->colorAllocate(0, 0, 0), 922 | $im->colorAllocate(40, 40, 40), 923 | $im->colorAllocate(160, 160, 160), 924 | ); 925 | $im->stringTTF($black, $fonttype, $fontsize + 5, 0.0, int($imagewidth / 2), $fontsize * 2, $titletext, "middle"); 926 | $im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $imageheight - ($ypad2 / 2), " ", "", 'id="details"'); 927 | $im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $fontsize * 2, 928 | "Reset Zoom", "", 'id="unzoom" onclick="unzoom()" style="opacity:0.0;cursor:pointer"'); 929 | $im->stringTTF($black, $fonttype, $fontsize, 0.0, $imagewidth - $xpad - 100, 930 | $fontsize * 2, "Search", "", 'id="search" onmouseover="searchover()" onmouseout="searchout()" onclick="search_prompt()" style="opacity:0.1;cursor:pointer"'); 931 | $im->stringTTF($black, $fonttype, $fontsize, 0.0, $imagewidth - $xpad - 100, $imageheight - ($ypad2 / 2), " ", "", 'id="matched"'); 932 | 933 | if ($palette) { 934 | read_palette(); 935 | } 936 | 937 | # draw frames 938 | while (my ($id, $node) = each %Node) { 939 | my ($func, $depth, $etime) = split ";", $id; 940 | my $stime = $node->{stime}; 941 | my $delta = $node->{delta}; 942 | 943 | $etime = $timemax if $func eq "" and $depth == 0; 944 | 945 | my $x1 = $xpad + $stime * $widthpertime; 946 | my $x2 = $xpad + $etime * $widthpertime; 947 | my ($y1, $y2); 948 | unless ($inverted) { 949 | $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad; 950 | $y2 = $imageheight - $ypad2 - $depth * $frameheight; 951 | } else { 952 | $y1 = $ypad1 + $depth * $frameheight; 953 | $y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad; 954 | } 955 | 956 | my $samples = sprintf "%.0f", ($etime - $stime) * $factor; 957 | (my $samples_txt = $samples) # add commas per perlfaq5 958 | =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g; 959 | 960 | my $info; 961 | if ($func eq "" and $depth == 0) { 962 | $info = "all ($samples_txt $countname, 100%)"; 963 | } else { 964 | my $pct = sprintf "%.2f", ((100 * $samples) / ($timemax * $factor)); 965 | my $escaped_func = $func; 966 | $escaped_func =~ s/&/&/g; 967 | $escaped_func =~ s//>/g; 969 | $escaped_func =~ s/"/"/g; 970 | unless (defined $delta) { 971 | $info = "$escaped_func ($samples_txt $countname, $pct%)"; 972 | } else { 973 | my $d = $negate ? -$delta : $delta; 974 | my $deltapct = sprintf "%.2f", ((100 * $d) / ($timemax * $factor)); 975 | $deltapct = $d > 0 ? "+$deltapct" : $deltapct; 976 | $info = "$escaped_func ($samples_txt $countname, $pct%; $deltapct%)"; 977 | } 978 | } 979 | 980 | my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone 981 | $nameattr->{class} ||= "func_g"; 982 | $nameattr->{onmouseover} ||= "s(this)"; 983 | $nameattr->{onmouseout} ||= "c()"; 984 | $nameattr->{onclick} ||= "zoom(this)"; 985 | $nameattr->{title} ||= $info; 986 | $im->group_start($nameattr); 987 | 988 | my $color; 989 | if ($func eq "-") { 990 | $color = $vdgrey; 991 | } elsif (defined $delta) { 992 | $color = color_scale($delta, $maxdelta); 993 | } elsif ($palette) { 994 | $color = color_map($colors, $func); 995 | } else { 996 | $color = color($colors, $hash, $func); 997 | } 998 | $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"'); 999 | 1000 | my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth)); 1001 | my $text = ""; 1002 | if ($chars >= 3) { # room for one char plus two dots 1003 | $text = substr $func, 0, $chars; 1004 | substr($text, -2, 2) = ".." if $chars < length $func; 1005 | $text =~ s/&/&/g; 1006 | $text =~ s//>/g; 1008 | } 1009 | $im->stringTTF($black, $fonttype, $fontsize, 0.0, $x1 + 3, 3 + ($y1 + $y2) / 2, $text, ""); 1010 | 1011 | $im->group_end($nameattr); 1012 | } 1013 | 1014 | print $im->svg; 1015 | 1016 | if ($palette) { 1017 | write_palette(); 1018 | } 1019 | 1020 | # vim: ts=8 sts=8 sw=8 noexpandtab 1021 | --------------------------------------------------------------------------------