#!/usr/bin/perl -w
#
# stackcolllapse-jstack.pl	collapse jstack samples into single lines.
#
# Parses Java stacks generated by jstack(1) and outputs RUNNABLE stacks as
# single lines, with methods separated by semicolons, and then a space and an
# occurrence count. This also filters some other "RUNNABLE" states that we
# know are probably not running, such as epollWait. For use with flamegraph.pl.
#
# You want this to process the output of at least 100 jstack(1)s. ie, run it
# 100 times with a sleep interval, and append to a file. This is really a poor
# man's Java profiler, due to the overheads of jstack(1), and how it isn't
# capturing stacks asynchronously. For a better profiler, see:
# http://www.brendangregg.com/blog/2014-06-12/java-flame-graphs.html
#
# USAGE: ./stackcollapse-jstack.pl infile > outfile
#
# Example input:
#
# "MyProg" #273 daemon prio=9 os_prio=0 tid=0x00007f273c038800 nid=0xe3c runnable [0x00007f28a30f2000]
#    java.lang.Thread.State: RUNNABLE
#        at java.net.SocketInputStream.socketRead0(Native Method)
#        at java.net.SocketInputStream.read(SocketInputStream.java:121)
#        ...
#        at java.lang.Thread.run(Thread.java:744)
#
# Example output:
#
#  MyProg;java.lang.Thread.run;java.net.SocketInputStream.read;java.net.SocketInputStream.socketRead0 1
#
# Input may be created and processed using:
#
#  i=0; while (( i++ < 200 )); do jstack PID >> out.jstacks; sleep 10; done
#  cat out.jstacks | ./stackcollapse-jstack.pl > out.stacks-folded
#
# WARNING: jstack(1) incurs overheads. Test before use, or use a real profiler.
#
# Copyright 2014 Brendan Gregg.  All rights reserved.
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
#  (http://www.gnu.org/copyleft/gpl.html)
#
# 14-Sep-2014	Brendan Gregg	Created this.

use strict;

my %collapsed;

sub remember_stack {
	my ($stack, $count) = @_;
	$collapsed{$stack} += $count;
}

my @stack;
my $pname;
my $include_pname = 1;		# include process names in stacks
my $include_tid = 0;		# include thread IDs in stacks
my $shorten_pkgs = 0;		# shorten package names
my $state = "?";

foreach (<>) {
	next if m/^#/;
	chomp;

	if (m/^$/) {
		# only include RUNNABLE states
		goto clear if $state ne "RUNNABLE";

		# save stack
		if (defined $pname) { unshift @stack, $pname; }
		remember_stack(join(";", @stack), 1) if @stack;
clear:
		undef @stack;
		undef $pname;
		$state = "?";
		next;
	}

	#
	# While parsing jstack output, the $state variable may be altered from
	# RUNNABLE to other states. This causes the stacks to be filtered later,
	# since only RUNNABLE stacks are included.
	#

	if (/^"([^"]*)/) {
		my $name = $1;

		if ($include_pname) {
			$pname = $name;
			unless ($include_tid) {
				$pname =~ s/-\d+$//;
			}
		}

		# set state for various background threads
		$state = "BACKGROUND" if $name =~ /C. CompilerThread/;
		$state = "BACKGROUND" if $name =~ /Signal Dispatcher/;
		$state = "BACKGROUND" if $name =~ /Service Thread/;
		$state = "BACKGROUND" if $name =~ /Attach Listener/;

	} elsif (/java.lang.Thread.State: (\S+)/) {
		$state = $1 if $state eq "?";
	} elsif (/^\s*at ([^\(]*)/) {
		my $func = $1;
		if ($shorten_pkgs) {
			my ($pkgs, $clsFunc) = ( $func =~ m/(.*\.)([^.]+\.[^.]+)$/ );
			$pkgs =~ s/(\w)\w*/$1/g;
			$func = $pkgs . $clsFunc;
		}
		unshift @stack, $func;

		# fix state for epollWait
		$state = "WAITING" if $func =~ /epollWait/;

		# fix state for various networking functions
		$state = "NETWORK" if $func =~ /socketAccept$/;
		$state = "NETWORK" if $func =~ /Socket.*accept0$/;
		$state = "NETWORK" if $func =~ /socketRead0$/;

	} elsif (/^\s*-/ or /^2\d\d\d-/ or /^Full thread dump/ or
		 /^JNI global references:/) {
		# skip these info lines
		next;
	} else {
		warn "Unrecognized line: $_";
	}
}

foreach my $k (sort { $a cmp $b } keys %collapsed) {
	print "$k $collapsed{$k}\n";
}
