├── docs ├── ArcLamp_2019_diagram.png └── cddl1.txt ├── scap └── scap.cfg ├── arclamp-log.yaml ├── arclamp-generate-index.py ├── arclamp-compress-logs ├── README.md ├── ArcLamp.php ├── arclamp-grep.py ├── arclamp-log.py ├── arclamp-generate-metrics ├── arclamp-generate-svgs ├── LICENSE └── flamegraph.pl /docs/ArcLamp_2019_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikimedia/performance-arc-lamp/HEAD/docs/ArcLamp_2019_diagram.png -------------------------------------------------------------------------------- /scap/scap.cfg: -------------------------------------------------------------------------------- 1 | [global] 2 | git_repo: performance/arc-lamp 3 | git_repo_user: deploy-service 4 | ssh_user: deploy-service 5 | dsh_targets: arclamp 6 | service_name: arclamp 7 | 8 | # Skip restart if service is absent or masked 9 | require_valid_service: True 10 | -------------------------------------------------------------------------------- /arclamp-log.yaml: -------------------------------------------------------------------------------- 1 | base_path: /srv/arclamp/logs 2 | logs: 3 | - format: "%Y-%m-%d_%H" 4 | period: hourly 5 | # 336 hourly files = 14 days * 24 hourly files 6 | retain: 336 7 | - format: "%Y-%m-%d" 8 | period: daily 9 | retain: 45 10 | redis: 11 | host: "127.0.0.1" 12 | port: 6379 13 | -------------------------------------------------------------------------------- /arclamp-generate-index.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | arclamp-generate-index.py 4 | ~~~~~~~~~ 5 | 6 | Writes a JSON index of SVGs stored within Swift. 7 | 8 | """ 9 | 10 | from tempfile import NamedTemporaryFile 11 | import os 12 | import shutil 13 | import logging 14 | from swiftclient.service import SwiftService, SwiftError 15 | import json 16 | 17 | class Output(object): 18 | def __enter__(self): 19 | self.out = NamedTemporaryFile(prefix='arclamp-index-', suffix='.json') 20 | self.first = True 21 | return self 22 | 23 | def __exit__(self, type, value, tb): 24 | self.out.close() 25 | 26 | def append(self, x): 27 | if self.first: 28 | self.out.write('[') 29 | self.first = False 30 | else: 31 | self.out.write(',') 32 | self.out.write(json.dumps(x)) 33 | 34 | def done(self): 35 | self.out.write(']') 36 | self.out.flush() 37 | shutil.copy(self.out.name, '/srv/arclamp/index.json') 38 | os.chmod('/srv/arclamp/index.json', 0o644) 39 | 40 | logging.basicConfig(level=logging.ERROR) 41 | logging.getLogger("requests").setLevel(logging.CRITICAL) 42 | logging.getLogger("swiftclient").setLevel(logging.CRITICAL) 43 | logger = logging.getLogger(__name__) 44 | 45 | with SwiftService() as swift: 46 | with Output() as out: 47 | try: 48 | for container in ['arclamp-svgs-daily', 'arclamp-svgs-hourly']: 49 | for page in swift.list(container=container): 50 | if page["success"]: 51 | for item in page["listing"]: 52 | out.append(item) 53 | else: 54 | raise page["error"] 55 | out.done() 56 | except SwiftError as e: 57 | logger.error(e.value) 58 | -------------------------------------------------------------------------------- /arclamp-compress-logs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | days="$1" 4 | case $days in 5 | ''|*[!0-9]*): 6 | echo "usage: arclamp-compress-logs " >&2 7 | exit 1 8 | ;; 9 | esac 10 | 11 | # -e indicates this script should abort if any command does not exit 12 | # with status 0. To debug this, you can print commands to stderr as 13 | # they are executed, by running: `bash -x arclamp-compress-logs`. 14 | set -e 15 | set -u 16 | shopt -s globstar nullglob 17 | 18 | # SWIFT_RETENTION specifies the number of seconds to keep compressed 19 | # logs in Swift. 20 | SWIFT_RETENTION=94608000 # three years 21 | 22 | function compress_log() { 23 | local filename="$1" 24 | echo "Compressing $filename" 25 | 26 | # Files not matching *.log{,.gz} are ignored; we take advantage of 27 | # this to prevent partially-compressed output from being consumed. 28 | # 29 | # -N is the default, but it is included explicitly here, to call 30 | # out that the mtime of the compressed file *must* match the 31 | # original. 32 | # 33 | # -k keeps the original file around, to remove a race condition 34 | # with arclamp-generate-svgs between gzip and mv. This also 35 | # ensures we don't lose the original if gzip succeeds but mv 36 | # fails. 37 | nice gzip -9 -S .gz.tmp -N -k "$filename" 38 | if mv "$filename.gz.tmp" "$filename.gz"; then 39 | rm "$filename" 40 | else 41 | rm "$filename.gz.tmp" 42 | fi 43 | } 44 | 45 | function sort_log() { 46 | local filename="$1" 47 | echo "Sorting $filename" 48 | rm -vf "$filename.tmp" 49 | 50 | # Sorting the file first makes compression much more effective. 51 | # (Final compressed size is ~4x-6x bigger if input is unsorted). 52 | # 53 | # LC_ALL=C ensures sorting is by byte values rather than symbols, 54 | # which is a lot slower. 55 | LC_ALL=C nice sort -o "$filename.tmp" "$filename" 56 | 57 | # Make the sorted file have the same mtime as the unsorted input. 58 | touch "$filename.tmp" -m --reference="$filename" 59 | mv "$filename.tmp" "$filename" 60 | } 61 | 62 | for log in `find /srv/arclamp/logs -type f -name '*.log' -mtime +$days || true`; do 63 | sort_log "$log" 64 | compress_log "$log" 65 | 66 | # Copy to Swift if configured. ($ST_AUTH expression prevents -u 67 | # from complaining if unset.) 68 | # 69 | # See https://docs.openstack.org/python-swiftclient/latest for 70 | # more details on the Swift CLI. 71 | if [ -s "$log.gz" -a -n "${ST_AUTH-}" -a -x /usr/bin/swift ]; then 72 | period=$(basename $(dirname "$log")) 73 | swift upload arclamp-logs-$period "$log.gz" \ 74 | --object-name `basename "$log.gz"` \ 75 | -H content-encoding:gzip \ 76 | -H x-delete-after:$SWIFT_RETENTION 77 | fi 78 | done 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Packagist](https://img.shields.io/packagist/v/wikimedia/arc-lamp.svg?style=flat)](https://packagist.org/packages/wikimedia/arc-lamp) 2 | 3 | # Arc Lamp 4 | 5 | Arc Lamp helps gather stack traces from a running PHP 7 application 6 | and publish them in the form of flame graphs and trace logs. 7 | 8 | See [performance.wikimedia.org](https://performance.wikimedia.org/php-profiling/) for a live example. 9 | 10 | ## Prerequisites 11 | 12 | Client: 13 | * Your PHP application, with [php-excimer](https://www.mediawiki.org/wiki/Excimer), and php-redis. 14 | 15 | Processor: 16 | * A Redis server. 17 | * Python, with `python-redis` and `python-yaml`. 18 | * The `ifne` command from `moreutils` ([Debian](https://packages.debian.org/stable/moreutils), [Homebrew](https://formulae.brew.sh/formula/moreutils), [source](https://joeyh.name/code/moreutils/)). 19 | 20 | ## Quick start 21 | 22 | ```php 23 | require_once 'ArcLamp.php'; 24 | 25 | Wikimedia\ArcLamp::collect( [ 'redis-host' => '127.0.0.1' ] ); 26 | ``` 27 | 28 | To automatically enable this for all web requests and entry points, you can use the PHP 29 | [`auto_prepend_file`](https://www.php.net/manual/en/ini.core.php#ini.auto-prepend-file) 30 | option. See [Wikimedia's own configuration](https://github.com/wikimedia/operations-mediawiki-config/blob/5959dc3e7610aa06d9307b42baed17a5bf1be719/wmf-config/PhpAutoPrepend.php#L5) for example. 31 | 32 | ## How it works 33 | 34 | _See also: [Profiling PHP in production](https://techblog.wikimedia.org/2021/03/03/profiling-php-in-production-at-scale/) on Wikimedia Techblog._ 35 | 36 | ![](./docs/ArcLamp_2019_diagram.png) 37 | 38 | The Arc Lamp pipeline comprises of three stages: 39 | 40 | 1. Capture stack traces. 41 | 2. Create trace logs. 42 | 3. Generate flame graphs. 43 | 44 | ### Capture stack traces 45 | 46 | The [php-excimer](https://www.mediawiki.org/wiki/Excimer) extension is used to periodically 47 | collect a backtrace. It has no run-time overhead by default. When enabled from a web request, 48 | it periodically schedules a graceful interrupt at which point it captures a backtrace. 49 | 50 | These traces can be collected from a PHP callback and dispatched to a socket or file as-needed. 51 | 52 | The default in `ArcLamp.php` is to send the trace to a Redis pubsub channel. 53 | 54 | > _Arc Lamp was originally created in 2014 for [Xenon](https://github.com/facebook/hhvm/wiki/Profiling#xenon), a sampling profiler native to the HHVM engine for PHP. To use Arc Lamp with HHVM Xenon, see [arc-lamp v1.0](https://gerrit.wikimedia.org/g/performance/arc-lamp/+/1.0.0/) instead._ 55 | 56 | ### Create trace logs 57 | 58 | In Wikimedia's production cluster, Arc Lamp's Redis server resides in the high availability "Tier 1" 59 | zone, and thus kept separate from offline performance, research, and statistics services ("Tier 2"). 60 | 61 | Each of the production web servers uses the `ArcLamp::collect` client in PHP to send traces to Redis. 62 | 63 | The `arclamp-log` service subscribes to the Redis pubsub channel, normalizes the stack traces and 64 | write them to the relevant trace log files. The [example configuration](./arclamp-log.yaml) creates 65 | a trace log file for each hour and each day. 66 | 67 | Within those two time periods, it segregates the trace logs by entry point of the PHP application. 68 | 69 | For example, the MediaWiki application has `index.php`, and `rest.php` (web) and `RunJobs.php` (CLI) 70 | entry points. This results in the following trace logs: 71 | 72 | * `daily/2019-12-21.all.log` 73 | * `daily/2019-12-21.index.log` 74 | * `daily/2019-12-21.rest.log` 75 | * `daily/2019-12-21.RunJobs.log` 76 | * … 77 | * `hourly/2019-12-21_20.all.log` 78 | * `hourly/2019-12-21_20.index.log` 79 | * `hourly/2019-12-21_20.rest.log` 80 | * `hourly/2019-12-21_20.RunJobs.log` 81 | * … 82 | 83 | The `arclamp-log` service is also responsible for pruning trace logs older than the configured 84 | retention period. 85 | 86 | ### Generate flame graphs 87 | 88 | The `arclamp-generate-svgs` script runs at a regular interval and creates or updates the flame graph 89 | associated with each trace log file. It also maintains a _reverse_ version of each flame graph. 90 | 91 | For example: 92 | 93 | * `daily/2019-12-21.all.svgz` 94 | * `daily/2019-12-21.all.reversed.svgz` 95 | * `daily/2019-12-21.index.svgz` 96 | * `daily/2019-12-21.index.reversed.svgz` 97 | * … 98 | * `hourly/2019-12-21_20.all.svgz` 99 | * `hourly/2019-12-21_20.all.reversed.svgz` 100 | * `hourly/2019-12-21_20.index.svgz` 101 | * `hourly/2019-12-21_20.index.reversed.svgz` 102 | * … 103 | 104 | The `arclamp-generate-svgs` script also removes graphs for which a trace log no longer exists. 105 | 106 | Flamegraphs are generated using Brendan Gregg's [flamegraph.pl](https://github.com/brendangregg/FlameGraph). 107 | 108 | ## Demo 109 | 110 | See [performance.wikimedia.org](https://performance.wikimedia.org/php-profiling/) for a live example. 111 | -------------------------------------------------------------------------------- /ArcLamp.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * Copyright Ori Livneh 6 | * Copyright Timo Tijhof 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * https://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | namespace Wikimedia; 21 | 22 | use Exception; 23 | use ExcimerProfiler; 24 | use Redis; 25 | 26 | class ArcLamp { 27 | /** 28 | * Collect stack samples in the background of this request. 29 | * 30 | * If a sample is taken during this web request, it will be flushed 31 | * to the specified Redis server on the "excimer" pubsub channel. 32 | * 33 | * @param array $options 34 | * - excimer-period: The sampling interval (in seconds) 35 | * Default: 60. 36 | * This generally means you will collect at most one sample from 37 | * any given web request. The start time is staggered by Excimer 38 | * to ensure equal distribution and fair chance to all code, 39 | * including early on in the request. 40 | * - redis-host: Redis host to flush samples to. 41 | * Default: "127.0.0.1". 42 | * - redis-port: Redis port 43 | * Default: 6379. 44 | * - redis-timeout: The Redis socket timeout (in seconds) 45 | * Default: 0.1. 46 | * - statsd-host: [optional] StatsD host address (ip:port or hostname:port), 47 | * to report metrics about collection failures. 48 | * - statsd-prefix: For example `"MyApplication."`, prepended to 49 | * the `arclamp_client_error.` and `arclamp_client_discarded.` 50 | * counter metrics. 51 | * Default: "". 52 | */ 53 | final public static function collect( array $options = [] ) { 54 | $options += [ 55 | 'excimer-period' => 60, 56 | 'redis-host' => '127.0.0.1', 57 | 'redis-port' => 6379, 58 | 'redis-timeout' => 0.1, 59 | ]; 60 | 61 | if ( PHP_SAPI !== 'cli' && extension_loaded( 'excimer' ) ) { 62 | // Used for unconditional sampling of production web requests. 63 | self::excimerSetup( $options ); 64 | } 65 | } 66 | 67 | /** 68 | * Start Excimer sampling profiler in production. 69 | * 70 | * @param array $options 71 | */ 72 | final public static function excimerSetup( $options ) { 73 | // Keep the object in scope until the end of the request 74 | static $realProf; 75 | 76 | $realProf = new ExcimerProfiler; 77 | $realProf->setEventType( EXCIMER_REAL ); 78 | $realProf->setPeriod( 60 ); 79 | // Limit the depth of stack traces to 250 (T176916) 80 | $realProf->setMaxDepth( 250 ); 81 | $realProf->setFlushCallback( 82 | static function ( $log ) use ( $options ) { 83 | $logLines = explode( "\n", $log->formatCollapsed() ); 84 | $redisChannel = 'excimer'; 85 | self::excimerFlushToArclamp( $logLines, $options, $redisChannel ); 86 | }, 87 | /* $maxSamples = */ 1 88 | ); 89 | $realProf->start(); 90 | } 91 | 92 | /** 93 | * Flush callback, called any time Excimer samples a stack trace in production. 94 | * 95 | * @param string[] $logLines Result of ExcimerLog::formatCollapsed() 96 | * @param array $options 97 | * @param string $redisChannel 98 | */ 99 | public static function excimerFlushToArclamp( $logLines, $options, $redisChannel ) { 100 | $error = null; 101 | try { 102 | $redis = new Redis(); 103 | $ok = $redis->connect( 104 | $options['redis-host'], 105 | $options['redis-port'], 106 | $options['redis-timeout'] 107 | ); 108 | if ( !$ok ) { 109 | $error = 'connect_error'; 110 | } else { 111 | $firstFrame = realpath( $_SERVER['SCRIPT_FILENAME'] ) . ';'; 112 | foreach ( $logLines as $line ) { 113 | if ( $line === '' ) { 114 | // formatCollapsed() ends with a line break 115 | continue; 116 | } 117 | 118 | // There are two ways a stack trace may be missing the first few frames: 119 | // 120 | // 1. Destructor callbacks, as of PHP 7.2, may be formatted as 121 | // "LBFactory::__destruct;LBFactory::LBFactory::shutdown;… 1" 122 | // 2. Stack traces that are longer than the configured maxDepth, will be 123 | // missing their top-most frames in favour of excimer_truncated (T176916) 124 | // 125 | // Arc Lamp requires the top frame to be the PHP entry point file. 126 | // If the first frame isn't the expected entry point, prepend it. 127 | // This check includes the semicolon to avoid false positives. 128 | if ( substr( $line, 0, strlen( $firstFrame ) ) !== $firstFrame ) { 129 | $line = $firstFrame . $line; 130 | } 131 | $redis->publish( $redisChannel, $line ); 132 | } 133 | } 134 | } catch ( \Exception $e ) { 135 | // Known failure scenarios: 136 | // 137 | // - "RedisException: read error on connection" 138 | // Each publish() in the above loop writes data to Redis and 139 | // subsequently reads from the socket for Redis' response. 140 | // If any socket read takes longer than $timeout, it throws (T206092). 141 | // As of writing, this is rare (a few times per day at most), 142 | // which is considered an acceptable loss in profile samples. 143 | $error = 'exception'; 144 | } 145 | 146 | if ( $error ) { 147 | $dest = $options['statsd-host'] ?? null; 148 | $prefix = $options['statsd-prefix'] ?? ''; 149 | if ( $dest ) { 150 | $sock = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); 151 | if ( $error ) { 152 | $stat = $prefix . "arclamp_client_error.{$error}:1|c"; 153 | @socket_sendto( $sock, $stat, strlen( $stat ), 0, $dest, 8125 ); 154 | } 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /arclamp-grep.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | arclamp-grep -- analyze Arc Lamp logs. This is a CLI tool for parsing trace 5 | logs and printing a leaderboard of the functions which are most 6 | frequently on-CPU. 7 | 8 | usage: arclamp-grep [--resolution TIME] [--entrypoint NAME] 9 | [--grep STRING] [--slice SLICE] [--count COUNT] 10 | [--channel CHANNEL] 11 | 12 | Options: 13 | --resolution TIME Which log files to analyze. May be one of 'hourly', 14 | 'daily', or 'weekly'. (Default: 'daily'). 15 | 16 | --entrypoint NAME Analyze logs for this entry point. May be one of 17 | 'all', 'index', 'api', or 'load'). (Default: 'all'). 18 | 19 | --grep STRING Only include stacks which include this string 20 | 21 | --count COUNT Show the top COUNT entries. (Default: 20). 22 | 23 | --slice SLICE Slice of files to analyze, in Python slice notation. 24 | Files are ordered from oldest to newest, so 25 | '--slice=-2:' means the two most recent files. 26 | --channel CHANNEL Which channel to look at. defaults to "xenon" 27 | 28 | Copyright 2015 Ori Livneh 29 | 30 | Licensed under the Apache License, Version 2.0 (the "License"); 31 | you may not use this file except in compliance with the License. 32 | You may obtain a copy of the License at 33 | 34 | http://www.apache.org/licenses/LICENSE-2.0 35 | 36 | Unless required by applicable law or agreed to in writing, software 37 | distributed under the License is distributed on an "AS IS" BASIS, 38 | WITHOUT WARRANTIES OR CONDITIONS OF ANY CODE, either express or implied. 39 | See the License for the specific language governing permissions and 40 | limitations under the License. 41 | 42 | """ 43 | import argparse 44 | import collections 45 | import fnmatch 46 | import glob 47 | import gzip 48 | import operator 49 | import os.path 50 | import re 51 | import sys 52 | import textwrap 53 | 54 | # Stack frames which match any of these shell-style wildcard patterns 55 | # are excluded from the leaderboard. 56 | SKIP_PATTERNS = ('*BagOStuff*', '*Http::exec*', '*ObjectCache*', '/srv*', 57 | 'AutoLoader*', 'Curl*', 'Database*', 'Hooks*', 'Http::*', 58 | 'LoadBalancer*', 'Memcached*', 'wfGetDB*') 59 | RESET = '\033[0m' 60 | YELLOW = '\033[93m' 61 | 62 | 63 | def slicer(spec): 64 | args = re.match('(-?[0-9]+)?(?::(-?[0-9]+))?', spec).groups() 65 | args = [int(arg) if arg is not None else arg for arg in args] 66 | return lambda seq: operator.getitem(seq, slice(*args)) 67 | 68 | 69 | def should_skip(f): 70 | return f.lower() == f or any(fnmatch.fnmatch(f, p) for p in SKIP_PATTERNS) 71 | 72 | 73 | def parse_line(line): 74 | line = re.sub(r'\d\.\d\dwmf\d+', 'X.XXwmfXX', line.rstrip()) 75 | funcs, count = line.split(' ', 1) 76 | return funcs.split(';'), int(count) 77 | 78 | 79 | def grep(fname, search_string): 80 | if fname.endswith(".gz"): 81 | opener = gzip.open 82 | else: 83 | opener = open 84 | 85 | with opener(fname) as f: 86 | for line in f: 87 | if search_string in line: 88 | yield line 89 | 90 | 91 | def iter_funcs(files): 92 | for fname in files: 93 | for line in grep(fname, args.grep): 94 | funcs, count = parse_line(line) 95 | while funcs and should_skip(funcs[-1]): 96 | funcs.pop() 97 | if funcs: 98 | func = funcs.pop() 99 | for _ in range(count): 100 | yield func 101 | 102 | 103 | if {'-h', '--help'}.intersection(sys.argv): 104 | sys.exit(textwrap.dedent(__doc__)) 105 | 106 | arg_parser = argparse.ArgumentParser(add_help=False) 107 | arg_parser.add_argument( 108 | '--resolution', 109 | default='daily', 110 | choices=('hourly', 'daily', 'weekly'), 111 | ) 112 | arg_parser.add_argument( 113 | '--count', 114 | default=20, 115 | type=int, 116 | help='show this many entries', 117 | ) 118 | arg_parser.add_argument( 119 | '--entrypoint', 120 | choices=('all', 'index', 'api', 'load'), 121 | default='all', 122 | ) 123 | arg_parser.add_argument( 124 | '--grep', 125 | default='', 126 | help='only include stacks which include this string', 127 | ) 128 | arg_parser.add_argument( 129 | '--slice', 130 | default='-2:', 131 | help='slice of files to consider', 132 | type=slicer, 133 | ) 134 | arg_parser.add_argument( 135 | '--channel', 136 | default='xenon', 137 | help='What channel to look at', 138 | choices=['xenon', 'excimer'], 139 | ) 140 | args = arg_parser.parse_args() 141 | 142 | # Legacy: the 'xenon' channel has a generic filename for now. 143 | if args.channel == 'xenon': 144 | glob_pattern = '/srv/arclamp/logs/%(resolution)s/*.%(entrypoint)s.log*' 145 | else: 146 | glob_pattern = '/srv/arclamp/logs/%(resolution)s/*.%(channel)s.%(entrypoint)s.log*' 147 | file_names = glob.glob(glob_pattern % vars(args)) 148 | file_names.sort(key=os.path.getmtime) 149 | file_names = args.slice(file_names) 150 | file_names = [fn for fn in file_names if fn.endswith(".log.gz") or fn.endswith(".log")] 151 | counter = collections.Counter(iter_funcs(file_names)) 152 | total = sum(1 for _ in counter.elements()) 153 | 154 | max_len = max(len(f) for f, _ in counter.most_common(args.count)) 155 | 156 | desc = 'Top %d functions' % args.count 157 | if args.grep: 158 | desc += ' in traces matching "%s"' % args.grep 159 | if args.entrypoint == 'all': 160 | desc += ', all entry-points:' 161 | else: 162 | desc += ', %s.php:' % args.entrypoint 163 | 164 | print(desc) 165 | print('-' * len(desc)) 166 | 167 | for idx, (func, count) in enumerate(counter.most_common(args.count)): 168 | ordinal = idx + 1 169 | percent = 100.0 * count / total 170 | func = YELLOW + (('%% -%ds' % max_len) % func) + RESET 171 | print('% 4d | %s |% 5.2f%%' % (ordinal, func, percent)) 172 | 173 | print('-' * len(desc)) 174 | print('Log files:') 175 | for f in file_names: 176 | print(' - %s' % f) 177 | print('') 178 | -------------------------------------------------------------------------------- /arclamp-log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | arclamp-log 5 | ~~~~~~~~~ 6 | 7 | This Arc Lamp components reads strack traces from a Redis channel, 8 | and writes them to one or more log files, organised by date 9 | and application entry point. 10 | 11 | """ 12 | from __future__ import print_function 13 | 14 | import argparse 15 | import datetime 16 | import errno 17 | import fnmatch 18 | import os 19 | import os.path 20 | import re 21 | import sys 22 | 23 | import redis 24 | import yaml 25 | 26 | 27 | # arclamp-log will exit with an error if TIMEOUT_SECS elapse with no new 28 | # messages arriving. This ensures that arclamp-log is restarted if the 29 | # connection to redis is interrupted (see T215740). It is set to a high value 30 | # (30m) to avoid crash-looping on the beta cluster, where profiler samples are 31 | # infrequent. 32 | # 33 | # TODO: Use health-checks instead, once available in our version of redis-py. 34 | # See . 35 | TIMEOUT_SECS = 60 * 30 36 | 37 | parser = argparse.ArgumentParser() 38 | # Configuration keys: 39 | # 40 | # base_path: Directory in which files should be created. [optional] 41 | # Default: "/srv/arclamp/logs". 42 | # 43 | # redis: Parameters to establish a connection using python-redis. 44 | # host: [required] 45 | # port: [required] 46 | # 47 | # redis_channel: The name of the Redis PubSub channel to subscribe to. [optional] 48 | # Default: "arclamp". 49 | # 50 | # logs: A list of one or more log file groups. [required] 51 | # 52 | # Each log group has a date-time string that informs how much time a 53 | # single file in the group represents (e.g. an hour or a day), and what 54 | # pattern to use for the file name. 55 | # 56 | # format: Format string for Python strftime. This informs both the 57 | # time aggregation and the filename. 58 | # 59 | # The formatted time and the suffix ".{tag}.log" together 60 | # form the log file name. The "tag" represents the application 61 | # entry point. All traces are also written to a second file, 62 | # with the tag "all", which combines all entry points. 63 | # The tag is determined by the first frame of the stack trace. 64 | # For example, a stack "index.php;main;Stuff::doIt 1" will be 65 | # written to "{format}.all.log" and "{format}.index.log". 66 | # 67 | # period: Directory name. The files formatted by 'format' will be 68 | # placed in a sub directory of 'base_path' by this name. 69 | # retain: How many files to keep in the 'period' directory for 70 | # a single application entry point. Once this has been exceeded, 71 | # files exceeding this limit will be removed (oldest first). 72 | # 73 | parser.add_argument('config', nargs='?', default='/etc/arclamp-log.yaml') 74 | args = parser.parse_args() 75 | 76 | with open(args.config) as f: 77 | config = yaml.safe_load(f) 78 | 79 | 80 | class TimeLog(object): 81 | 82 | base_path = config.get('base_path', '/srv/arclamp/logs') 83 | 84 | def __init__(self, period, format, retain, sample_pop=1): 85 | self.period = period 86 | self.format = format 87 | self.retain = retain 88 | self.sample_pop = sample_pop 89 | self.samples_skipped = 0 90 | self.path = os.path.join(self.base_path, period) 91 | try: 92 | os.makedirs(self.path, 0o755) 93 | except OSError as exc: 94 | if exc.errno != errno.EEXIST: 95 | raise 96 | 97 | def in_sample(self): 98 | self.samples_skipped += 1 99 | if self.samples_skipped >= self.sample_pop: 100 | self.samples_skipped = 0 101 | return True 102 | else: 103 | return False 104 | 105 | def write(self, message, time=None, tag='all'): 106 | time = datetime.datetime.utcnow() if time is None else time 107 | base_name = '%s.%s.log' % (time.strftime(self.format), tag) 108 | file_path = os.path.join(self.path, base_name) 109 | if not os.path.isfile(file_path): 110 | self.prune_files(tag) 111 | # T169249 buffering=1 makes it line-buffered 112 | with open(file_path, mode='ab', buffering=1) as f: 113 | f.write(message) 114 | f.write(b"\n") 115 | 116 | def prune_files(self, tag): 117 | mask = '*.%s.log*' % tag 118 | files = {} 119 | for base_name in os.listdir(self.path): 120 | if not fnmatch.fnmatch(base_name, mask): 121 | continue 122 | if not base_name.endswith(".log") and not base_name.endswith(".log.gz"): 123 | continue 124 | file_path = os.path.join(self.path, base_name) 125 | try: 126 | files[file_path] = os.path.getmtime(file_path) 127 | except ValueError: 128 | continue 129 | files = list(sorted(files, key=files.get, reverse=True)) 130 | for file_path in files[self.retain:]: 131 | try: 132 | os.remove(file_path) 133 | except OSError: 134 | continue 135 | 136 | 137 | logs = [TimeLog(**log) for log in config['logs']] 138 | conn = redis.Redis(**config['redis']) 139 | pubsub = conn.pubsub() 140 | pubsub.subscribe(config.get('redis_channel', 'arclamp')) 141 | 142 | 143 | def get_tag(raw_stack): 144 | m = re.match(r'(?:[^;]+/)*(\w+).php', raw_stack) 145 | return m.group(1) if m else None 146 | 147 | 148 | while True: 149 | message = pubsub.get_message(timeout=TIMEOUT_SECS) 150 | 151 | if message is None: 152 | raise RuntimeError("Timed out while waiting for message.") 153 | 154 | # T169249 skip the subscription confirmation message 155 | if message['type'] != 'message': 156 | continue 157 | 158 | data = message['data'] 159 | time = datetime.datetime.utcnow() 160 | tag = get_tag(str(data)) 161 | for log in logs: 162 | if log.in_sample(): 163 | log.write(data, time, 'all') 164 | if tag: 165 | log.write(data, time, tag) 166 | -------------------------------------------------------------------------------- /arclamp-generate-metrics: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # -e indicates this script should abort if any command does not exit 4 | # with status 0. To debug this, you can print commands to stderr as 5 | # they are executed, by running: `bash -x arclamp-generate-metrics`. 6 | set -e 7 | set -u 8 | shopt -s globstar nullglob 9 | 10 | function print_metric() { 11 | local metric="$1" 12 | local period="$2" 13 | local profiler="$3" 14 | local entry="$4" 15 | local value="$5" 16 | local compressed="$6" 17 | 18 | if [ -z "$value" ]; then 19 | return 20 | fi 21 | 22 | # Sanitize values which come from filesystem 23 | period=`sed -E 's/[^a-zA-Z0-9_.-]/_/g' <<< "$period"` 24 | profiler=`sed -E 's/[^a-zA-Z0-9_.-]/_/g' <<< "$profiler"` 25 | entry=`sed -E 's/[^a-zA-Z0-9_.-]/_/g' <<< "$entry"` 26 | 27 | if [ -n "$compressed" ]; then 28 | echo "$metric{period=\"$period\",profiler=\"$profiler\",entry=\"$entry\",compressed=\"$compressed\"} $value" 29 | else 30 | echo "$metric{period=\"$period\",profiler=\"$profiler\",entry=\"$entry\"} $value" 31 | fi 32 | } 33 | 34 | function update_metrics() { 35 | local data_dir="$1" 36 | local tmpout=`mktemp -t arclamp-metrics.XXXXXXXXXX` 37 | 38 | local periods=`find $data_dir/logs -mindepth 1 -maxdepth 1 -type d -printf '%f '` 39 | local profilers=`find $data_dir/logs -type f | awk 'match($0, /\.([^\.]+)\.[^\.]+\.log(\.|$)/, a) {print a[1];}' | sort -u` 40 | local entries=`find $data_dir/logs -type f | awk 'match($0, /\.([^\.]+)\.log(\.|$)/, a) {print a[1];}' | sort -u` 41 | 42 | echo "# HELP arclamp_oldest_log_seconds Timestamp of oldest input file" >>$tmpout 43 | echo "# TYPE arclamp_oldest_log_seconds gauge" >>$tmpout 44 | for period in $periods; do 45 | for profiler in $profilers; do 46 | for entry in $entries; do 47 | local oldest=`find $data_dir/logs/$period -name "*$profiler.$entry.log" -type f -printf '%T@\n' 2>/dev/null | sort | head -1` 48 | print_metric arclamp_oldest_log_seconds "$period" "$profiler" "$entry" "$oldest" "no" >>$tmpout 49 | oldest=`find $data_dir/logs/$period -name "*$profiler.$entry.log.gz" -type f -printf '%T@\n' 2>/dev/null | sort | head -1` 50 | print_metric arclamp_oldest_log_seconds "$period" "$profiler" "$entry" "$oldest" "yes" >>$tmpout 51 | done 52 | done 53 | done 54 | 55 | echo "# HELP arclamp_newest_log_seconds Timestamp of newest input file" >>$tmpout 56 | echo "# TYPE arclamp_newest_log_seconds gauge" >>$tmpout 57 | for period in $periods; do 58 | for profiler in $profilers; do 59 | for entry in $entries; do 60 | local newest=`find $data_dir/logs/$period -name "*$profiler.$entry.log" -type f -printf '%T@\n' 2>/dev/null | sort | tail -1` 61 | print_metric arclamp_newest_log_seconds "$period" "$profiler" "$entry" "$newest" "no" >>$tmpout 62 | newest=`find $data_dir/logs/$period -name "*$profiler.$entry.log.gz" -type f -printf '%T@\n' 2>/dev/null | sort | tail -1` 63 | print_metric arclamp_newest_log_seconds "$period" "$profiler" "$entry" "$newest" "yes" >>$tmpout 64 | done 65 | done 66 | done 67 | 68 | local oldest_tmp=`find $data_dir/logs -name "*.log.gz.tmp" -type f -printf '%T@\n' 2>/dev/null | sort | head -1` 69 | if [ -z "$oldest_tmp" ]; then 70 | # Pretend there's a file with the current timestamp, so that 71 | # Prometheus won't interpolate from the last reported value. 72 | # (We plan to alert if temporary file age is over a certain 73 | # threshold.) 74 | oldest_tmp=`date +%s.0` 75 | fi 76 | echo "# HELP arclamp_oldest_temporary_file_seconds Timestamp of oldest temporary file" >>$tmpout 77 | echo "# TYPE arclamp_oldest_temporary_file_seconds gauge" >>$tmpout 78 | echo "arclamp_oldest_temporary_file_seconds $oldest_tmp" >>$tmpout 79 | 80 | echo "# HELP arclamp_oldest_svg_seconds Timestamp of oldest output file" >>$tmpout 81 | echo "# TYPE arclamp_oldest_svg_seconds gauge" >>$tmpout 82 | for period in $periods; do 83 | for profiler in $profilers; do 84 | for entry in $entries; do 85 | local oldest=`find $data_dir/svgs/$period -name "*$profiler.$entry.*svgz" -type f -printf '%T@\n' 2>/dev/null | sort | head -1` 86 | print_metric arclamp_oldest_svg_seconds "$period" "$profiler" "$entry" "$oldest" "" >>$tmpout 87 | done 88 | done 89 | done 90 | 91 | echo "# HELP arclamp_newest_svg_seconds Timestamp of newest output file" >>$tmpout 92 | echo "# TYPE arclamp_newest_svg_seconds gauge" >>$tmpout 93 | for period in $periods; do 94 | for profiler in $profilers; do 95 | for entry in $entries; do 96 | local newest=`find $data_dir/svgs/$period -name "*$profiler.$entry.*svgz" -type f -size +21c -printf '%T@\n' 2>/dev/null | sort | tail -1` 97 | print_metric arclamp_newest_svg_seconds "$period" "$profiler" "$entry" "$newest" "" >>$tmpout 98 | done 99 | done 100 | done 101 | 102 | echo "# HELP arclamp_truncated_svg_files_total Number of output files 20 bytes or smaller" >>$tmpout 103 | echo "# TYPE arclamp_truncated_svg_files_total gauge" >>$tmpout 104 | for period in $periods; do 105 | for profiler in $profilers; do 106 | for entry in $entries; do 107 | local count=`find $data_dir/svgs/$period -name "*$profiler.$entry.*svgz" -type f -size -21c | wc -l` 108 | print_metric arclamp_truncated_svg_files_total "$period" "$profiler" "$entry" "$count" "" >>$tmpout 109 | done 110 | done 111 | done 112 | 113 | echo "# HELP arclamp_log_bytes_total Size of input files" >>$tmpout 114 | echo "# TYPE arclamp_log_bytes_total gauge" >>$tmpout 115 | for period in $periods; do 116 | local bytes=`du -sb $data_dir/logs/$period 2>/dev/null | cut -f1` 117 | if [ -n "$bytes" ]; then 118 | period=`sed -E 's/[^a-zA-Z0-9_.-]/_/g' <<< "$period"` 119 | echo "arclamp_log_bytes_total{period=\"$period\"} $bytes" >>$tmpout 120 | fi 121 | done 122 | 123 | echo "# HELP arclamp_svg_bytes_total Size of output files" >>$tmpout 124 | echo "# TYPE arclamp_svg_bytes_total gauge" >>$tmpout 125 | for period in $periods; do 126 | local bytes=`du -sb $data_dir/svgs/$period 2>/dev/null | cut -f1` 127 | if [ -n "$bytes" ]; then 128 | period=`sed -E 's/[^a-zA-Z0-9_.-]/_/g' <<< "$period"` 129 | echo "arclamp_svg_bytes_total{period=\"$period\"} $bytes" >>$tmpout 130 | fi 131 | done 132 | 133 | mv $tmpout $data_dir/metrics 134 | chmod 644 $data_dir/metrics 135 | } 136 | 137 | # Delete any old temporary files from failed runs: 138 | find $(dirname $(mktemp -u)) -name 'arclamp-metrics.*' -mmin 60 -type f -delete 2>/dev/null || true 139 | 140 | update_metrics /srv/arclamp 141 | -------------------------------------------------------------------------------- /arclamp-generate-svgs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Prefer flamegraph.pl in current directory. If not found, look in PATH. 4 | flamegraph=`dirname $0`/flamegraph.pl 5 | if [ ! -x "$flamegraph" ]; then 6 | flamegraph=`which flamegraph.pl` 7 | if [ ! -x "$flamegraph" ]; then 8 | echo "$0: cannot execute flamegraph.pl" >&2 9 | exit 1 10 | fi 11 | fi 12 | 13 | # -e indicates this script should abort if any command does not exit 14 | # with status 0. To debug this, you can print commands to stderr as 15 | # they are executed, by running: `bash -x arclamp-generate-svgs`. 16 | set -e 17 | set -u 18 | set +C # OK to clobber out-of-date SVGs 19 | shopt -s globstar nullglob 20 | 21 | function exit_if_already_running() { 22 | local pid_file="$1/arclamp-generate-svgs.pid" 23 | local other_pid=$(expr $(cat $pid_file 2>/dev/null) + 0 || true) 24 | if [ -d "/proc/${other_pid:-0}" ]; then 25 | echo "arclamp-generate-svgs is already running (PID $other_pid), exiting" 26 | exit 0 27 | fi 28 | echo $$ >$pid_file 29 | } 30 | 31 | function svg_path_for_log_and_fqfn_label() { 32 | # Log file, e.g. /srv/arclamp/logs/INTERVAL/YYYY-MM-DD.excimer.ENTRY.log 33 | local log_path="$1" 34 | # Stable label for a fully-qualified function name or "{main}" 35 | local function_label="$2" 36 | # Either "forward" or "reversed" 37 | local call_flow="$3" 38 | 39 | local svg_path 40 | svg_path="$(sed -E -e 's#/logs/#/svgs/#g' -e 's#\.log(\.gz)?$##' <<<"$log_path")" 41 | if [ "$function_label" != "{main}" ]; then 42 | svg_path+=".fn-${function_label}" 43 | fi 44 | if [ "$call_flow" != 'forward' ]; then 45 | svg_path+=".${call_flow}" 46 | fi 47 | svg_path+=".svgz" 48 | 49 | # SVG file, e.g. /srv/arclamp/svgs/INTERVAL/YYYY-MM-DD.excimer.ENTRY(.fn-FUNCTION)?(.reversed.)?.svgz 50 | echo "$svg_path" 51 | } 52 | 53 | function source_log_path_for_svg() { 54 | # SVG file, e.g. /srv/arclamp/svgs/INTERVAL/YYYY-MM-DD.excimer.ENTRY(.fn-FUNCTION)?(.reversed.)?.svgz 55 | local svg_path="$1" 56 | 57 | local log_path 58 | log_path=$(sed -e 's#/svgs/#/logs/#g' -e 's#\(\.fn-[^./]*\)\?\(\.reversed\)\?\.svgz\?$##' <<<"$svg_path") 59 | log_path+=".log" 60 | 61 | # Log file, e.g. /srv/arclamp/logs/INTERVAL/YYYY-MM-DD.excimer.ENTRY.log 62 | echo "$log_path" 63 | } 64 | 65 | function stream_log_lines_mentioning_fqfn() { 66 | local log="$1" 67 | local fqfn="$2" 68 | 69 | local cmd_prefix="" 70 | case "$log" in 71 | *.gz) 72 | cmd_prefix="z" 73 | ;; 74 | esac 75 | 76 | if [ "$fqfn" == "{main}" ]; then 77 | ${cmd_prefix}cat "$log" 78 | else 79 | ${cmd_prefix}grep -F -a ";${fqfn};" "$log" 80 | fi 81 | } 82 | 83 | function update_log_svgs_for_fqfn() { 84 | local log="$1" 85 | local fqfn="$2" 86 | local function_label="$3" 87 | 88 | local period time ftitle fsvg rsvg 89 | period="$(basename "$(dirname "$log")")" 90 | time="$(basename "$log" | sed -E 's/\.log(\.gz)?$//')" 91 | ftitle="MediaWiki - ${period} - ${time/_/ }" 92 | rtitle="$ftitle - reversed" 93 | fsvg="$(svg_path_for_log_and_fqfn_label "$log" "$function_label" "forward")" 94 | rsvg="$(svg_path_for_log_and_fqfn_label "$log" "$function_label" "reversed")" 95 | 96 | # Bail if both flamegraphs are already up to date 97 | if [ -s "$fsvg" ] && [ "$fsvg" -nt "$log" ] && [ -s "$rsvg" ] && [ "$rsvg" -nt "$log" ]; then 98 | # If flamegraph.pl crashes, it will leave a 20-byte file behind (T259167). 99 | # We want those to be re-generated. 100 | fsize=$(stat -c%s "$fsvg") 101 | rsize=$(stat -c%s "$rsvg") 102 | if [ "$fsize" -gt 20 ] && [ "$rsize" -gt 20 ]; then 103 | return 104 | fi 105 | fi 106 | # shellcheck disable=SC2174 107 | mkdir -m0755 -p "$(dirname "$fsvg")" "$(dirname "$rsvg")" 108 | logsize=$(stat -c%s "$log") 109 | if [ "$logsize" -lt 100000000 ]; then 110 | echo "Generating $fsvg and $rsvg..." 111 | # Scan the log file once, feeding the same output to forward and reverse flamegraph processes 112 | stream_log_lines_mentioning_fqfn "$log" "$fqfn" | tee \ 113 | >(ifne sh -c "$flamegraph --minwidth=1 --title=\"$ftitle\" | gzip -9 >\"$fsvg\"") \ 114 | | ifne sh -c "$flamegraph --minwidth=1 --reverse --colors=blue --title=\"$rtitle\" | gzip -9 >\"$rsvg\"" 115 | else 116 | # Large files need to be processed serially to reduced RAM usage; see T259167. 117 | echo "Generating $fsvg..." 118 | stream_log_lines_mentioning_fqfn "$log" "$fqfn" | \ 119 | ifne sh -c "$flamegraph --minwidth=1 --title=\"$ftitle\" | gzip -9 >\"$fsvg\"" 120 | echo "Generating $rsvg..." 121 | stream_log_lines_mentioning_fqfn "$log" "$fqfn" | \ 122 | ifne sh -c "$flamegraph --minwidth=1 --reverse --colors=blue --title=\"$rtitle\" | gzip -9 >\"$rsvg\"" 123 | fi 124 | 125 | if [ -n "$ST_AUTH" -a -x /usr/bin/swift ]; then 126 | [ ! -s "$fsvg" ] || swift upload arclamp-svgs-$period "$fsvg" --object-name `basename "$fsvg"` -H content-encoding:gzip 127 | [ ! -s "$rsvg" ] || swift upload arclamp-svgs-$period "$rsvg" --object-name `basename "$rsvg"` -H content-encoding:gzip 128 | fi 129 | } 130 | 131 | function update_svgs_for_existing_logs() { 132 | local data_dir="$1" 133 | 134 | declare -A label_by_fqfn_with_dedicated_svgs=( 135 | [EditAction::show]="EditAction" 136 | [MediaWiki\\MediaWikiEntryPoint::prepareForOutput]="PreSend" 137 | [MediaWiki\\MediaWikiEntryPoint::doPostOutputShutdown]="PostSend" 138 | [UploadBase::performUpload]="Upload" 139 | ) 140 | 141 | while IFS= read -r -d $'\0' log; do 142 | # T243762: It's possible for the log to be removed between when 143 | # the shell glob is calculated and when all files are processed. 144 | [ -r "$log" ] || continue 145 | # Overall forward+reverse SVGs for this log file 146 | update_log_svgs_for_fqfn "${log}" "{main}" "{main}" 147 | # Function-specific forward+reverse SVGs for this log file 148 | case $log in *.all.log) 149 | for fqfn in "${!label_by_fqfn_with_dedicated_svgs[@]}"; do 150 | update_log_svgs_for_fqfn "${log}" "${fqfn}" "${label_by_fqfn_with_dedicated_svgs[$fqfn]}" 151 | done 152 | ;; 153 | esac 154 | done < <(find "$data_dir/logs" -type f '(' -name '*.log' -o -name '*.log.gz' ')' -print0 | shuf -z) 155 | } 156 | 157 | function delete_svgs_for_nonexistant_logs() { 158 | local data_dir="$1" 159 | 160 | local log 161 | 162 | for svgz in "$data_dir"/**/*.svgz; do 163 | log="$(source_log_path_for_svg "$svgz")" 164 | [ ! -f "$log" -a ! -f "$log.gz" ] && rm -f "$svgz" || true 165 | done 166 | } 167 | 168 | exit_if_already_running /srv/arclamp 169 | update_svgs_for_existing_logs /srv/arclamp 170 | delete_svgs_for_nonexistant_logs /srv/arclamp 171 | rm -f /srv/arclamp/arclamp-generate-svgs.pid 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Files in this distribution other than flamegraph.pl are licensed under the 2 | Apache License, version 2.0. 3 | 4 | flamegraph.pl is licensed under the CDDL; see docs/cddl1.txt. 5 | 6 | -------------------------------------------------------------------- 7 | 8 | Apache License 9 | Version 2.0, January 2004 10 | http://www.apache.org/licenses/ 11 | 12 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 13 | 14 | 1. Definitions. 15 | 16 | "License" shall mean the terms and conditions for use, reproduction, 17 | and distribution as defined by Sections 1 through 9 of this document. 18 | 19 | "Licensor" shall mean the copyright owner or entity authorized by 20 | the copyright owner that is granting the License. 21 | 22 | "Legal Entity" shall mean the union of the acting entity and all 23 | other entities that control, are controlled by, or are under common 24 | control with that entity. For the purposes of this definition, 25 | "control" means (i) the power, direct or indirect, to cause the 26 | direction or management of such entity, whether by contract or 27 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 28 | outstanding shares, or (iii) beneficial ownership of such entity. 29 | 30 | "You" (or "Your") shall mean an individual or Legal Entity 31 | exercising permissions granted by this License. 32 | 33 | "Source" form shall mean the preferred form for making modifications, 34 | including but not limited to software source code, documentation 35 | source, and configuration files. 36 | 37 | "Object" form shall mean any form resulting from mechanical 38 | transformation or translation of a Source form, including but 39 | not limited to compiled object code, generated documentation, 40 | and conversions to other media types. 41 | 42 | "Work" shall mean the work of authorship, whether in Source or 43 | Object form, made available under the License, as indicated by a 44 | copyright notice that is included in or attached to the work 45 | (an example is provided in the Appendix below). 46 | 47 | "Derivative Works" shall mean any work, whether in Source or Object 48 | form, that is based on (or derived from) the Work and for which the 49 | editorial revisions, annotations, elaborations, or other modifications 50 | represent, as a whole, an original work of authorship. For the purposes 51 | of this License, Derivative Works shall not include works that remain 52 | separable from, or merely link (or bind by name) to the interfaces of, 53 | the Work and Derivative Works thereof. 54 | 55 | "Contribution" shall mean any work of authorship, including 56 | the original version of the Work and any modifications or additions 57 | to that Work or Derivative Works thereof, that is intentionally 58 | submitted to Licensor for inclusion in the Work by the copyright owner 59 | or by an individual or Legal Entity authorized to submit on behalf of 60 | the copyright owner. For the purposes of this definition, "submitted" 61 | means any form of electronic, verbal, or written communication sent 62 | to the Licensor or its representatives, including but not limited to 63 | communication on electronic mailing lists, source code control systems, 64 | and issue tracking systems that are managed by, or on behalf of, the 65 | Licensor for the purpose of discussing and improving the Work, but 66 | excluding communication that is conspicuously marked or otherwise 67 | designated in writing by the copyright owner as "Not a Contribution." 68 | 69 | "Contributor" shall mean Licensor and any individual or Legal Entity 70 | on behalf of whom a Contribution has been received by Licensor and 71 | subsequently incorporated within the Work. 72 | 73 | 2. Grant of Copyright License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | copyright license to reproduce, prepare Derivative Works of, 77 | publicly display, publicly perform, sublicense, and distribute the 78 | Work and such Derivative Works in Source or Object form. 79 | 80 | 3. Grant of Patent License. Subject to the terms and conditions of 81 | this License, each Contributor hereby grants to You a perpetual, 82 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 83 | (except as stated in this section) patent license to make, have made, 84 | use, offer to sell, sell, import, and otherwise transfer the Work, 85 | where such license applies only to those patent claims licensable 86 | by such Contributor that are necessarily infringed by their 87 | Contribution(s) alone or by combination of their Contribution(s) 88 | with the Work to which such Contribution(s) was submitted. If You 89 | institute patent litigation against any entity (including a 90 | cross-claim or counterclaim in a lawsuit) alleging that the Work 91 | or a Contribution incorporated within the Work constitutes direct 92 | or contributory patent infringement, then any patent licenses 93 | granted to You under this License for that Work shall terminate 94 | as of the date such litigation is filed. 95 | 96 | 4. Redistribution. You may reproduce and distribute copies of the 97 | Work or Derivative Works thereof in any medium, with or without 98 | modifications, and in Source or Object form, provided that You 99 | meet the following conditions: 100 | 101 | (a) You must give any other recipients of the Work or 102 | Derivative Works a copy of this License; and 103 | 104 | (b) You must cause any modified files to carry prominent notices 105 | stating that You changed the files; and 106 | 107 | (c) You must retain, in the Source form of any Derivative Works 108 | that You distribute, all copyright, patent, trademark, and 109 | attribution notices from the Source form of the Work, 110 | excluding those notices that do not pertain to any part of 111 | the Derivative Works; and 112 | 113 | (d) If the Work includes a "NOTICE" text file as part of its 114 | distribution, then any Derivative Works that You distribute must 115 | include a readable copy of the attribution notices contained 116 | within such NOTICE file, excluding those notices that do not 117 | pertain to any part of the Derivative Works, in at least one 118 | of the following places: within a NOTICE text file distributed 119 | as part of the Derivative Works; within the Source form or 120 | documentation, if provided along with the Derivative Works; or, 121 | within a display generated by the Derivative Works, if and 122 | wherever such third-party notices normally appear. The contents 123 | of the NOTICE file are for informational purposes only and 124 | do not modify the License. You may add Your own attribution 125 | notices within Derivative Works that You distribute, alongside 126 | or as an addendum to the NOTICE text from the Work, provided 127 | that such additional attribution notices cannot be construed 128 | as modifying the License. 129 | 130 | You may add Your own copyright statement to Your modifications and 131 | may provide additional or different license terms and conditions 132 | for use, reproduction, or distribution of Your modifications, or 133 | for any such Derivative Works as a whole, provided Your use, 134 | reproduction, and distribution of the Work otherwise complies with 135 | the conditions stated in this License. 136 | 137 | 5. Submission of Contributions. Unless You explicitly state otherwise, 138 | any Contribution intentionally submitted for inclusion in the Work 139 | by You to the Licensor shall be under the terms and conditions of 140 | this License, without any additional terms or conditions. 141 | Notwithstanding the above, nothing herein shall supersede or modify 142 | the terms of any separate license agreement you may have executed 143 | with Licensor regarding such Contributions. 144 | 145 | 6. Trademarks. This License does not grant permission to use the trade 146 | names, trademarks, service marks, or product names of the Licensor, 147 | except as required for reasonable and customary use in describing the 148 | origin of the Work and reproducing the content of the NOTICE file. 149 | 150 | 7. Disclaimer of Warranty. Unless required by applicable law or 151 | agreed to in writing, Licensor provides the Work (and each 152 | Contributor provides its Contributions) on an "AS IS" BASIS, 153 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 154 | implied, including, without limitation, any warranties or conditions 155 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 156 | PARTICULAR PURPOSE. You are solely responsible for determining the 157 | appropriateness of using or redistributing the Work and assume any 158 | risks associated with Your exercise of permissions under this License. 159 | 160 | 8. Limitation of Liability. In no event and under no legal theory, 161 | whether in tort (including negligence), contract, or otherwise, 162 | unless required by applicable law (such as deliberate and grossly 163 | negligent acts) or agreed to in writing, shall any Contributor be 164 | liable to You for damages, including any direct, indirect, special, 165 | incidental, or consequential damages of any character arising as a 166 | result of this License or out of the use or inability to use the 167 | Work (including but not limited to damages for loss of goodwill, 168 | work stoppage, computer failure or malfunction, or any and all 169 | other commercial damages or losses), even if such Contributor 170 | has been advised of the possibility of such damages. 171 | 172 | 9. Accepting Warranty or Additional Liability. While redistributing 173 | the Work or Derivative Works thereof, You may choose to offer, 174 | and charge a fee for, acceptance of support, warranty, indemnity, 175 | or other liability obligations and/or rights consistent with this 176 | License. However, in accepting such obligations, You may act only 177 | on Your own behalf and on Your sole responsibility, not on behalf 178 | of any other Contributor, and only if You agree to indemnify, 179 | defend, and hold each Contributor harmless for any liability 180 | incurred by, or claims asserted against, such Contributor by reason 181 | of your accepting any such warranty or additional liability. 182 | 183 | END OF TERMS AND CONDITIONS 184 | 185 | APPENDIX: How to apply the Apache License to your work. 186 | 187 | To apply the Apache License to your work, attach the following 188 | boilerplate notice, with the fields enclosed by brackets "{}" 189 | replaced with your own identifying information. (Don't include 190 | the brackets!) The text should be enclosed in the appropriate 191 | comment syntax for the file format. We also recommend that a 192 | file or class name and description of purpose be included on the 193 | same "printed page" as the copyright notice for easier 194 | identification within third-party archives. 195 | 196 | Copyright {yyyy} {name of copyright owner} 197 | 198 | Licensed under the Apache License, Version 2.0 (the "License"); 199 | you may not use this file except in compliance with the License. 200 | You may obtain a copy of the License at 201 | 202 | http://www.apache.org/licenses/LICENSE-2.0 203 | 204 | Unless required by applicable law or agreed to in writing, software 205 | distributed under the License is distributed on an "AS IS" BASIS, 206 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 207 | See the License for the specific language governing permissions and 208 | limitations under the License. 209 | 210 | -------------------------------------------------------------------------------- /docs/cddl1.txt: -------------------------------------------------------------------------------- 1 | flamegraph.pl was downloaded from https://github.com/brendangregg/FlameGraph 2 | and is licensed under the CDDL license version 1.0. 3 | 4 | The remaining files in this distribution are licensed under the Apache 5 | License, version 2.0. See LICENSE in the parent directory. 6 | 7 | -------------------------------------------------------------------- 8 | 9 | COMMON DEVELOPMENT AND DISTRIBUTION LICENSE Version 1.0 10 | 11 | 1. Definitions. 12 | 13 | 1.1. "Contributor" means each individual or entity that creates 14 | or contributes to the creation of Modifications. 15 | 16 | 1.2. "Contributor Version" means the combination of the Original 17 | Software, prior Modifications used by a Contributor (if any), 18 | and the Modifications made by that particular Contributor. 19 | 20 | 1.3. "Covered Software" means (a) the Original Software, or (b) 21 | Modifications, or (c) the combination of files containing 22 | Original Software with files containing Modifications, in 23 | each case including portions thereof. 24 | 25 | 1.4. "Executable" means the Covered Software in any form other 26 | than Source Code. 27 | 28 | 1.5. "Initial Developer" means the individual or entity that first 29 | makes Original Software available under this License. 30 | 31 | 1.6. "Larger Work" means a work which combines Covered Software or 32 | portions thereof with code not governed by the terms of this 33 | License. 34 | 35 | 1.7. "License" means this document. 36 | 37 | 1.8. "Licensable" means having the right to grant, to the maximum 38 | extent possible, whether at the time of the initial grant or 39 | subsequently acquired, any and all of the rights conveyed 40 | herein. 41 | 42 | 1.9. "Modifications" means the Source Code and Executable form of 43 | any of the following: 44 | 45 | A. Any file that results from an addition to, deletion from or 46 | modification of the contents of a file containing Original 47 | Software or previous Modifications; 48 | 49 | B. Any new file that contains any part of the Original 50 | Software or previous Modifications; or 51 | 52 | C. Any new file that is contributed or otherwise made 53 | available under the terms of this License. 54 | 55 | 1.10. "Original Software" means the Source Code and Executable 56 | form of computer software code that is originally released 57 | under this License. 58 | 59 | 1.11. "Patent Claims" means any patent claim(s), now owned or 60 | hereafter acquired, including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by 62 | grantor. 63 | 64 | 1.12. "Source Code" means (a) the common form of computer software 65 | code in which modifications are made and (b) associated 66 | documentation included in or with such code. 67 | 68 | 1.13. "You" (or "Your") means an individual or a legal entity 69 | exercising rights under, and complying with all of the terms 70 | of, this License. For legal entities, "You" includes any 71 | entity which controls, is controlled by, or is under common 72 | control with You. For purposes of this definition, 73 | "control" means (a) the power, direct or indirect, to cause 74 | the direction or management of such entity, whether by 75 | contract or otherwise, or (b) ownership of more than fifty 76 | percent (50%) of the outstanding shares or beneficial 77 | ownership of such entity. 78 | 79 | 2. License Grants. 80 | 81 | 2.1. The Initial Developer Grant. 82 | 83 | Conditioned upon Your compliance with Section 3.1 below and 84 | subject to third party intellectual property claims, the Initial 85 | Developer hereby grants You a world-wide, royalty-free, 86 | non-exclusive license: 87 | 88 | (a) under intellectual property rights (other than patent or 89 | trademark) Licensable by Initial Developer, to use, 90 | reproduce, modify, display, perform, sublicense and 91 | distribute the Original Software (or portions thereof), 92 | with or without Modifications, and/or as part of a Larger 93 | Work; and 94 | 95 | (b) under Patent Claims infringed by the making, using or 96 | selling of Original Software, to make, have made, use, 97 | practice, sell, and offer for sale, and/or otherwise 98 | dispose of the Original Software (or portions thereof). 99 | 100 | (c) The licenses granted in Sections 2.1(a) and (b) are 101 | effective on the date Initial Developer first distributes 102 | or otherwise makes the Original Software available to a 103 | third party under the terms of this License. 104 | 105 | (d) Notwithstanding Section 2.1(b) above, no patent license is 106 | granted: (1) for code that You delete from the Original 107 | Software, or (2) for infringements caused by: (i) the 108 | modification of the Original Software, or (ii) the 109 | combination of the Original Software with other software 110 | or devices. 111 | 112 | 2.2. Contributor Grant. 113 | 114 | Conditioned upon Your compliance with Section 3.1 below and 115 | subject to third party intellectual property claims, each 116 | Contributor hereby grants You a world-wide, royalty-free, 117 | non-exclusive license: 118 | 119 | (a) under intellectual property rights (other than patent or 120 | trademark) Licensable by Contributor to use, reproduce, 121 | modify, display, perform, sublicense and distribute the 122 | Modifications created by such Contributor (or portions 123 | thereof), either on an unmodified basis, with other 124 | Modifications, as Covered Software and/or as part of a 125 | Larger Work; and 126 | 127 | (b) under Patent Claims infringed by the making, using, or 128 | selling of Modifications made by that Contributor either 129 | alone and/or in combination with its Contributor Version 130 | (or portions of such combination), to make, use, sell, 131 | offer for sale, have made, and/or otherwise dispose of: 132 | (1) Modifications made by that Contributor (or portions 133 | thereof); and (2) the combination of Modifications made by 134 | that Contributor with its Contributor Version (or portions 135 | of such combination). 136 | 137 | (c) The licenses granted in Sections 2.2(a) and 2.2(b) are 138 | effective on the date Contributor first distributes or 139 | otherwise makes the Modifications available to a third 140 | party. 141 | 142 | (d) Notwithstanding Section 2.2(b) above, no patent license is 143 | granted: (1) for any code that Contributor has deleted 144 | from the Contributor Version; (2) for infringements caused 145 | by: (i) third party modifications of Contributor Version, 146 | or (ii) the combination of Modifications made by that 147 | Contributor with other software (except as part of the 148 | Contributor Version) or other devices; or (3) under Patent 149 | Claims infringed by Covered Software in the absence of 150 | Modifications made by that Contributor. 151 | 152 | 3. Distribution Obligations. 153 | 154 | 3.1. Availability of Source Code. 155 | 156 | Any Covered Software that You distribute or otherwise make 157 | available in Executable form must also be made available in Source 158 | Code form and that Source Code form must be distributed only under 159 | the terms of this License. You must include a copy of this 160 | License with every copy of the Source Code form of the Covered 161 | Software You distribute or otherwise make available. You must 162 | inform recipients of any such Covered Software in Executable form 163 | as to how they can obtain such Covered Software in Source Code 164 | form in a reasonable manner on or through a medium customarily 165 | used for software exchange. 166 | 167 | 3.2. Modifications. 168 | 169 | The Modifications that You create or to which You contribute are 170 | governed by the terms of this License. You represent that You 171 | believe Your Modifications are Your original creation(s) and/or 172 | You have sufficient rights to grant the rights conveyed by this 173 | License. 174 | 175 | 3.3. Required Notices. 176 | 177 | You must include a notice in each of Your Modifications that 178 | identifies You as the Contributor of the Modification. You may 179 | not remove or alter any copyright, patent or trademark notices 180 | contained within the Covered Software, or any notices of licensing 181 | or any descriptive text giving attribution to any Contributor or 182 | the Initial Developer. 183 | 184 | 3.4. Application of Additional Terms. 185 | 186 | You may not offer or impose any terms on any Covered Software in 187 | Source Code form that alters or restricts the applicable version 188 | of this License or the recipients' rights hereunder. You may 189 | choose to offer, and to charge a fee for, warranty, support, 190 | indemnity or liability obligations to one or more recipients of 191 | Covered Software. However, you may do so only on Your own behalf, 192 | and not on behalf of the Initial Developer or any Contributor. 193 | You must make it absolutely clear that any such warranty, support, 194 | indemnity or liability obligation is offered by You alone, and You 195 | hereby agree to indemnify the Initial Developer and every 196 | Contributor for any liability incurred by the Initial Developer or 197 | such Contributor as a result of warranty, support, indemnity or 198 | liability terms You offer. 199 | 200 | 3.5. Distribution of Executable Versions. 201 | 202 | You may distribute the Executable form of the Covered Software 203 | under the terms of this License or under the terms of a license of 204 | Your choice, which may contain terms different from this License, 205 | provided that You are in compliance with the terms of this License 206 | and that the license for the Executable form does not attempt to 207 | limit or alter the recipient's rights in the Source Code form from 208 | the rights set forth in this License. If You distribute the 209 | Covered Software in Executable form under a different license, You 210 | must make it absolutely clear that any terms which differ from 211 | this License are offered by You alone, not by the Initial 212 | Developer or Contributor. You hereby agree to indemnify the 213 | Initial Developer and every Contributor for any liability incurred 214 | by the Initial Developer or such Contributor as a result of any 215 | such terms You offer. 216 | 217 | 3.6. Larger Works. 218 | 219 | You may create a Larger Work by combining Covered Software with 220 | other code not governed by the terms of this License and 221 | distribute the Larger Work as a single product. In such a case, 222 | You must make sure the requirements of this License are fulfilled 223 | for the Covered Software. 224 | 225 | 4. Versions of the License. 226 | 227 | 4.1. New Versions. 228 | 229 | Sun Microsystems, Inc. is the initial license steward and may 230 | publish revised and/or new versions of this License from time to 231 | time. Each version will be given a distinguishing version number. 232 | Except as provided in Section 4.3, no one other than the license 233 | steward has the right to modify this License. 234 | 235 | 4.2. Effect of New Versions. 236 | 237 | You may always continue to use, distribute or otherwise make the 238 | Covered Software available under the terms of the version of the 239 | License under which You originally received the Covered Software. 240 | If the Initial Developer includes a notice in the Original 241 | Software prohibiting it from being distributed or otherwise made 242 | available under any subsequent version of the License, You must 243 | distribute and make the Covered Software available under the terms 244 | of the version of the License under which You originally received 245 | the Covered Software. Otherwise, You may also choose to use, 246 | distribute or otherwise make the Covered Software available under 247 | the terms of any subsequent version of the License published by 248 | the license steward. 249 | 250 | 4.3. Modified Versions. 251 | 252 | When You are an Initial Developer and You want to create a new 253 | license for Your Original Software, You may create and use a 254 | modified version of this License if You: (a) rename the license 255 | and remove any references to the name of the license steward 256 | (except to note that the license differs from this License); and 257 | (b) otherwise make it clear that the license contains terms which 258 | differ from this License. 259 | 260 | 5. DISCLAIMER OF WARRANTY. 261 | 262 | COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" 263 | BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, 264 | INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED 265 | SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR 266 | PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND 267 | PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY 268 | COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE 269 | INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY 270 | NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF 271 | WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF 272 | ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS 273 | DISCLAIMER. 274 | 275 | 6. TERMINATION. 276 | 277 | 6.1. This License and the rights granted hereunder will terminate 278 | automatically if You fail to comply with terms herein and fail to 279 | cure such breach within 30 days of becoming aware of the breach. 280 | Provisions which, by their nature, must remain in effect beyond 281 | the termination of this License shall survive. 282 | 283 | 6.2. If You assert a patent infringement claim (excluding 284 | declaratory judgment actions) against Initial Developer or a 285 | Contributor (the Initial Developer or Contributor against whom You 286 | assert such claim is referred to as "Participant") alleging that 287 | the Participant Software (meaning the Contributor Version where 288 | the Participant is a Contributor or the Original Software where 289 | the Participant is the Initial Developer) directly or indirectly 290 | infringes any patent, then any and all rights granted directly or 291 | indirectly to You by such Participant, the Initial Developer (if 292 | the Initial Developer is not the Participant) and all Contributors 293 | under Sections 2.1 and/or 2.2 of this License shall, upon 60 days 294 | notice from Participant terminate prospectively and automatically 295 | at the expiration of such 60 day notice period, unless if within 296 | such 60 day period You withdraw Your claim with respect to the 297 | Participant Software against such Participant either unilaterally 298 | or pursuant to a written agreement with Participant. 299 | 300 | 6.3. In the event of termination under Sections 6.1 or 6.2 above, 301 | all end user licenses that have been validly granted by You or any 302 | distributor hereunder prior to termination (excluding licenses 303 | granted to You by any distributor) shall survive termination. 304 | 305 | 7. LIMITATION OF LIABILITY. 306 | 307 | UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT 308 | (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE 309 | INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF 310 | COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE 311 | LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR 312 | CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT 313 | LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK 314 | STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER 315 | COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN 316 | INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF 317 | LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL 318 | INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT 319 | APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO 320 | NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR 321 | CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT 322 | APPLY TO YOU. 323 | 324 | 8. U.S. GOVERNMENT END USERS. 325 | 326 | The Covered Software is a "commercial item," as that term is 327 | defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial 328 | computer software" (as that term is defined at 48 329 | C.F.R. 252.227-7014(a)(1)) and "commercial computer software 330 | documentation" as such terms are used in 48 C.F.R. 12.212 331 | (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 332 | C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all 333 | U.S. Government End Users acquire Covered Software with only those 334 | rights set forth herein. This U.S. Government Rights clause is in 335 | lieu of, and supersedes, any other FAR, DFAR, or other clause or 336 | provision that addresses Government rights in computer software 337 | under this License. 338 | 339 | 9. MISCELLANEOUS. 340 | 341 | This License represents the complete agreement concerning subject 342 | matter hereof. If any provision of this License is held to be 343 | unenforceable, such provision shall be reformed only to the extent 344 | necessary to make it enforceable. This License shall be governed 345 | by the law of the jurisdiction specified in a notice contained 346 | within the Original Software (except to the extent applicable law, 347 | if any, provides otherwise), excluding such jurisdiction's 348 | conflict-of-law provisions. Any litigation relating to this 349 | License shall be subject to the jurisdiction of the courts located 350 | in the jurisdiction and venue specified in a notice contained 351 | within the Original Software, with the losing party responsible 352 | for costs, including, without limitation, court costs and 353 | reasonable attorneys' fees and expenses. The application of the 354 | United Nations Convention on Contracts for the International Sale 355 | of Goods is expressly excluded. Any law or regulation which 356 | provides that the language of a contract shall be construed 357 | against the drafter shall not apply to this License. You agree 358 | that You alone are responsible for compliance with the United 359 | States export administration regulations (and the export control 360 | laws and regulation of any other countries) when You use, 361 | distribute or otherwise make available any Covered Software. 362 | 363 | 10. RESPONSIBILITY FOR CLAIMS. 364 | 365 | As between Initial Developer and the Contributors, each party is 366 | responsible for claims and damages arising, directly or 367 | indirectly, out of its utilization of rights under this License 368 | and You agree to work with Initial Developer and Contributors to 369 | distribute such responsibility on an equitable basis. Nothing 370 | herein is intended or shall be deemed to constitute any admission 371 | of liability. 372 | 373 | -------------------------------------------------------------------- 374 | 375 | NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND 376 | DISTRIBUTION LICENSE (CDDL) 377 | 378 | For Covered Software in this distribution, this License shall 379 | be governed by the laws of the State of California (excluding 380 | conflict-of-law provisions). 381 | 382 | Any litigation relating to this License shall be subject to the 383 | jurisdiction of the Federal Courts of the Northern District of 384 | California and the state courts of the State of California, with 385 | venue lying in Santa Clara County, California. 386 | -------------------------------------------------------------------------------- /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 for Linux perf script output using 21 | # stackcollapse-perf.pl, for DTrace using stackcollapse.pl, and for other tools 22 | # using the other stackcollapse programs. Example input: 23 | # 24 | # swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 1 25 | # 26 | # An optional extra column of counts can be provided to generate a differential 27 | # flame graph of the counts, colored red for more, and blue for less. This 28 | # can be useful when using flame graphs for non-regression testing. 29 | # See the header comment in the difffolded.pl program for instructions. 30 | # 31 | # The input functions can optionally have annotations at the end of each 32 | # function name, following a precedent by some tools (Linux perf's _[k]): 33 | # _[k] for kernel 34 | # _[i] for inlined 35 | # _[j] for jit 36 | # _[w] for waker 37 | # Some of the stackcollapse programs support adding these annotations, eg, 38 | # stackcollapse-perf.pl --kernel --jit. They are used merely for colors by 39 | # some palettes, eg, flamegraph.pl --color=java. 40 | # 41 | # The output flame graph shows relative presence of functions in stack samples. 42 | # The ordering on the x-axis has no meaning; since the data is samples, time 43 | # order of events is not known. The order used sorts function names 44 | # alphabetically. 45 | # 46 | # While intended to process stack samples, this can also process stack traces. 47 | # For example, tracing stacks for memory allocation, or resource usage. You 48 | # can use --title to set the title to reflect the content, and --countname 49 | # to change "samples" to "bytes" etc. 50 | # 51 | # There are a few different palettes, selectable using --color. By default, 52 | # the colors are selected at random (except for differentials). Functions 53 | # called "-" will be printed gray, which can be used for stack separators (eg, 54 | # between user and kernel stacks). 55 | # 56 | # HISTORY 57 | # 58 | # This was inspired by Neelakanth Nadgir's excellent function_call_graph.rb 59 | # program, which visualized function entry and return trace events. As Neel 60 | # wrote: "The output displayed is inspired by Roch's CallStackAnalyzer which 61 | # was in turn inspired by the work on vftrace by Jan Boerhout". See: 62 | # https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and 63 | # 64 | # Copyright 2016 Netflix, Inc. 65 | # Copyright 2011 Joyent, Inc. All rights reserved. 66 | # Copyright 2011 Brendan Gregg. All rights reserved. 67 | # 68 | # CDDL HEADER START 69 | # 70 | # The contents of this file are subject to the terms of the 71 | # Common Development and Distribution License (the "License"). 72 | # You may not use this file except in compliance with the License. 73 | # 74 | # You can obtain a copy of the license at docs/cddl1.txt or 75 | # http://opensource.org/licenses/CDDL-1.0. 76 | # See the License for the specific language governing permissions 77 | # and limitations under the License. 78 | # 79 | # When distributing Covered Code, include this CDDL HEADER in each 80 | # file and include the License file at docs/cddl1.txt. 81 | # If applicable, add the following below this CDDL HEADER, with the 82 | # fields enclosed by brackets "[]" replaced with your own identifying 83 | # information: Portions Copyright [yyyy] [name of copyright owner] 84 | # 85 | # CDDL HEADER END 86 | # 87 | # 11-Oct-2014 Adrien Mahieux Added zoom. 88 | # 21-Nov-2013 Shawn Sterling Added consistent palette file option 89 | # 17-Mar-2013 Tim Bunce Added options and more tunables. 90 | # 15-Dec-2011 Dave Pacheco Support for frames with whitespace. 91 | # 10-Sep-2011 Brendan Gregg Created this. 92 | 93 | use strict; 94 | 95 | use Getopt::Long; 96 | 97 | use open qw(:std :utf8); 98 | 99 | # tunables 100 | my $encoding; 101 | my $fonttype = "Verdana"; 102 | my $imagewidth = 1200; # max width, pixels 103 | my $frameheight = 16; # max height is dynamic 104 | my $fontsize = 12; # base text size 105 | my $fontwidth = 0.59; # avg width relative to fontsize 106 | my $minwidth = 0.1; # min function width, pixels 107 | my $nametype = "Function:"; # what are the names in the data? 108 | my $countname = "samples"; # what are the counts in the data? 109 | my $colors = "hot"; # color theme 110 | my $bgcolors = ""; # background color theme 111 | my $nameattrfile; # file holding function attributes 112 | my $timemax; # (override the) sum of the counts 113 | my $factor = 1; # factor to scale counts by 114 | my $hash = 0; # color by function name 115 | my $palette = 0; # if we use consistent palettes (default off) 116 | my %palette_map; # palette map hash 117 | my $pal_file = "palette.map"; # palette map file name 118 | my $stackreverse = 0; # reverse stack order, switching merge end 119 | my $inverted = 0; # icicle graph 120 | my $flamechart = 0; # produce a flame chart (sort by time, do not merge stacks) 121 | my $negate = 0; # switch differential hues 122 | my $titletext = ""; # centered heading 123 | my $titledefault = "Flame Graph"; # overwritten by --title 124 | my $titleinverted = "Icicle Graph"; # " " 125 | my $searchcolor = "rgb(230,0,230)"; # color for search highlighting 126 | my $notestext = ""; # embedded notes in SVG 127 | my $subtitletext = ""; # second level title (optional) 128 | my $help = 0; 129 | 130 | sub usage { 131 | die < outfile.svg\n 133 | --title TEXT # change title text 134 | --subtitle TEXT # second level title (optional) 135 | --width NUM # width of image (default 1200) 136 | --height NUM # height of each frame (default 16) 137 | --minwidth NUM # omit smaller functions (default 0.1 pixels) 138 | --fonttype FONT # font type (default "Verdana") 139 | --fontsize NUM # font size (default 12) 140 | --countname TEXT # count type label (default "samples") 141 | --nametype TEXT # name type label (default "Function:") 142 | --colors PALETTE # set color palette. choices are: hot (default), mem, 143 | # io, wakeup, chain, java, js, perl, red, green, blue, 144 | # aqua, yellow, purple, orange 145 | --bgcolors COLOR # set background colors. gradient choices are yellow 146 | # (default), blue, green, grey; flat colors use "#rrggbb" 147 | --hash # colors are keyed by function name hash 148 | --cp # use consistent palette (palette.map) 149 | --reverse # generate stack-reversed flame graph 150 | --inverted # icicle graph 151 | --flamechart # produce a flame chart (sort by time, do not merge stacks) 152 | --negate # switch differential hues (blue<->red) 153 | --notes TEXT # add notes comment in SVG (for debugging) 154 | --help # this message 155 | 156 | eg, 157 | $0 --title="Flame Graph: malloc()" trace.txt > graph.svg 158 | USAGE_END 159 | } 160 | 161 | GetOptions( 162 | 'fonttype=s' => \$fonttype, 163 | 'width=i' => \$imagewidth, 164 | 'height=i' => \$frameheight, 165 | 'encoding=s' => \$encoding, 166 | 'fontsize=f' => \$fontsize, 167 | 'fontwidth=f' => \$fontwidth, 168 | 'minwidth=f' => \$minwidth, 169 | 'title=s' => \$titletext, 170 | 'subtitle=s' => \$subtitletext, 171 | 'nametype=s' => \$nametype, 172 | 'countname=s' => \$countname, 173 | 'nameattr=s' => \$nameattrfile, 174 | 'total=s' => \$timemax, 175 | 'factor=f' => \$factor, 176 | 'colors=s' => \$colors, 177 | 'bgcolors=s' => \$bgcolors, 178 | 'hash' => \$hash, 179 | 'cp' => \$palette, 180 | 'reverse' => \$stackreverse, 181 | 'inverted' => \$inverted, 182 | 'flamechart' => \$flamechart, 183 | 'negate' => \$negate, 184 | 'notes=s' => \$notestext, 185 | 'help' => \$help, 186 | ) or usage(); 187 | $help && usage(); 188 | 189 | # internals 190 | my $ypad1 = $fontsize * 3; # pad top, include title 191 | my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels 192 | my $ypad3 = $fontsize * 2; # pad top, include subtitle (optional) 193 | my $xpad = 10; # pad lefm and right 194 | my $framepad = 1; # vertical padding for frames 195 | my $depthmax = 0; 196 | my %Events; 197 | my %nameattr; 198 | 199 | if ($flamechart && $titletext eq "") { 200 | $titletext = "Flame Chart"; 201 | } 202 | 203 | if ($titletext eq "") { 204 | unless ($inverted) { 205 | $titletext = $titledefault; 206 | } else { 207 | $titletext = $titleinverted; 208 | } 209 | } 210 | 211 | if ($nameattrfile) { 212 | # The name-attribute file format is a function name followed by a tab then 213 | # a sequence of tab separated name=value pairs. 214 | open my $attrfh, $nameattrfile or die "Can't read $nameattrfile: $!\n"; 215 | while (<$attrfh>) { 216 | chomp; 217 | my ($funcname, $attrstr) = split /\t/, $_, 2; 218 | die "Invalid format in $nameattrfile" unless defined $attrstr; 219 | $nameattr{$funcname} = { map { split /=/, $_, 2 } split /\t/, $attrstr }; 220 | } 221 | } 222 | 223 | if ($notestext =~ /[<>]/) { 224 | die "Notes string can't contain < or >" 225 | } 226 | 227 | # background colors: 228 | # - yellow gradient: default (hot, java, js, perl) 229 | # - green gradient: mem 230 | # - blue gradient: io, wakeup, chain 231 | # - gray gradient: flat colors (red, green, blue, ...) 232 | if ($bgcolors eq "") { 233 | # choose a default 234 | if ($colors eq "mem") { 235 | $bgcolors = "green"; 236 | } elsif ($colors =~ /^(io|wakeup|chain)$/) { 237 | $bgcolors = "blue"; 238 | } elsif ($colors =~ /^(red|green|blue|aqua|yellow|purple|orange)$/) { 239 | $bgcolors = "grey"; 240 | } else { 241 | $bgcolors = "yellow"; 242 | } 243 | } 244 | my ($bgcolor1, $bgcolor2); 245 | if ($bgcolors eq "yellow") { 246 | $bgcolor1 = "#eeeeee"; # background color gradient start 247 | $bgcolor2 = "#eeeeb0"; # background color gradient stop 248 | } elsif ($bgcolors eq "blue") { 249 | $bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff"; 250 | } elsif ($bgcolors eq "green") { 251 | $bgcolor1 = "#eef2ee"; $bgcolor2 = "#e0ffe0"; 252 | } elsif ($bgcolors eq "grey") { 253 | $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8"; 254 | } elsif ($bgcolors =~ /^#......$/) { 255 | $bgcolor1 = $bgcolor2 = $bgcolors; 256 | } else { 257 | die "Unrecognized bgcolor option \"$bgcolors\"" 258 | } 259 | 260 | # SVG functions 261 | { package SVG; 262 | sub new { 263 | my $class = shift; 264 | my $self = {}; 265 | bless ($self, $class); 266 | return $self; 267 | } 268 | 269 | sub header { 270 | my ($self, $w, $h) = @_; 271 | my $enc_attr = ''; 272 | if (defined $encoding) { 273 | $enc_attr = qq{ encoding="$encoding"}; 274 | } 275 | $self->{svg} .= < 277 | 278 | 279 | 280 | 281 | SVG 282 | } 283 | 284 | sub include { 285 | my ($self, $content) = @_; 286 | $self->{svg} .= $content; 287 | } 288 | 289 | sub colorAllocate { 290 | my ($self, $r, $g, $b) = @_; 291 | return "rgb($r,$g,$b)"; 292 | } 293 | 294 | sub group_start { 295 | my ($self, $attr) = @_; 296 | 297 | my @g_attr = map { 298 | exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : () 299 | } qw(id class); 300 | push @g_attr, $attr->{g_extra} if $attr->{g_extra}; 301 | if ($attr->{href}) { 302 | my @a_attr; 303 | push @a_attr, sprintf qq/xlink:href="%s"/, $attr->{href} if $attr->{href}; 304 | # default target=_top else links will open within SVG 305 | push @a_attr, sprintf qq/target="%s"/, $attr->{target} || "_top"; 306 | push @a_attr, $attr->{a_extra} if $attr->{a_extra}; 307 | $self->{svg} .= sprintf qq/\n/, join(' ', (@a_attr, @g_attr)); 308 | } else { 309 | $self->{svg} .= sprintf qq/\n/, join(' ', @g_attr); 310 | } 311 | 312 | $self->{svg} .= sprintf qq/%s<\/title>/, $attr->{title} 313 | if $attr->{title}; # should be first element within g container 314 | } 315 | 316 | sub group_end { 317 | my ($self, $attr) = @_; 318 | $self->{svg} .= $attr->{href} ? qq/<\/a>\n/ : qq/<\/g>\n/; 319 | } 320 | 321 | sub filledRectangle { 322 | my ($self, $x1, $y1, $x2, $y2, $fill, $extra) = @_; 323 | $x1 = sprintf "%0.1f", $x1; 324 | $x2 = sprintf "%0.1f", $x2; 325 | my $w = sprintf "%0.1f", $x2 - $x1; 326 | my $h = sprintf "%0.1f", $y2 - $y1; 327 | $extra = defined $extra ? $extra : ""; 328 | $self->{svg} .= qq/\n/; 329 | } 330 | 331 | sub stringTTF { 332 | my ($self, $id, $x, $y, $str, $extra) = @_; 333 | $x = sprintf "%0.2f", $x; 334 | $id = defined $id ? qq/id="$id"/ : ""; 335 | $extra ||= ""; 336 | $self->{svg} .= qq/$str<\/text>\n/; 337 | } 338 | 339 | sub svg { 340 | my $self = shift; 341 | return "$self->{svg}\n"; 342 | } 343 | 1; 344 | } 345 | 346 | sub namehash { 347 | # Generate a vector hash for the name string, weighting early over 348 | # later characters. We want to pick the same colors for function 349 | # names across different flame graphs. 350 | my $name = shift; 351 | my $vector = 0; 352 | my $weight = 1; 353 | my $max = 1; 354 | my $mod = 10; 355 | # if module name present, trunc to 1st char 356 | $name =~ s/.(.*?)`//; 357 | foreach my $c (split //, $name) { 358 | my $i = (ord $c) % $mod; 359 | $vector += ($i / ($mod++ - 1)) * $weight; 360 | $max += 1 * $weight; 361 | $weight *= 0.70; 362 | last if $mod > 12; 363 | } 364 | return (1 - $vector / $max) 365 | } 366 | 367 | sub color { 368 | my ($type, $hash, $name) = @_; 369 | my ($v1, $v2, $v3); 370 | 371 | if ($hash) { 372 | $v1 = namehash($name); 373 | $v2 = $v3 = namehash(scalar reverse $name); 374 | } else { 375 | $v1 = rand(1); 376 | $v2 = rand(1); 377 | $v3 = rand(1); 378 | } 379 | 380 | # theme palettes 381 | if (defined $type and $type eq "hot") { 382 | my $r = 205 + int(50 * $v3); 383 | my $g = 0 + int(230 * $v1); 384 | my $b = 0 + int(55 * $v2); 385 | return "rgb($r,$g,$b)"; 386 | } 387 | if (defined $type and $type eq "mem") { 388 | my $r = 0; 389 | my $g = 190 + int(50 * $v2); 390 | my $b = 0 + int(210 * $v1); 391 | return "rgb($r,$g,$b)"; 392 | } 393 | if (defined $type and $type eq "io") { 394 | my $r = 80 + int(60 * $v1); 395 | my $g = $r; 396 | my $b = 190 + int(55 * $v2); 397 | return "rgb($r,$g,$b)"; 398 | } 399 | 400 | # multi palettes 401 | if (defined $type and $type eq "java") { 402 | # Handle both annotations (_[j], _[i], ...; which are 403 | # accurate), as well as input that lacks any annotations, as 404 | # best as possible. Without annotations, we get a little hacky 405 | # and match on java|org|com, etc. 406 | if ($name =~ m:_\[j\]$:) { # jit annotation 407 | $type = "green"; 408 | } elsif ($name =~ m:_\[i\]$:) { # inline annotation 409 | $type = "aqua"; 410 | } elsif ($name =~ m:^L?(java|javax|jdk|net|org|com|io|sun)/:) { # Java 411 | $type = "green"; 412 | } elsif ($name =~ /:::/) { # Java, typical perf-map-agent method separator 413 | $type = "green"; 414 | } elsif ($name =~ /::/) { # C++ 415 | $type = "yellow"; 416 | } elsif ($name =~ m:_\[k\]$:) { # kernel annotation 417 | $type = "orange"; 418 | } elsif ($name =~ /::/) { # C++ 419 | $type = "yellow"; 420 | } else { # system 421 | $type = "red"; 422 | } 423 | # fall-through to color palettes 424 | } 425 | if (defined $type and $type eq "perl") { 426 | if ($name =~ /::/) { # C++ 427 | $type = "yellow"; 428 | } elsif ($name =~ m:Perl: or $name =~ m:\.pl:) { # Perl 429 | $type = "green"; 430 | } elsif ($name =~ m:_\[k\]$:) { # kernel 431 | $type = "orange"; 432 | } else { # system 433 | $type = "red"; 434 | } 435 | # fall-through to color palettes 436 | } 437 | if (defined $type and $type eq "js") { 438 | # Handle both annotations (_[j], _[i], ...; which are 439 | # accurate), as well as input that lacks any annotations, as 440 | # best as possible. Without annotations, we get a little hacky, 441 | # and match on a "/" with a ".js", etc. 442 | if ($name =~ m:_\[j\]$:) { # jit annotation 443 | if ($name =~ m:/:) { 444 | $type = "green"; # source 445 | } else { 446 | $type = "aqua"; # builtin 447 | } 448 | } elsif ($name =~ /::/) { # C++ 449 | $type = "yellow"; 450 | } elsif ($name =~ m:/.*\.js:) { # JavaScript (match "/" in path) 451 | $type = "green"; 452 | } elsif ($name =~ m/:/) { # JavaScript (match ":" in builtin) 453 | $type = "aqua"; 454 | } elsif ($name =~ m/^ $/) { # Missing symbol 455 | $type = "green"; 456 | } elsif ($name =~ m:_\[k\]:) { # kernel 457 | $type = "orange"; 458 | } else { # system 459 | $type = "red"; 460 | } 461 | # fall-through to color palettes 462 | } 463 | if (defined $type and $type eq "wakeup") { 464 | $type = "aqua"; 465 | # fall-through to color palettes 466 | } 467 | if (defined $type and $type eq "chain") { 468 | if ($name =~ m:_\[w\]:) { # waker 469 | $type = "aqua" 470 | } else { # off-CPU 471 | $type = "blue"; 472 | } 473 | # fall-through to color palettes 474 | } 475 | 476 | # color palettes 477 | if (defined $type and $type eq "red") { 478 | my $r = 200 + int(55 * $v1); 479 | my $x = 50 + int(80 * $v1); 480 | return "rgb($r,$x,$x)"; 481 | } 482 | if (defined $type and $type eq "green") { 483 | my $g = 200 + int(55 * $v1); 484 | my $x = 50 + int(60 * $v1); 485 | return "rgb($x,$g,$x)"; 486 | } 487 | if (defined $type and $type eq "blue") { 488 | my $b = 205 + int(50 * $v1); 489 | my $x = 80 + int(60 * $v1); 490 | return "rgb($x,$x,$b)"; 491 | } 492 | if (defined $type and $type eq "yellow") { 493 | my $x = 175 + int(55 * $v1); 494 | my $b = 50 + int(20 * $v1); 495 | return "rgb($x,$x,$b)"; 496 | } 497 | if (defined $type and $type eq "purple") { 498 | my $x = 190 + int(65 * $v1); 499 | my $g = 80 + int(60 * $v1); 500 | return "rgb($x,$g,$x)"; 501 | } 502 | if (defined $type and $type eq "aqua") { 503 | my $r = 50 + int(60 * $v1); 504 | my $g = 165 + int(55 * $v1); 505 | my $b = 165 + int(55 * $v1); 506 | return "rgb($r,$g,$b)"; 507 | } 508 | if (defined $type and $type eq "orange") { 509 | my $r = 190 + int(65 * $v1); 510 | my $g = 90 + int(65 * $v1); 511 | return "rgb($r,$g,0)"; 512 | } 513 | 514 | return "rgb(0,0,0)"; 515 | } 516 | 517 | sub color_scale { 518 | my ($value, $max) = @_; 519 | my ($r, $g, $b) = (255, 255, 255); 520 | $value = -$value if $negate; 521 | if ($value > 0) { 522 | $g = $b = int(210 * ($max - $value) / $max); 523 | } elsif ($value < 0) { 524 | $r = $g = int(210 * ($max + $value) / $max); 525 | } 526 | return "rgb($r,$g,$b)"; 527 | } 528 | 529 | sub color_map { 530 | my ($colors, $func) = @_; 531 | if (exists $palette_map{$func}) { 532 | return $palette_map{$func}; 533 | } else { 534 | $palette_map{$func} = color($colors, $hash, $func); 535 | return $palette_map{$func}; 536 | } 537 | } 538 | 539 | sub write_palette { 540 | open(FILE, ">$pal_file"); 541 | foreach my $key (sort keys %palette_map) { 542 | print FILE $key."->".$palette_map{$key}."\n"; 543 | } 544 | close(FILE); 545 | } 546 | 547 | sub read_palette { 548 | if (-e $pal_file) { 549 | open(FILE, $pal_file) or die "can't open file $pal_file: $!"; 550 | while ( my $line = ) { 551 | chomp($line); 552 | (my $key, my $value) = split("->",$line); 553 | $palette_map{$key}=$value; 554 | } 555 | close(FILE) 556 | } 557 | } 558 | 559 | my %Node; # Hash of merged frame data 560 | my %Tmp; 561 | 562 | # flow() merges two stacks, storing the merged frames and value data in %Node. 563 | sub flow { 564 | my ($last, $this, $v, $d) = @_; 565 | 566 | my $len_a = @$last - 1; 567 | my $len_b = @$this - 1; 568 | 569 | my $i = 0; 570 | my $len_same; 571 | for (; $i <= $len_a; $i++) { 572 | last if $i > $len_b; 573 | last if $last->[$i] ne $this->[$i]; 574 | } 575 | $len_same = $i; 576 | 577 | for ($i = $len_a; $i >= $len_same; $i--) { 578 | my $k = "$last->[$i];$i"; 579 | # a unique ID is constructed from "func;depth;etime"; 580 | # func-depth isn't unique, it may be repeated later. 581 | $Node{"$k;$v"}->{stime} = delete $Tmp{$k}->{stime}; 582 | if (defined $Tmp{$k}->{delta}) { 583 | $Node{"$k;$v"}->{delta} = delete $Tmp{$k}->{delta}; 584 | } 585 | delete $Tmp{$k}; 586 | } 587 | 588 | for ($i = $len_same; $i <= $len_b; $i++) { 589 | my $k = "$this->[$i];$i"; 590 | $Tmp{$k}->{stime} = $v; 591 | if (defined $d) { 592 | $Tmp{$k}->{delta} += $i == $len_b ? $d : 0; 593 | } 594 | } 595 | 596 | return $this; 597 | } 598 | 599 | # parse input 600 | my @Data; 601 | my $last = []; 602 | my $time = 0; 603 | my $delta = undef; 604 | my $ignored = 0; 605 | my $line; 606 | my $entry; 607 | my $maxdelta = 1; 608 | 609 | # reverse if needed 610 | foreach (<>) { 611 | chomp; 612 | $line = $_; 613 | if ($stackreverse) { 614 | # there may be an extra samples column for differentials 615 | # XXX todo: redo these REs as one. It's repeated below. 616 | my($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 617 | my $samples2 = undef; 618 | if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { 619 | $samples2 = $samples; 620 | ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 621 | $entry = join(";", reverse split(";", $stack)) . " $samples $samples2"; 622 | } else { 623 | $entry = join(";", reverse split(";", $stack)) . " $samples"; 624 | } 625 | } else { 626 | $entry = $line; 627 | } 628 | 629 | if ( $flamechart ) { 630 | # TODO: why does this unshift and then reverse? just push instead? 631 | unshift @Data, $entry; 632 | } else { 633 | push @Data, $entry; 634 | } 635 | } 636 | 637 | 638 | if ($flamechart) { 639 | # In flame chart mode, just reverse the data so time moves from left to right. 640 | @Data = reverse @Data; 641 | } else { 642 | # Use in-place sort to reduce memory usage. 643 | # https://stackoverflow.com/a/24960868 644 | @Data = sort @Data; 645 | } 646 | 647 | # process and merge frames; 648 | # free up memory from @Data as %Node get populated 649 | while ($_ = shift @Data) { 650 | chomp; 651 | # process: folded_stack count 652 | # eg: func_a;func_b;func_c 31 653 | my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 654 | unless (defined $samples and defined $stack) { 655 | ++$ignored; 656 | next; 657 | } 658 | 659 | # there may be an extra samples column for differentials: 660 | my $samples2 = undef; 661 | if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { 662 | $samples2 = $samples; 663 | ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 664 | } 665 | $delta = undef; 666 | if (defined $samples2) { 667 | $delta = $samples2 - $samples; 668 | $maxdelta = abs($delta) if abs($delta) > $maxdelta; 669 | } 670 | 671 | # for chain graphs, annotate waker frames with "_[w]", for later 672 | # coloring. This is a hack, but has a precedent ("_[k]" from perf). 673 | if ($colors eq "chain") { 674 | my @parts = split ";--;", $stack; 675 | my @newparts = (); 676 | $stack = shift @parts; 677 | $stack .= ";--;"; 678 | foreach my $part (@parts) { 679 | $part =~ s/;/_[w];/g; 680 | $part .= "_[w]"; 681 | push @newparts, $part; 682 | } 683 | $stack .= join ";--;", @parts; 684 | } 685 | 686 | # merge frames and populate %Node: 687 | $last = flow($last, [ '', split ";", $stack ], $time, $delta); 688 | 689 | if (defined $samples2) { 690 | $time += $samples2; 691 | } else { 692 | $time += $samples; 693 | } 694 | } 695 | flow($last, [], $time, $delta); 696 | undef @Data; 697 | 698 | warn "Ignored $ignored lines with invalid format\n" if $ignored; 699 | unless ($time) { 700 | warn "ERROR: No stack counts found\n"; 701 | my $im = SVG->new(); 702 | # emit an error message SVG, for tools automating flamegraph use 703 | my $imageheight = $fontsize * 5; 704 | $im->header($imagewidth, $imageheight); 705 | $im->stringTTF(undef, int($imagewidth / 2), $fontsize * 2, 706 | "ERROR: No valid input provided to flamegraph.pl."); 707 | print $im->svg; 708 | exit 2; 709 | } 710 | if ($timemax and $timemax < $time) { 711 | warn "Specified --total $timemax is less than actual total $time, so ignored\n" 712 | if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc) 713 | undef $timemax; 714 | } 715 | $timemax ||= $time; 716 | 717 | my $widthpertime = ($imagewidth - 2 * $xpad) / $timemax; 718 | my $minwidth_time = $minwidth / $widthpertime; 719 | 720 | # prune blocks that are too narrow and determine max depth 721 | while (my ($id, $node) = each %Node) { 722 | my ($func, $depth, $etime) = split ";", $id; 723 | my $stime = $node->{stime}; 724 | die "missing start for $id" if not defined $stime; 725 | 726 | if (($etime-$stime) < $minwidth_time) { 727 | delete $Node{$id}; 728 | next; 729 | } 730 | $depthmax = $depth if $depth > $depthmax; 731 | } 732 | 733 | # draw canvas, and embed interactive JavaScript program 734 | my $imageheight = (($depthmax + 1) * $frameheight) + $ypad1 + $ypad2; 735 | $imageheight += $ypad3 if $subtitletext ne ""; 736 | my $titlesize = $fontsize + 5; 737 | my $im = SVG->new(); 738 | my ($black, $vdgrey, $dgrey) = ( 739 | $im->colorAllocate(0, 0, 0), 740 | $im->colorAllocate(160, 160, 160), 741 | $im->colorAllocate(200, 200, 200), 742 | ); 743 | $im->header($imagewidth, $imageheight); 744 | my $inc = < 746 | 747 | 748 | 749 | 750 | 751 | 762 | 1154 | INC 1155 | $im->include($inc); 1156 | $im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)'); 1157 | $im->stringTTF("title", int($imagewidth / 2), $fontsize * 2, $titletext); 1158 | $im->stringTTF("subtitle", int($imagewidth / 2), $fontsize * 4, $subtitletext) if $subtitletext ne ""; 1159 | $im->stringTTF("details", $xpad, $imageheight - ($ypad2 / 2), " "); 1160 | $im->stringTTF("unzoom", $xpad, $fontsize * 2, "Reset Zoom", 'class="hide"'); 1161 | $im->stringTTF("search", $imagewidth - $xpad - 100, $fontsize * 2, "Search"); 1162 | $im->stringTTF("ignorecase", $imagewidth - $xpad - 16, $fontsize * 2, "ic"); 1163 | $im->stringTTF("matched", $imagewidth - $xpad - 100, $imageheight - ($ypad2 / 2), " "); 1164 | 1165 | if ($palette) { 1166 | read_palette(); 1167 | } 1168 | 1169 | # draw frames 1170 | $im->group_start({id => "frames"}); 1171 | while (my ($id, $node) = each %Node) { 1172 | my ($func, $depth, $etime) = split ";", $id; 1173 | my $stime = $node->{stime}; 1174 | my $delta = $node->{delta}; 1175 | # free up memory as we go 1176 | delete $Node{$id}; 1177 | 1178 | $etime = $timemax if $func eq "" and $depth == 0; 1179 | 1180 | my $x1 = $xpad + $stime * $widthpertime; 1181 | my $x2 = $xpad + $etime * $widthpertime; 1182 | my ($y1, $y2); 1183 | unless ($inverted) { 1184 | $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad; 1185 | $y2 = $imageheight - $ypad2 - $depth * $frameheight; 1186 | } else { 1187 | $y1 = $ypad1 + $depth * $frameheight; 1188 | $y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad; 1189 | } 1190 | 1191 | my $samples = sprintf "%.0f", ($etime - $stime) * $factor; 1192 | (my $samples_txt = $samples) # add commas per perlfaq5 1193 | =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g; 1194 | 1195 | my $info; 1196 | if ($func eq "" and $depth == 0) { 1197 | $info = "all ($samples_txt $countname, 100%)"; 1198 | } else { 1199 | my $pct = sprintf "%.2f", ((100 * $samples) / ($timemax * $factor)); 1200 | my $escaped_func = $func; 1201 | # clean up SVG breaking characters: 1202 | $escaped_func =~ s/&/&/g; 1203 | $escaped_func =~ s//>/g; 1205 | $escaped_func =~ s/"/"/g; 1206 | $escaped_func =~ s/_\[[kwij]\]$//; # strip any annotation 1207 | unless (defined $delta) { 1208 | $info = "$escaped_func ($samples_txt $countname, $pct%)"; 1209 | } else { 1210 | my $d = $negate ? -$delta : $delta; 1211 | my $deltapct = sprintf "%.2f", ((100 * $d) / ($timemax * $factor)); 1212 | $deltapct = $d > 0 ? "+$deltapct" : $deltapct; 1213 | $info = "$escaped_func ($samples_txt $countname, $pct%; $deltapct%)"; 1214 | } 1215 | } 1216 | 1217 | my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone 1218 | $nameattr->{title} ||= $info; 1219 | $im->group_start($nameattr); 1220 | 1221 | my $color; 1222 | if ($func eq "--") { 1223 | $color = $vdgrey; 1224 | } elsif ($func eq "-") { 1225 | $color = $dgrey; 1226 | } elsif (defined $delta) { 1227 | $color = color_scale($delta, $maxdelta); 1228 | } elsif ($palette) { 1229 | $color = color_map($colors, $func); 1230 | } else { 1231 | $color = color($colors, $hash, $func); 1232 | } 1233 | $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"'); 1234 | 1235 | my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth)); 1236 | my $text = ""; 1237 | if ($chars >= 3) { # room for one char plus two dots 1238 | $func =~ s/_\[[kwij]\]$//; # strip any annotation 1239 | $text = substr $func, 0, $chars; 1240 | substr($text, -2, 2) = ".." if $chars < length $func; 1241 | $text =~ s/&/&/g; 1242 | $text =~ s//>/g; 1244 | } 1245 | $im->stringTTF(undef, $x1 + 3, 3 + ($y1 + $y2) / 2, $text); 1246 | 1247 | $im->group_end($nameattr); 1248 | } 1249 | $im->group_end(); 1250 | 1251 | print $im->svg; 1252 | 1253 | if ($palette) { 1254 | write_palette(); 1255 | } 1256 | 1257 | # vim: ts=8 sts=8 sw=8 noexpandtab 1258 | --------------------------------------------------------------------------------