mirror of
https://github.com/MariaDB/server.git
synced 2025-01-27 17:33:44 +01:00
10946 lines
391 KiB
Perl
10946 lines
391 KiB
Perl
#!/usr/bin/perl
|
|
|
|
# vim: tw=160:nowrap:expandtab:tabstop=3:shiftwidth=3:softtabstop=3
|
|
|
|
# This program is copyright (c) 2006 Baron Schwartz, baron at xaprb dot com.
|
|
# Feedback and improvements are gratefully received.
|
|
#
|
|
# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
|
|
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
|
|
# MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
|
#
|
|
# 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, version 2; OR the Perl Artistic License. On UNIX and similar
|
|
# systems, you can issue `man perlgpl' or `man perlartistic' to read these
|
|
|
|
# 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
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
|
|
our $VERSION = '1.7.1';
|
|
|
|
# Find the home directory; it's different on different OSes.
|
|
our $homepath = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.';
|
|
|
|
# Configuration files
|
|
our $default_home_conf = "$homepath/.innotop/innotop.conf";
|
|
our $default_central_conf = "/etc/innotop/innotop.conf";
|
|
our $conf_file = "";
|
|
|
|
## Begin packages ##
|
|
|
|
package DSNParser;
|
|
|
|
use DBI;
|
|
use Data::Dumper;
|
|
$Data::Dumper::Indent = 0;
|
|
$Data::Dumper::Quotekeys = 0;
|
|
use English qw(-no_match_vars);
|
|
|
|
use constant MKDEBUG => $ENV{MKDEBUG};
|
|
|
|
# Defaults are built-in, but you can add/replace items by passing them as
|
|
# hashrefs of {key, desc, copy, dsn}. The desc and dsn items are optional.
|
|
# You can set properties with the prop() sub. Don't set the 'opts' property.
|
|
sub new {
|
|
my ( $class, @opts ) = @_;
|
|
my $self = {
|
|
opts => {
|
|
A => {
|
|
desc => 'Default character set',
|
|
dsn => 'charset',
|
|
copy => 1,
|
|
},
|
|
D => {
|
|
desc => 'Database to use',
|
|
dsn => 'database',
|
|
copy => 1,
|
|
},
|
|
F => {
|
|
desc => 'Only read default options from the given file',
|
|
dsn => 'mysql_read_default_file',
|
|
copy => 1,
|
|
},
|
|
h => {
|
|
desc => 'Connect to host',
|
|
dsn => 'host',
|
|
copy => 1,
|
|
},
|
|
p => {
|
|
desc => 'Password to use when connecting',
|
|
dsn => 'password',
|
|
copy => 1,
|
|
},
|
|
P => {
|
|
desc => 'Port number to use for connection',
|
|
dsn => 'port',
|
|
copy => 1,
|
|
},
|
|
S => {
|
|
desc => 'Socket file to use for connection',
|
|
dsn => 'mysql_socket',
|
|
copy => 1,
|
|
},
|
|
u => {
|
|
desc => 'User for login if not current user',
|
|
dsn => 'user',
|
|
copy => 1,
|
|
},
|
|
},
|
|
};
|
|
foreach my $opt ( @opts ) {
|
|
MKDEBUG && _d('Adding extra property ' . $opt->{key});
|
|
$self->{opts}->{$opt->{key}} = { desc => $opt->{desc}, copy => $opt->{copy} };
|
|
}
|
|
return bless $self, $class;
|
|
}
|
|
|
|
# Recognized properties:
|
|
# * autokey: which key to treat a bareword as (typically h=host).
|
|
# * dbidriver: which DBI driver to use; assumes mysql, supports Pg.
|
|
# * required: which parts are required (hashref).
|
|
# * setvars: a list of variables to set after connecting
|
|
sub prop {
|
|
my ( $self, $prop, $value ) = @_;
|
|
if ( @_ > 2 ) {
|
|
MKDEBUG && _d("Setting $prop property");
|
|
$self->{$prop} = $value;
|
|
}
|
|
return $self->{$prop};
|
|
}
|
|
|
|
sub parse {
|
|
my ( $self, $dsn, $prev, $defaults ) = @_;
|
|
if ( !$dsn ) {
|
|
MKDEBUG && _d('No DSN to parse');
|
|
return;
|
|
}
|
|
MKDEBUG && _d("Parsing $dsn");
|
|
$prev ||= {};
|
|
$defaults ||= {};
|
|
my %given_props;
|
|
my %final_props;
|
|
my %opts = %{$self->{opts}};
|
|
my $prop_autokey = $self->prop('autokey');
|
|
|
|
# Parse given props
|
|
foreach my $dsn_part ( split(/,/, $dsn) ) {
|
|
if ( my ($prop_key, $prop_val) = $dsn_part =~ m/^(.)=(.*)$/ ) {
|
|
# Handle the typical DSN parts like h=host, P=3306, etc.
|
|
$given_props{$prop_key} = $prop_val;
|
|
}
|
|
elsif ( $prop_autokey ) {
|
|
# Handle barewords
|
|
MKDEBUG && _d("Interpreting $dsn_part as $prop_autokey=$dsn_part");
|
|
$given_props{$prop_autokey} = $dsn_part;
|
|
}
|
|
else {
|
|
MKDEBUG && _d("Bad DSN part: $dsn_part");
|
|
}
|
|
}
|
|
|
|
# Fill in final props from given, previous, and/or default props
|
|
foreach my $key ( keys %opts ) {
|
|
MKDEBUG && _d("Finding value for $key");
|
|
$final_props{$key} = $given_props{$key};
|
|
if ( !defined $final_props{$key}
|
|
&& defined $prev->{$key} && $opts{$key}->{copy} )
|
|
{
|
|
$final_props{$key} = $prev->{$key};
|
|
MKDEBUG && _d("Copying value for $key from previous DSN");
|
|
}
|
|
if ( !defined $final_props{$key} ) {
|
|
$final_props{$key} = $defaults->{$key};
|
|
MKDEBUG && _d("Copying value for $key from defaults");
|
|
}
|
|
}
|
|
|
|
# Sanity check props
|
|
foreach my $key ( keys %given_props ) {
|
|
die "Unrecognized DSN part '$key' in '$dsn'\n"
|
|
unless exists $opts{$key};
|
|
}
|
|
if ( (my $required = $self->prop('required')) ) {
|
|
foreach my $key ( keys %$required ) {
|
|
die "Missing DSN part '$key' in '$dsn'\n" unless $final_props{$key};
|
|
}
|
|
}
|
|
|
|
return \%final_props;
|
|
}
|
|
|
|
sub as_string {
|
|
my ( $self, $dsn ) = @_;
|
|
return $dsn unless ref $dsn;
|
|
return join(',',
|
|
map { "$_=" . ($_ eq 'p' ? '...' : $dsn->{$_}) }
|
|
grep { defined $dsn->{$_} && $self->{opts}->{$_} }
|
|
sort keys %$dsn );
|
|
}
|
|
|
|
sub usage {
|
|
my ( $self ) = @_;
|
|
my $usage
|
|
= "DSN syntax is key=value[,key=value...] Allowable DSN keys:\n"
|
|
. " KEY COPY MEANING\n"
|
|
. " === ==== =============================================\n";
|
|
my %opts = %{$self->{opts}};
|
|
foreach my $key ( sort keys %opts ) {
|
|
$usage .= " $key "
|
|
. ($opts{$key}->{copy} ? 'yes ' : 'no ')
|
|
. ($opts{$key}->{desc} || '[No description]')
|
|
. "\n";
|
|
}
|
|
if ( (my $key = $self->prop('autokey')) ) {
|
|
$usage .= " If the DSN is a bareword, the word is treated as the '$key' key.\n";
|
|
}
|
|
return $usage;
|
|
}
|
|
|
|
# Supports PostgreSQL via the dbidriver element of $info, but assumes MySQL by
|
|
# default.
|
|
sub get_cxn_params {
|
|
my ( $self, $info ) = @_;
|
|
my $dsn;
|
|
my %opts = %{$self->{opts}};
|
|
my $driver = $self->prop('dbidriver') || '';
|
|
if ( $driver eq 'Pg' ) {
|
|
$dsn = 'DBI:Pg:dbname=' . ( $info->{D} || '' ) . ';'
|
|
. join(';', map { "$opts{$_}->{dsn}=$info->{$_}" }
|
|
grep { defined $info->{$_} }
|
|
qw(h P));
|
|
}
|
|
else {
|
|
$dsn = 'DBI:mysql:' . ( $info->{D} || '' ) . ';'
|
|
. join(';', map { "$opts{$_}->{dsn}=$info->{$_}" }
|
|
grep { defined $info->{$_} }
|
|
qw(F h P S A))
|
|
. ';mysql_read_default_group=client';
|
|
}
|
|
MKDEBUG && _d($dsn);
|
|
return ($dsn, $info->{u}, $info->{p});
|
|
}
|
|
|
|
|
|
# Fills in missing info from a DSN after successfully connecting to the server.
|
|
sub fill_in_dsn {
|
|
my ( $self, $dbh, $dsn ) = @_;
|
|
my $vars = $dbh->selectall_hashref('SHOW VARIABLES', 'Variable_name');
|
|
my ($user, $db) = $dbh->selectrow_array('SELECT USER(), DATABASE()');
|
|
$user =~ s/@.*//;
|
|
$dsn->{h} ||= $vars->{hostname}->{Value};
|
|
$dsn->{S} ||= $vars->{'socket'}->{Value};
|
|
$dsn->{P} ||= $vars->{port}->{Value};
|
|
$dsn->{u} ||= $user;
|
|
$dsn->{D} ||= $db;
|
|
}
|
|
|
|
sub get_dbh {
|
|
my ( $self, $cxn_string, $user, $pass, $opts ) = @_;
|
|
$opts ||= {};
|
|
my $defaults = {
|
|
AutoCommit => 0,
|
|
RaiseError => 1,
|
|
PrintError => 0,
|
|
mysql_enable_utf8 => ($cxn_string =~ m/charset=utf8/ ? 1 : 0),
|
|
};
|
|
@{$defaults}{ keys %$opts } = values %$opts;
|
|
my $dbh;
|
|
my $tries = 2;
|
|
while ( !$dbh && $tries-- ) {
|
|
eval {
|
|
MKDEBUG && _d($cxn_string, ' ', $user, ' ', $pass, ' {',
|
|
join(', ', map { "$_=>$defaults->{$_}" } keys %$defaults ), '}');
|
|
$dbh = DBI->connect($cxn_string, $user, $pass, $defaults);
|
|
# Immediately set character set and binmode on STDOUT.
|
|
if ( my ($charset) = $cxn_string =~ m/charset=(\w+)/ ) {
|
|
my $sql = "/*!40101 SET NAMES $charset*/";
|
|
MKDEBUG && _d("$dbh: $sql");
|
|
$dbh->do($sql);
|
|
MKDEBUG && _d('Enabling charset for STDOUT');
|
|
if ( $charset eq 'utf8' ) {
|
|
binmode(STDOUT, ':utf8')
|
|
or die "Can't binmode(STDOUT, ':utf8'): $OS_ERROR";
|
|
}
|
|
else {
|
|
binmode(STDOUT) or die "Can't binmode(STDOUT): $OS_ERROR";
|
|
}
|
|
}
|
|
};
|
|
if ( !$dbh && $EVAL_ERROR ) {
|
|
MKDEBUG && _d($EVAL_ERROR);
|
|
if ( $EVAL_ERROR =~ m/not a compiled character set|character set utf8/ ) {
|
|
MKDEBUG && _d("Going to try again without utf8 support");
|
|
delete $defaults->{mysql_enable_utf8};
|
|
}
|
|
if ( !$tries ) {
|
|
die $EVAL_ERROR;
|
|
}
|
|
}
|
|
}
|
|
# If setvars exists and it's MySQL connection, set them
|
|
my $setvars = $self->prop('setvars');
|
|
if ( $cxn_string =~ m/mysql/i && $setvars ) {
|
|
my $sql = "SET $setvars";
|
|
MKDEBUG && _d("$dbh: $sql");
|
|
eval {
|
|
$dbh->do($sql);
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
MKDEBUG && _d($EVAL_ERROR);
|
|
}
|
|
}
|
|
MKDEBUG && _d('DBH info: ',
|
|
$dbh,
|
|
Dumper($dbh->selectrow_hashref(
|
|
'SELECT DATABASE(), CONNECTION_ID(), VERSION()/*!50038 , @@hostname*/')),
|
|
' Connection info: ', ($dbh->{mysql_hostinfo} || 'undef'),
|
|
' Character set info: ',
|
|
Dumper($dbh->selectall_arrayref(
|
|
'SHOW VARIABLES LIKE "character_set%"', { Slice => {}})),
|
|
' $DBD::mysql::VERSION: ', $DBD::mysql::VERSION,
|
|
' $DBI::VERSION: ', $DBI::VERSION,
|
|
);
|
|
return $dbh;
|
|
}
|
|
|
|
# Tries to figure out a hostname for the connection.
|
|
sub get_hostname {
|
|
my ( $self, $dbh ) = @_;
|
|
if ( my ($host) = ($dbh->{mysql_hostinfo} || '') =~ m/^(\w+) via/ ) {
|
|
return $host;
|
|
}
|
|
my ( $hostname, $one ) = $dbh->selectrow_array(
|
|
'SELECT /*!50038 @@hostname, */ 1');
|
|
return $hostname;
|
|
}
|
|
|
|
# Disconnects a database handle, but complains verbosely if there are any active
|
|
# children. These are usually $sth handles that haven't been finish()ed.
|
|
sub disconnect {
|
|
my ( $self, $dbh ) = @_;
|
|
MKDEBUG && $self->print_active_handles($dbh);
|
|
$dbh->disconnect;
|
|
}
|
|
|
|
sub print_active_handles {
|
|
my ( $self, $thing, $level ) = @_;
|
|
$level ||= 0;
|
|
printf("# Active %sh: %s %s %s\n", ($thing->{Type} || 'undef'), "\t" x $level,
|
|
$thing, (($thing->{Type} || '') eq 'st' ? $thing->{Statement} || '' : ''))
|
|
or die "Cannot print: $OS_ERROR";
|
|
foreach my $handle ( grep {defined} @{ $thing->{ChildHandles} } ) {
|
|
$self->print_active_handles( $handle, $level + 1 );
|
|
}
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
# Use $$ instead of $PID in case the package
|
|
# does not use English.
|
|
print "# $package:$line $$ ", @_, "\n";
|
|
}
|
|
|
|
1;
|
|
|
|
package InnoDBParser;
|
|
|
|
use Data::Dumper;
|
|
$Data::Dumper::Sortkeys = 1;
|
|
use English qw(-no_match_vars);
|
|
use List::Util qw(max);
|
|
|
|
# Some common patterns
|
|
my $d = qr/(\d+)/; # Digit
|
|
my $f = qr/(\d+\.\d+)/; # Float
|
|
my $t = qr/(\d+ \d+)/; # Transaction ID
|
|
my $i = qr/((?:\d{1,3}\.){3}\d+)/; # IP address
|
|
my $n = qr/([^`\s]+)/; # MySQL object name
|
|
my $w = qr/(\w+)/; # Words
|
|
my $fl = qr/([\w\.\/]+) line $d/; # Filename and line number
|
|
my $h = qr/((?:0x)?[0-9a-f]*)/; # Hex
|
|
my $s = qr/(\d{6} .\d:\d\d:\d\d)/; # InnoDB timestamp
|
|
|
|
# If you update this variable, also update the SYNOPSIS in the pod.
|
|
my %innodb_section_headers = (
|
|
"TRANSACTIONS" => "tx",
|
|
"BUFFER POOL AND MEMORY" => "bp",
|
|
"SEMAPHORES" => "sm",
|
|
"LOG" => "lg",
|
|
"ROW OPERATIONS" => "ro",
|
|
"INSERT BUFFER AND ADAPTIVE HASH INDEX" => "ib",
|
|
"FILE I/O" => "io",
|
|
"LATEST DETECTED DEADLOCK" => "dl",
|
|
"LATEST FOREIGN KEY ERROR" => "fk",
|
|
);
|
|
|
|
my %parser_for = (
|
|
tx => \&parse_tx_section,
|
|
bp => \&parse_bp_section,
|
|
sm => \&parse_sm_section,
|
|
lg => \&parse_lg_section,
|
|
ro => \&parse_ro_section,
|
|
ib => \&parse_ib_section,
|
|
io => \&parse_io_section,
|
|
dl => \&parse_dl_section,
|
|
fk => \&parse_fk_section,
|
|
);
|
|
|
|
my %fk_parser_for = (
|
|
Transaction => \&parse_fk_transaction_error,
|
|
Error => \&parse_fk_bad_constraint_error,
|
|
Cannot => \&parse_fk_cant_drop_parent_error,
|
|
);
|
|
|
|
# A thread's proc_info can be at least 98 different things I've found in the
|
|
# source. Fortunately, most of them begin with a gerunded verb. These are
|
|
# the ones that don't.
|
|
my %is_proc_info = (
|
|
'After create' => 1,
|
|
'Execution of init_command' => 1,
|
|
'FULLTEXT initialization' => 1,
|
|
'Reopen tables' => 1,
|
|
'Repair done' => 1,
|
|
'Repair with keycache' => 1,
|
|
'System lock' => 1,
|
|
'Table lock' => 1,
|
|
'Thread initialized' => 1,
|
|
'User lock' => 1,
|
|
'copy to tmp table' => 1,
|
|
'discard_or_import_tablespace' => 1,
|
|
'end' => 1,
|
|
'got handler lock' => 1,
|
|
'got old table' => 1,
|
|
'init' => 1,
|
|
'key cache' => 1,
|
|
'locks' => 1,
|
|
'malloc' => 1,
|
|
'query end' => 1,
|
|
'rename result table' => 1,
|
|
'rename' => 1,
|
|
'setup' => 1,
|
|
'statistics' => 1,
|
|
'status' => 1,
|
|
'table cache' => 1,
|
|
'update' => 1,
|
|
);
|
|
|
|
sub new {
|
|
bless {}, shift;
|
|
}
|
|
|
|
# Parse the status and return it.
|
|
# See srv_printf_innodb_monitor in innobase/srv/srv0srv.c
|
|
# Pass in the text to parse, whether to be in debugging mode, which sections
|
|
# to parse (hashref; if empty, parse all), and whether to parse full info from
|
|
# locks and such (probably shouldn't unless you need to).
|
|
sub parse_status_text {
|
|
my ( $self, $fulltext, $debug, $sections, $full ) = @_;
|
|
|
|
die "I can't parse undef" unless defined $fulltext;
|
|
$fulltext =~ s/[\r\n]+/\n/g;
|
|
|
|
$sections ||= {};
|
|
die '$sections must be a hashref' unless ref($sections) eq 'HASH';
|
|
|
|
my %innodb_data = (
|
|
got_all => 0, # Whether I was able to get the whole thing
|
|
ts => '', # Timestamp the server put on it
|
|
last_secs => 0, # Num seconds the averages are over
|
|
sections => {}, # Parsed values from each section
|
|
);
|
|
|
|
if ( $debug ) {
|
|
$innodb_data{'fulltext'} = $fulltext;
|
|
}
|
|
|
|
# Get the most basic info about the status: beginning and end, and whether
|
|
# I got the whole thing (if there has been a big deadlock and there are
|
|
# too many locks to print, the output might be truncated)
|
|
my ( $time_text ) = $fulltext =~ m/^$s INNODB MONITOR OUTPUT$/m;
|
|
$innodb_data{'ts'} = [ parse_innodb_timestamp( $time_text ) ];
|
|
$innodb_data{'timestring'} = ts_to_string($innodb_data{'ts'});
|
|
( $innodb_data{'last_secs'} ) = $fulltext
|
|
=~ m/Per second averages calculated from the last $d seconds/;
|
|
|
|
( my $got_all ) = $fulltext =~ m/END OF INNODB MONITOR OUTPUT/;
|
|
$innodb_data{'got_all'} = $got_all || 0;
|
|
|
|
# Split it into sections. Each section begins with
|
|
# -----
|
|
# LABEL
|
|
# -----
|
|
my %innodb_sections;
|
|
my @matches = $fulltext
|
|
=~ m#\n(---+)\n([A-Z /]+)\n\1\n(.*?)(?=\n(---+)\n[A-Z /]+\n\4\n|$)#gs;
|
|
while ( my ( $start, $name, $text, $end ) = splice(@matches, 0, 4) ) {
|
|
$innodb_sections{$name} = [ $text, $end ? 1 : 0 ];
|
|
}
|
|
# The Row Operations section is a special case, because instead of ending
|
|
# with the beginning of another section, it ends with the end of the file.
|
|
# So this section is complete if the entire file is complete.
|
|
$innodb_sections{'ROW OPERATIONS'}->[1] ||= $innodb_data{'got_all'};
|
|
|
|
# Just for sanity's sake, make sure I understand what to do with each
|
|
# section
|
|
eval {
|
|
foreach my $section ( keys %innodb_sections ) {
|
|
my $header = $innodb_section_headers{$section};
|
|
die "Unknown section $section in $fulltext\n"
|
|
unless $header;
|
|
$innodb_data{'sections'}->{ $header }
|
|
->{'fulltext'} = $innodb_sections{$section}->[0];
|
|
$innodb_data{'sections'}->{ $header }
|
|
->{'complete'} = $innodb_sections{$section}->[1];
|
|
}
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
_debug( $debug, $EVAL_ERROR);
|
|
}
|
|
|
|
# ################################################################
|
|
# Parse the detailed data out of the sections.
|
|
# ################################################################
|
|
eval {
|
|
foreach my $section ( keys %parser_for ) {
|
|
if ( defined $innodb_data{'sections'}->{$section}
|
|
&& (!%$sections || (defined($sections->{$section} && $sections->{$section})) )) {
|
|
$parser_for{$section}->(
|
|
$innodb_data{'sections'}->{$section},
|
|
$innodb_data{'sections'}->{$section}->{'complete'},
|
|
$debug,
|
|
$full )
|
|
or delete $innodb_data{'sections'}->{$section};
|
|
}
|
|
else {
|
|
delete $innodb_data{'sections'}->{$section};
|
|
}
|
|
}
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
_debug( $debug, $EVAL_ERROR);
|
|
}
|
|
|
|
return \%innodb_data;
|
|
}
|
|
|
|
# Parses the status text and returns it flattened out as a single hash.
|
|
sub get_status_hash {
|
|
my ( $self, $fulltext, $debug, $sections, $full ) = @_;
|
|
|
|
# Parse the status text...
|
|
my $innodb_status
|
|
= $self->parse_status_text($fulltext, $debug, $sections, $full );
|
|
|
|
# Flatten the hierarchical structure into a single list by grabbing desired
|
|
# sections from it.
|
|
return
|
|
(map { 'IB_' . $_ => $innodb_status->{$_} } qw(timestring last_secs got_all)),
|
|
(map { 'IB_bp_' . $_ => $innodb_status->{'sections'}->{'bp'}->{$_} }
|
|
qw( writes_pending buf_pool_hit_rate total_mem_alloc buf_pool_reads
|
|
awe_mem_alloc pages_modified writes_pending_lru page_creates_sec
|
|
reads_pending pages_total buf_pool_hits writes_pending_single_page
|
|
page_writes_sec pages_read pages_written page_reads_sec
|
|
writes_pending_flush_list buf_pool_size add_pool_alloc
|
|
dict_mem_alloc pages_created buf_free complete )),
|
|
(map { 'IB_tx_' . $_ => $innodb_status->{'sections'}->{'tx'}->{$_} }
|
|
qw( num_lock_structs history_list_len purge_done_for transactions
|
|
purge_undo_for is_truncated trx_id_counter complete )),
|
|
(map { 'IB_ib_' . $_ => $innodb_status->{'sections'}->{'ib'}->{$_} }
|
|
qw( hash_table_size hash_searches_s non_hash_searches_s
|
|
bufs_in_node_heap used_cells size free_list_len seg_size inserts
|
|
merged_recs merges complete )),
|
|
(map { 'IB_lg_' . $_ => $innodb_status->{'sections'}->{'lg'}->{$_} }
|
|
qw( log_ios_done pending_chkp_writes last_chkp log_ios_s
|
|
log_flushed_to log_seq_no pending_log_writes complete )),
|
|
(map { 'IB_sm_' . $_ => $innodb_status->{'sections'}->{'sm'}->{$_} }
|
|
qw( wait_array_size rw_shared_spins rw_excl_os_waits mutex_os_waits
|
|
mutex_spin_rounds mutex_spin_waits rw_excl_spins rw_shared_os_waits
|
|
waits signal_count reservation_count complete )),
|
|
(map { 'IB_ro_' . $_ => $innodb_status->{'sections'}->{'ro'}->{$_} }
|
|
qw( queries_in_queue n_reserved_extents main_thread_state
|
|
main_thread_proc_no main_thread_id read_sec del_sec upd_sec ins_sec
|
|
read_views_open num_rows_upd num_rows_ins num_rows_read
|
|
queries_inside num_rows_del complete )),
|
|
(map { 'IB_fk_' . $_ => $innodb_status->{'sections'}->{'fk'}->{$_} }
|
|
qw( trigger parent_table child_index parent_index attempted_op
|
|
child_db timestring fk_name records col_name reason txn parent_db
|
|
type child_table parent_col complete )),
|
|
(map { 'IB_io_' . $_ => $innodb_status->{'sections'}->{'io'}->{$_} }
|
|
qw( pending_buffer_pool_flushes pending_pwrites pending_preads
|
|
pending_normal_aio_reads fsyncs_s os_file_writes pending_sync_ios
|
|
reads_s flush_type avg_bytes_s pending_ibuf_aio_reads writes_s
|
|
threads os_file_reads pending_aio_writes pending_log_ios os_fsyncs
|
|
pending_log_flushes complete )),
|
|
(map { 'IB_dl_' . $_ => $innodb_status->{'sections'}->{'dl'}->{$_} }
|
|
qw( timestring rolled_back txns complete ));
|
|
|
|
}
|
|
|
|
sub ts_to_string {
|
|
my $parts = shift;
|
|
return sprintf('%02d-%02d-%02d %02d:%02d:%02d', @$parts);
|
|
}
|
|
|
|
sub parse_innodb_timestamp {
|
|
my $text = shift;
|
|
my ( $y, $m, $d, $h, $i, $s )
|
|
= $text =~ m/^(\d\d)(\d\d)(\d\d) +(\d+):(\d+):(\d+)$/;
|
|
die("Can't get timestamp from $text\n") unless $y;
|
|
$y += 2000;
|
|
return ( $y, $m, $d, $h, $i, $s );
|
|
}
|
|
|
|
sub parse_fk_section {
|
|
my ( $section, $complete, $debug, $full ) = @_;
|
|
my $fulltext = $section->{'fulltext'};
|
|
|
|
return 0 unless $fulltext;
|
|
|
|
my ( $ts, $type ) = $fulltext =~ m/^$s\s+(\w+)/m;
|
|
$section->{'ts'} = [ parse_innodb_timestamp( $ts ) ];
|
|
$section->{'timestring'} = ts_to_string($section->{'ts'});
|
|
$section->{'type'} = $type;
|
|
|
|
# Decide which type of FK error happened, and dispatch to the right parser.
|
|
if ( $type && $fk_parser_for{$type} ) {
|
|
$fk_parser_for{$type}->( $section, $complete, $debug, $fulltext, $full );
|
|
}
|
|
|
|
delete $section->{'fulltext'} unless $debug;
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub parse_fk_cant_drop_parent_error {
|
|
my ( $section, $complete, $debug, $fulltext, $full ) = @_;
|
|
|
|
# Parse the parent/child table info out
|
|
@{$section}{ qw(attempted_op parent_db parent_table) } = $fulltext
|
|
=~ m{Cannot $w table `(.*)/(.*)`}m;
|
|
@{$section}{ qw(child_db child_table) } = $fulltext
|
|
=~ m{because it is referenced by `(.*)/(.*)`}m;
|
|
|
|
( $section->{'reason'} ) = $fulltext =~ m/(Cannot .*)/s;
|
|
$section->{'reason'} =~ s/\n(?:InnoDB: )?/ /gm
|
|
if $section->{'reason'};
|
|
|
|
# Certain data may not be present. Make them '' if not present.
|
|
map { $section->{$_} ||= "" }
|
|
qw(child_index fk_name col_name parent_col);
|
|
}
|
|
|
|
# See dict/dict0dict.c, function dict_foreign_error_report
|
|
# I don't care much about these. There are lots of different messages, and
|
|
# they come from someone trying to create a foreign key, or similar
|
|
# statements. They aren't indicative of some transaction trying to insert,
|
|
# delete or update data. Sometimes it is possible to parse out a lot of
|
|
# information about the tables and indexes involved, but often the message
|
|
# contains the DDL string the user entered, which is way too much for this
|
|
# module to try to handle.
|
|
sub parse_fk_bad_constraint_error {
|
|
my ( $section, $complete, $debug, $fulltext, $full ) = @_;
|
|
|
|
# Parse the parent/child table and index info out
|
|
@{$section}{ qw(child_db child_table) } = $fulltext
|
|
=~ m{Error in foreign key constraint of table (.*)/(.*):$}m;
|
|
$section->{'attempted_op'} = 'DDL';
|
|
|
|
# FK name, parent info... if possible.
|
|
@{$section}{ qw(fk_name col_name parent_db parent_table parent_col) }
|
|
= $fulltext
|
|
=~ m/CONSTRAINT `?$n`? FOREIGN KEY \(`?$n`?\) REFERENCES (?:`?$n`?\.)?`?$n`? \(`?$n`?\)/;
|
|
|
|
if ( !defined($section->{'fk_name'}) ) {
|
|
# Try to parse SQL a user might have typed in a CREATE statement or such
|
|
@{$section}{ qw(col_name parent_db parent_table parent_col) }
|
|
= $fulltext
|
|
=~ m/FOREIGN\s+KEY\s*\(`?$n`?\)\s+REFERENCES\s+(?:`?$n`?\.)?`?$n`?\s*\(`?$n`?\)/i;
|
|
}
|
|
$section->{'parent_db'} ||= $section->{'child_db'};
|
|
|
|
# Name of the child index (index in the same table where the FK is, see
|
|
# definition of dict_foreign_struct in include/dict0mem.h, where it is
|
|
# called foreign_index, as opposed to referenced_index which is in the
|
|
# parent table. This may not be possible to find.
|
|
@{$section}{ qw(child_index) } = $fulltext
|
|
=~ m/^The index in the foreign key in table is $n$/m;
|
|
|
|
@{$section}{ qw(reason) } = $fulltext =~ m/:\s*([^:]+)(?= Constraint:|$)/ms;
|
|
$section->{'reason'} =~ s/\s+/ /g
|
|
if $section->{'reason'};
|
|
|
|
# Certain data may not be present. Make them '' if not present.
|
|
map { $section->{$_} ||= "" }
|
|
qw(child_index fk_name col_name parent_table parent_col);
|
|
}
|
|
|
|
# see source file row/row0ins.c
|
|
sub parse_fk_transaction_error {
|
|
my ( $section, $complete, $debug, $fulltext, $full ) = @_;
|
|
|
|
# Parse the txn info out
|
|
my ( $txn ) = $fulltext
|
|
=~ m/Transaction:\n(TRANSACTION.*)\nForeign key constraint fails/s;
|
|
if ( $txn ) {
|
|
$section->{'txn'} = parse_tx_text( $txn, $complete, $debug, $full );
|
|
}
|
|
|
|
# Parse the parent/child table and index info out. There are two types: an
|
|
# update or a delete of a parent record leaves a child orphaned
|
|
# (row_ins_foreign_report_err), and an insert or update of a child record has
|
|
# no matching parent record (row_ins_foreign_report_add_err).
|
|
|
|
@{$section}{ qw(reason child_db child_table) }
|
|
= $fulltext =~ m{^(Foreign key constraint fails for table `(.*)/(.*)`:)$}m;
|
|
|
|
@{$section}{ qw(fk_name col_name parent_db parent_table parent_col) }
|
|
= $fulltext
|
|
=~ m/CONSTRAINT `$n` FOREIGN KEY \(`$n`\) REFERENCES (?:`$n`\.)?`$n` \(`$n`\)/;
|
|
$section->{'parent_db'} ||= $section->{'child_db'};
|
|
|
|
# Special case, which I don't know how to trigger, but see
|
|
# innobase/row/row0ins.c row_ins_check_foreign_constraint
|
|
if ( $fulltext =~ m/ibd file does not currently exist!/ ) {
|
|
my ( $attempted_op, $index, $records )
|
|
= $fulltext =~ m/^Trying to (add to index) `$n` tuple:\n(.*))?/sm;
|
|
$section->{'child_index'} = $index;
|
|
$section->{'attempted_op'} = $attempted_op || '';
|
|
if ( $records && $full ) {
|
|
( $section->{'records'} )
|
|
= parse_innodb_record_dump( $records, $complete, $debug );
|
|
}
|
|
@{$section}{qw(parent_db parent_table)}
|
|
=~ m/^But the parent table `$n`\.`$n`$/m;
|
|
}
|
|
else {
|
|
my ( $attempted_op, $which, $index )
|
|
= $fulltext =~ m/^Trying to ([\w ]*) in (child|parent) table, in index `$n` tuple:$/m;
|
|
if ( $which ) {
|
|
$section->{$which . '_index'} = $index;
|
|
$section->{'attempted_op'} = $attempted_op || '';
|
|
|
|
# Parse out the related records in the other table.
|
|
my ( $search_index, $records );
|
|
if ( $which eq 'child' ) {
|
|
( $search_index, $records ) = $fulltext
|
|
=~ m/^But in parent table [^,]*, in index `$n`,\nthe closest match we can find is record:\n(.*)/ms;
|
|
$section->{'parent_index'} = $search_index;
|
|
}
|
|
else {
|
|
( $search_index, $records ) = $fulltext
|
|
=~ m/^But in child table [^,]*, in index `$n`, (?:the record is not available|there is a record:\n(.*))?/ms;
|
|
$section->{'child_index'} = $search_index;
|
|
}
|
|
if ( $records && $full ) {
|
|
$section->{'records'}
|
|
= parse_innodb_record_dump( $records, $complete, $debug );
|
|
}
|
|
else {
|
|
$section->{'records'} = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
# Parse out the tuple trying to be updated, deleted or inserted.
|
|
my ( $trigger ) = $fulltext =~ m/^(DATA TUPLE: \d+ fields;\n.*)$/m;
|
|
if ( $trigger ) {
|
|
$section->{'trigger'} = parse_innodb_record_dump( $trigger, $complete, $debug );
|
|
}
|
|
|
|
# Certain data may not be present. Make them '' if not present.
|
|
map { $section->{$_} ||= "" }
|
|
qw(child_index fk_name col_name parent_table parent_col);
|
|
}
|
|
|
|
# There are new-style and old-style record formats. See rem/rem0rec.c
|
|
# TODO: write some tests for this
|
|
sub parse_innodb_record_dump {
|
|
my ( $dump, $complete, $debug ) = @_;
|
|
return undef unless $dump;
|
|
|
|
my $result = {};
|
|
|
|
if ( $dump =~ m/PHYSICAL RECORD/ ) {
|
|
my $style = $dump =~ m/compact format/ ? 'new' : 'old';
|
|
$result->{'style'} = $style;
|
|
|
|
# This is a new-style record.
|
|
if ( $style eq 'new' ) {
|
|
@{$result}{qw( heap_no type num_fields info_bits )}
|
|
= $dump
|
|
=~ m/^(?:Record lock, heap no $d )?([A-Z ]+): n_fields $d; compact format; info bits $d$/m;
|
|
}
|
|
|
|
# OK, it's old-style. Unfortunately there are variations here too.
|
|
elsif ( $dump =~ m/-byte offs / ) {
|
|
# Older-old style.
|
|
@{$result}{qw( heap_no type num_fields byte_offset info_bits )}
|
|
= $dump
|
|
=~ m/^(?:Record lock, heap no $d )?([A-Z ]+): n_fields $d; $d-byte offs [A-Z]+; info bits $d$/m;
|
|
if ( $dump !~ m/-byte offs TRUE/ ) {
|
|
$result->{'byte_offset'} = 0;
|
|
}
|
|
}
|
|
else {
|
|
# Newer-old style.
|
|
@{$result}{qw( heap_no type num_fields byte_offset info_bits )}
|
|
= $dump
|
|
=~ m/^(?:Record lock, heap no $d )?([A-Z ]+): n_fields $d; $d-byte offsets; info bits $d$/m;
|
|
}
|
|
|
|
}
|
|
else {
|
|
$result->{'style'} = 'tuple';
|
|
@{$result}{qw( type num_fields )}
|
|
= $dump =~ m/^(DATA TUPLE): $d fields;$/m;
|
|
}
|
|
|
|
# Fill in default values for things that couldn't be parsed.
|
|
map { $result->{$_} ||= 0 }
|
|
qw(heap_no num_fields byte_offset info_bits);
|
|
map { $result->{$_} ||= '' }
|
|
qw(style type );
|
|
|
|
my @fields = $dump =~ m/ (\d+:.*?;?);(?=$| \d+:)/gm;
|
|
$result->{'fields'} = [ map { parse_field($_, $complete, $debug ) } @fields ];
|
|
|
|
return $result;
|
|
}
|
|
|
|
# New/old-style applies here. See rem/rem0rec.c
|
|
# $text should not include the leading space or the second trailing semicolon.
|
|
sub parse_field {
|
|
my ( $text, $complete, $debug ) = @_;
|
|
|
|
# Sample fields:
|
|
# '4: SQL NULL, size 4 '
|
|
# '1: len 6; hex 000000005601; asc V ;'
|
|
# '6: SQL NULL'
|
|
# '5: len 30; hex 687474703a2f2f7777772e737765657477617465722e636f6d2f73746f72; asc http://www.sweetwater.com/stor;...(truncated)'
|
|
my ( $id, $nullsize, $len, $hex, $asc, $truncated );
|
|
( $id, $nullsize ) = $text =~ m/^$d: SQL NULL, size $d $/;
|
|
if ( !defined($id) ) {
|
|
( $id ) = $text =~ m/^$d: SQL NULL$/;
|
|
}
|
|
if ( !defined($id) ) {
|
|
( $id, $len, $hex, $asc, $truncated )
|
|
= $text =~ m/^$d: len $d; hex $h; asc (.*);(\.\.\.\(truncated\))?$/;
|
|
}
|
|
|
|
die "Could not parse this field: '$text'" unless defined $id;
|
|
return {
|
|
id => $id,
|
|
len => defined($len) ? $len : defined($nullsize) ? $nullsize : 0,
|
|
'hex' => defined($hex) ? $hex : '',
|
|
asc => defined($asc) ? $asc : '',
|
|
trunc => $truncated ? 1 : 0,
|
|
};
|
|
|
|
}
|
|
|
|
sub parse_dl_section {
|
|
my ( $dl, $complete, $debug, $full ) = @_;
|
|
return unless $dl;
|
|
my $fulltext = $dl->{'fulltext'};
|
|
return 0 unless $fulltext;
|
|
|
|
my ( $ts ) = $fulltext =~ m/^$s$/m;
|
|
return 0 unless $ts;
|
|
|
|
$dl->{'ts'} = [ parse_innodb_timestamp( $ts ) ];
|
|
$dl->{'timestring'} = ts_to_string($dl->{'ts'});
|
|
$dl->{'txns'} = {};
|
|
|
|
my @sections
|
|
= $fulltext
|
|
=~ m{
|
|
^\*{3}\s([^\n]*) # *** (1) WAITING FOR THIS...
|
|
(.*?) # Followed by anything, non-greedy
|
|
(?=(?:^\*{3})|\z) # Followed by another three stars or EOF
|
|
}gmsx;
|
|
|
|
|
|
# Loop through each section. There are no assumptions about how many
|
|
# there are, who holds and wants what locks, and who gets rolled back.
|
|
while ( my ($header, $body) = splice(@sections, 0, 2) ) {
|
|
my ( $txn_id, $what ) = $header =~ m/^\($d\) (.*):$/;
|
|
next unless $txn_id;
|
|
$dl->{'txns'}->{$txn_id} ||= {};
|
|
my $txn = $dl->{'txns'}->{$txn_id};
|
|
|
|
if ( $what eq 'TRANSACTION' ) {
|
|
$txn->{'tx'} = parse_tx_text( $body, $complete, $debug, $full );
|
|
}
|
|
else {
|
|
push @{$txn->{'locks'}}, parse_innodb_record_locks( $body, $complete, $debug, $full );
|
|
}
|
|
}
|
|
|
|
@{ $dl }{ qw(rolled_back) }
|
|
= $fulltext =~ m/^\*\*\* WE ROLL BACK TRANSACTION \($d\)$/m;
|
|
|
|
# Make sure certain values aren't undef
|
|
map { $dl->{$_} ||= '' } qw(rolled_back);
|
|
|
|
delete $dl->{'fulltext'} unless $debug;
|
|
return 1;
|
|
}
|
|
|
|
sub parse_innodb_record_locks {
|
|
my ( $text, $complete, $debug, $full ) = @_;
|
|
my @result;
|
|
|
|
foreach my $lock ( $text =~ m/(^(?:RECORD|TABLE) LOCKS?.*$)/gm ) {
|
|
my $hash = {};
|
|
@{$hash}{ qw(lock_type space_id page_no n_bits index db table txn_id lock_mode) }
|
|
= $lock
|
|
=~ m{^(RECORD|TABLE) LOCKS? (?:space id $d page no $d n bits $d index `?$n`? of )?table `$n(?:/|`\.`)$n` trx id $t lock.mode (\S+)}m;
|
|
( $hash->{'special'} )
|
|
= $lock =~ m/^(?:RECORD|TABLE) .*? locks (rec but not gap|gap before rec)/m;
|
|
$hash->{'insert_intention'}
|
|
= $lock =~ m/^(?:RECORD|TABLE) .*? insert intention/m ? 1 : 0;
|
|
$hash->{'waiting'}
|
|
= $lock =~ m/^(?:RECORD|TABLE) .*? waiting/m ? 1 : 0;
|
|
|
|
# Some things may not be in the text, so make sure they are not
|
|
# undef.
|
|
map { $hash->{$_} ||= 0 } qw(n_bits page_no space_id);
|
|
map { $hash->{$_} ||= "" } qw(index special);
|
|
push @result, $hash;
|
|
}
|
|
|
|
return @result;
|
|
}
|
|
|
|
sub parse_tx_text {
|
|
my ( $txn, $complete, $debug, $full ) = @_;
|
|
|
|
my ( $txn_id, $txn_status, $active_secs, $proc_no, $os_thread_id )
|
|
= $txn
|
|
=~ m/^(?:---)?TRANSACTION $t, (\D*?)(?: $d sec)?, (?:process no $d, )?OS thread id $d/m;
|
|
my ( $thread_status, $thread_decl_inside )
|
|
= $txn
|
|
=~ m/OS thread id \d+(?: ([^,]+?))?(?:, thread declared inside InnoDB $d)?$/m;
|
|
|
|
# Parsing the line that begins 'MySQL thread id' is complicated. The only
|
|
# thing always in the line is the thread and query id. See function
|
|
# innobase_mysql_print_thd in InnoDB source file sql/ha_innodb.cc.
|
|
my ( $thread_line ) = $txn =~ m/^(MySQL thread id .*)$/m;
|
|
my ( $mysql_thread_id, $query_id, $hostname, $ip, $user, $query_status );
|
|
|
|
if ( $thread_line ) {
|
|
# These parts can always be gotten.
|
|
( $mysql_thread_id, $query_id ) = $thread_line =~ m/^MySQL thread id $d, query id $d/m;
|
|
|
|
# If it's a master/slave thread, "Has (read|sent) all" may be the thread's
|
|
# proc_info. In these cases, there won't be any host/ip/user info
|
|
( $query_status ) = $thread_line =~ m/(Has (?:read|sent) all .*$)/m;
|
|
if ( defined($query_status) ) {
|
|
$user = 'system user';
|
|
}
|
|
|
|
# It may be the case that the query id is the last thing in the line.
|
|
elsif ( $thread_line =~ m/query id \d+ / ) {
|
|
# The IP address is the only non-word thing left, so it's the most
|
|
# useful marker for where I have to start guessing.
|
|
( $hostname, $ip ) = $thread_line =~ m/query id \d+(?: ([A-Za-z]\S+))? $i/m;
|
|
if ( defined $ip ) {
|
|
( $user, $query_status ) = $thread_line =~ m/$ip $w(?: (.*))?$/;
|
|
}
|
|
else { # OK, there wasn't an IP address.
|
|
# There might not be ANYTHING except the query status.
|
|
( $query_status ) = $thread_line =~ m/query id \d+ (.*)$/;
|
|
if ( $query_status !~ m/^\w+ing/ && !exists($is_proc_info{$query_status}) ) {
|
|
# The remaining tokens are, in order: hostname, user, query_status.
|
|
# It's basically impossible to know which is which.
|
|
( $hostname, $user, $query_status ) = $thread_line
|
|
=~ m/query id \d+(?: ([A-Za-z]\S+))?(?: $w(?: (.*))?)?$/m;
|
|
}
|
|
else {
|
|
$user = 'system user';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
my ( $lock_wait_status, $lock_structs, $heap_size, $row_locks, $undo_log_entries )
|
|
= $txn
|
|
=~ m/^(?:(\D*) )?$d lock struct\(s\), heap size $d(?:, $d row lock\(s\))?(?:, undo log entries $d)?$/m;
|
|
my ( $lock_wait_time )
|
|
= $txn
|
|
=~ m/^------- TRX HAS BEEN WAITING $d SEC/m;
|
|
|
|
my $locks;
|
|
# If the transaction has locks, grab the locks.
|
|
if ( $txn =~ m/^TABLE LOCK|RECORD LOCKS/ ) {
|
|
$locks = [parse_innodb_record_locks($txn, $complete, $debug, $full)];
|
|
}
|
|
|
|
my ( $tables_in_use, $tables_locked )
|
|
= $txn
|
|
=~ m/^mysql tables in use $d, locked $d$/m;
|
|
my ( $txn_doesnt_see_ge, $txn_sees_lt )
|
|
= $txn
|
|
=~ m/^Trx read view will not see trx with id >= $t, sees < $t$/m;
|
|
my $has_read_view = defined($txn_doesnt_see_ge);
|
|
# Only a certain number of bytes of the query text are included here, at least
|
|
# under some circumstances. Some versions include 300, some 600.
|
|
my ( $query_text )
|
|
= $txn
|
|
=~ m{
|
|
^MySQL\sthread\sid\s[^\n]+\n # This comes before the query text
|
|
(.*?) # The query text
|
|
(?= # Followed by any of...
|
|
^Trx\sread\sview
|
|
|^-------\sTRX\sHAS\sBEEN\sWAITING
|
|
|^TABLE\sLOCK
|
|
|^RECORD\sLOCKS\sspace\sid
|
|
|^(?:---)?TRANSACTION
|
|
|^\*\*\*\s\(\d\)
|
|
|\Z
|
|
)
|
|
}xms;
|
|
if ( $query_text ) {
|
|
$query_text =~ s/\s+$//;
|
|
}
|
|
else {
|
|
$query_text = '';
|
|
}
|
|
|
|
my %stuff = (
|
|
active_secs => $active_secs,
|
|
has_read_view => $has_read_view,
|
|
heap_size => $heap_size,
|
|
hostname => $hostname,
|
|
ip => $ip,
|
|
lock_structs => $lock_structs,
|
|
lock_wait_status => $lock_wait_status,
|
|
lock_wait_time => $lock_wait_time,
|
|
mysql_thread_id => $mysql_thread_id,
|
|
os_thread_id => $os_thread_id,
|
|
proc_no => $proc_no,
|
|
query_id => $query_id,
|
|
query_status => $query_status,
|
|
query_text => $query_text,
|
|
row_locks => $row_locks,
|
|
tables_in_use => $tables_in_use,
|
|
tables_locked => $tables_locked,
|
|
thread_decl_inside => $thread_decl_inside,
|
|
thread_status => $thread_status,
|
|
txn_doesnt_see_ge => $txn_doesnt_see_ge,
|
|
txn_id => $txn_id,
|
|
txn_sees_lt => $txn_sees_lt,
|
|
txn_status => $txn_status,
|
|
undo_log_entries => $undo_log_entries,
|
|
user => $user,
|
|
);
|
|
$stuff{'fulltext'} = $txn if $debug;
|
|
$stuff{'locks'} = $locks if $locks;
|
|
|
|
# Some things may not be in the txn text, so make sure they are not
|
|
# undef.
|
|
map { $stuff{$_} ||= 0 } qw(active_secs heap_size lock_structs
|
|
tables_in_use undo_log_entries tables_locked has_read_view
|
|
thread_decl_inside lock_wait_time proc_no row_locks);
|
|
map { $stuff{$_} ||= "" } qw(thread_status txn_doesnt_see_ge
|
|
txn_sees_lt query_status ip query_text lock_wait_status user);
|
|
$stuff{'hostname'} ||= $stuff{'ip'};
|
|
|
|
return \%stuff;
|
|
}
|
|
|
|
sub parse_tx_section {
|
|
my ( $section, $complete, $debug, $full ) = @_;
|
|
return unless $section && $section->{'fulltext'};
|
|
my $fulltext = $section->{'fulltext'};
|
|
$section->{'transactions'} = [];
|
|
|
|
# Handle the individual transactions
|
|
my @transactions = $fulltext =~ m/(---TRANSACTION \d.*?)(?=\n---TRANSACTION|$)/gs;
|
|
foreach my $txn ( @transactions ) {
|
|
my $stuff = parse_tx_text( $txn, $complete, $debug, $full );
|
|
delete $stuff->{'fulltext'} unless $debug;
|
|
push @{$section->{'transactions'}}, $stuff;
|
|
}
|
|
|
|
# Handle the general info
|
|
@{$section}{ 'trx_id_counter' }
|
|
= $fulltext =~ m/^Trx id counter $t$/m;
|
|
@{$section}{ 'purge_done_for', 'purge_undo_for' }
|
|
= $fulltext =~ m/^Purge done for trx's n:o < $t undo n:o < $t$/m;
|
|
@{$section}{ 'history_list_len' } # This isn't present in some 4.x versions
|
|
= $fulltext =~ m/^History list length $d$/m;
|
|
@{$section}{ 'num_lock_structs' }
|
|
= $fulltext =~ m/^Total number of lock structs in row lock hash table $d$/m;
|
|
@{$section}{ 'is_truncated' }
|
|
= $fulltext =~ m/^\.\.\. truncated\.\.\.$/m ? 1 : 0;
|
|
|
|
# Fill in things that might not be present
|
|
foreach ( qw(history_list_len) ) {
|
|
$section->{$_} ||= 0;
|
|
}
|
|
|
|
delete $section->{'fulltext'} unless $debug;
|
|
return 1;
|
|
}
|
|
|
|
# I've read the source for this section.
|
|
sub parse_ro_section {
|
|
my ( $section, $complete, $debug, $full ) = @_;
|
|
return unless $section && $section->{'fulltext'};
|
|
my $fulltext = $section->{'fulltext'};
|
|
|
|
# Grab the info
|
|
@{$section}{ 'queries_inside', 'queries_in_queue' }
|
|
= $fulltext =~ m/^$d queries inside InnoDB, $d queries in queue$/m;
|
|
( $section->{ 'read_views_open' } )
|
|
= $fulltext =~ m/^$d read views open inside InnoDB$/m;
|
|
( $section->{ 'n_reserved_extents' } )
|
|
= $fulltext =~ m/^$d tablespace extents now reserved for B-tree/m;
|
|
@{$section}{ 'main_thread_proc_no', 'main_thread_id', 'main_thread_state' }
|
|
= $fulltext =~ m/^Main thread (?:process no. $d, )?id $d, state: (.*)$/m;
|
|
@{$section}{ 'num_rows_ins', 'num_rows_upd', 'num_rows_del', 'num_rows_read' }
|
|
= $fulltext =~ m/^Number of rows inserted $d, updated $d, deleted $d, read $d$/m;
|
|
@{$section}{ 'ins_sec', 'upd_sec', 'del_sec', 'read_sec' }
|
|
= $fulltext =~ m#^$f inserts/s, $f updates/s, $f deletes/s, $f reads/s$#m;
|
|
$section->{'main_thread_proc_no'} ||= 0;
|
|
|
|
map { $section->{$_} ||= 0 } qw(read_views_open n_reserved_extents);
|
|
delete $section->{'fulltext'} unless $debug;
|
|
return 1;
|
|
}
|
|
|
|
sub parse_lg_section {
|
|
my ( $section, $complete, $debug, $full ) = @_;
|
|
return unless $section;
|
|
my $fulltext = $section->{'fulltext'};
|
|
|
|
# Grab the info
|
|
( $section->{ 'log_seq_no' } )
|
|
= $fulltext =~ m/Log sequence number \s*(\d.*)$/m;
|
|
( $section->{ 'log_flushed_to' } )
|
|
= $fulltext =~ m/Log flushed up to \s*(\d.*)$/m;
|
|
( $section->{ 'last_chkp' } )
|
|
= $fulltext =~ m/Last checkpoint at \s*(\d.*)$/m;
|
|
@{$section}{ 'pending_log_writes', 'pending_chkp_writes' }
|
|
= $fulltext =~ m/$d pending log writes, $d pending chkp writes/;
|
|
@{$section}{ 'log_ios_done', 'log_ios_s' }
|
|
= $fulltext =~ m#$d log i/o's done, $f log i/o's/second#;
|
|
|
|
delete $section->{'fulltext'} unless $debug;
|
|
return 1;
|
|
}
|
|
|
|
sub parse_ib_section {
|
|
my ( $section, $complete, $debug, $full ) = @_;
|
|
return unless $section && $section->{'fulltext'};
|
|
my $fulltext = $section->{'fulltext'};
|
|
|
|
# Some servers will output ibuf information for tablespace 0, as though there
|
|
# might be many tablespaces with insert buffers. (In practice I believe
|
|
# the source code shows there will only ever be one). I have to parse both
|
|
# cases here, but I assume there will only be one.
|
|
@{$section}{ 'size', 'free_list_len', 'seg_size' }
|
|
= $fulltext =~ m/^Ibuf(?: for space 0)?: size $d, free list len $d, seg size $d,$/m;
|
|
@{$section}{ 'inserts', 'merged_recs', 'merges' }
|
|
= $fulltext =~ m/^$d inserts, $d merged recs, $d merges$/m;
|
|
|
|
@{$section}{ 'hash_table_size', 'used_cells', 'bufs_in_node_heap' }
|
|
= $fulltext =~ m/^Hash table size $d, used cells $d, node heap has $d buffer\(s\)$/m;
|
|
@{$section}{ 'hash_searches_s', 'non_hash_searches_s' }
|
|
= $fulltext =~ m{^$f hash searches/s, $f non-hash searches/s$}m;
|
|
|
|
delete $section->{'fulltext'} unless $debug;
|
|
return 1;
|
|
}
|
|
|
|
sub parse_wait_array {
|
|
my ( $text, $complete, $debug, $full ) = @_;
|
|
my %result;
|
|
|
|
@result{ qw(thread waited_at_filename waited_at_line waited_secs) }
|
|
= $text =~ m/^--Thread $d has waited at $fl for $f seconds/m;
|
|
|
|
# Depending on whether it's a SYNC_MUTEX,RW_LOCK_EX,RW_LOCK_SHARED,
|
|
# there will be different text output
|
|
if ( $text =~ m/^Mutex at/m ) {
|
|
$result{'request_type'} = 'M';
|
|
@result{ qw( lock_mem_addr lock_cfile_name lock_cline lock_var) }
|
|
= $text =~ m/^Mutex at $h created file $fl, lock var $d$/m;
|
|
@result{ qw( waiters_flag )}
|
|
= $text =~ m/^waiters flag $d$/m;
|
|
}
|
|
else {
|
|
@result{ qw( request_type lock_mem_addr lock_cfile_name lock_cline) }
|
|
= $text =~ m/^(.)-lock on RW-latch at $h created in file $fl$/m;
|
|
@result{ qw( writer_thread writer_lock_mode ) }
|
|
= $text =~ m/^a writer \(thread id $d\) has reserved it in mode (.*)$/m;
|
|
@result{ qw( num_readers waiters_flag )}
|
|
= $text =~ m/^number of readers $d, waiters flag $d$/m;
|
|
@result{ qw(last_s_file_name last_s_line ) }
|
|
= $text =~ m/Last time read locked in file $fl$/m;
|
|
@result{ qw(last_x_file_name last_x_line ) }
|
|
= $text =~ m/Last time write locked in file $fl$/m;
|
|
}
|
|
|
|
$result{'cell_waiting'} = $text =~ m/^wait has ended$/m ? 0 : 1;
|
|
$result{'cell_event_set'} = $text =~ m/^wait is ending$/m ? 1 : 0;
|
|
|
|
# Because there are two code paths, some things won't get set.
|
|
map { $result{$_} ||= '' }
|
|
qw(last_s_file_name last_x_file_name writer_lock_mode);
|
|
map { $result{$_} ||= 0 }
|
|
qw(num_readers lock_var last_s_line last_x_line writer_thread);
|
|
|
|
return \%result;
|
|
}
|
|
|
|
sub parse_sm_section {
|
|
my ( $section, $complete, $debug, $full ) = @_;
|
|
return 0 unless $section && $section->{'fulltext'};
|
|
my $fulltext = $section->{'fulltext'};
|
|
|
|
# Grab the info
|
|
@{$section}{ 'reservation_count', 'signal_count' }
|
|
= $fulltext =~ m/^OS WAIT ARRAY INFO: reservation count $d, signal count $d$/m;
|
|
@{$section}{ 'mutex_spin_waits', 'mutex_spin_rounds', 'mutex_os_waits' }
|
|
= $fulltext =~ m/^Mutex spin waits $d, rounds $d, OS waits $d$/m;
|
|
@{$section}{ 'rw_shared_spins', 'rw_shared_os_waits', 'rw_excl_spins', 'rw_excl_os_waits' }
|
|
= $fulltext =~ m/^RW-shared spins $d, OS waits $d; RW-excl spins $d, OS waits $d$/m;
|
|
|
|
# Look for info on waits.
|
|
my @waits = $fulltext =~ m/^(--Thread.*?)^(?=Mutex spin|--Thread)/gms;
|
|
$section->{'waits'} = [ map { parse_wait_array($_, $complete, $debug) } @waits ];
|
|
$section->{'wait_array_size'} = scalar(@waits);
|
|
|
|
delete $section->{'fulltext'} unless $debug;
|
|
return 1;
|
|
}
|
|
|
|
# I've read the source for this section.
|
|
sub parse_bp_section {
|
|
my ( $section, $complete, $debug, $full ) = @_;
|
|
return unless $section && $section->{'fulltext'};
|
|
my $fulltext = $section->{'fulltext'};
|
|
|
|
# Grab the info
|
|
@{$section}{ 'total_mem_alloc', 'add_pool_alloc' }
|
|
= $fulltext =~ m/^Total memory allocated $d; in additional pool allocated $d$/m;
|
|
@{$section}{'dict_mem_alloc'} = $fulltext =~ m/Dictionary memory allocated $d/;
|
|
@{$section}{'awe_mem_alloc'} = $fulltext =~ m/$d MB of AWE memory/;
|
|
@{$section}{'buf_pool_size'} = $fulltext =~ m/^Buffer pool size\s*$d$/m;
|
|
@{$section}{'buf_free'} = $fulltext =~ m/^Free buffers\s*$d$/m;
|
|
@{$section}{'pages_total'} = $fulltext =~ m/^Database pages\s*$d$/m;
|
|
@{$section}{'pages_modified'} = $fulltext =~ m/^Modified db pages\s*$d$/m;
|
|
@{$section}{'pages_read', 'pages_created', 'pages_written'}
|
|
= $fulltext =~ m/^Pages read $d, created $d, written $d$/m;
|
|
@{$section}{'page_reads_sec', 'page_creates_sec', 'page_writes_sec'}
|
|
= $fulltext =~ m{^$f reads/s, $f creates/s, $f writes/s$}m;
|
|
@{$section}{'buf_pool_hits', 'buf_pool_reads'}
|
|
= $fulltext =~ m{Buffer pool hit rate $d / $d$}m;
|
|
if ($fulltext =~ m/^No buffer pool page gets since the last printout$/m) {
|
|
@{$section}{'buf_pool_hits', 'buf_pool_reads'} = (0, 0);
|
|
@{$section}{'buf_pool_hit_rate'} = '--';
|
|
}
|
|
else {
|
|
@{$section}{'buf_pool_hit_rate'}
|
|
= $fulltext =~ m{Buffer pool hit rate (\d+ / \d+)$}m;
|
|
}
|
|
@{$section}{'reads_pending'} = $fulltext =~ m/^Pending reads $d/m;
|
|
@{$section}{'writes_pending_lru', 'writes_pending_flush_list', 'writes_pending_single_page' }
|
|
= $fulltext =~ m/^Pending writes: LRU $d, flush list $d, single page $d$/m;
|
|
|
|
map { $section->{$_} ||= 0 }
|
|
qw(writes_pending_lru writes_pending_flush_list writes_pending_single_page
|
|
awe_mem_alloc dict_mem_alloc);
|
|
@{$section}{'writes_pending'} = List::Util::sum(
|
|
@{$section}{ qw(writes_pending_lru writes_pending_flush_list writes_pending_single_page) });
|
|
|
|
delete $section->{'fulltext'} unless $debug;
|
|
return 1;
|
|
}
|
|
|
|
# I've read the source for this.
|
|
sub parse_io_section {
|
|
my ( $section, $complete, $debug, $full ) = @_;
|
|
return unless $section && $section->{'fulltext'};
|
|
my $fulltext = $section->{'fulltext'};
|
|
$section->{'threads'} = {};
|
|
|
|
# Grab the I/O thread info
|
|
my @threads = $fulltext =~ m<^(I/O thread \d+ .*)$>gm;
|
|
foreach my $thread (@threads) {
|
|
my ( $tid, $state, $purpose, $event_set )
|
|
= $thread =~ m{I/O thread $d state: (.+?) \((.*)\)(?: ev set)?$}m;
|
|
if ( defined $tid ) {
|
|
$section->{'threads'}->{$tid} = {
|
|
thread => $tid,
|
|
state => $state,
|
|
purpose => $purpose,
|
|
event_set => $event_set ? 1 : 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
# Grab the reads/writes/flushes info
|
|
@{$section}{ 'pending_normal_aio_reads', 'pending_aio_writes' }
|
|
= $fulltext =~ m/^Pending normal aio reads: $d, aio writes: $d,$/m;
|
|
@{$section}{ 'pending_ibuf_aio_reads', 'pending_log_ios', 'pending_sync_ios' }
|
|
= $fulltext =~ m{^ ibuf aio reads: $d, log i/o's: $d, sync i/o's: $d$}m;
|
|
@{$section}{ 'flush_type', 'pending_log_flushes', 'pending_buffer_pool_flushes' }
|
|
= $fulltext =~ m/^Pending flushes \($w\) log: $d; buffer pool: $d$/m;
|
|
@{$section}{ 'os_file_reads', 'os_file_writes', 'os_fsyncs' }
|
|
= $fulltext =~ m/^$d OS file reads, $d OS file writes, $d OS fsyncs$/m;
|
|
@{$section}{ 'reads_s', 'avg_bytes_s', 'writes_s', 'fsyncs_s' }
|
|
= $fulltext =~ m{^$f reads/s, $d avg bytes/read, $f writes/s, $f fsyncs/s$}m;
|
|
@{$section}{ 'pending_preads', 'pending_pwrites' }
|
|
= $fulltext =~ m/$d pending preads, $d pending pwrites$/m;
|
|
@{$section}{ 'pending_preads', 'pending_pwrites' } = (0, 0)
|
|
unless defined($section->{'pending_preads'});
|
|
|
|
delete $section->{'fulltext'} unless $debug;
|
|
return 1;
|
|
}
|
|
|
|
sub _debug {
|
|
my ( $debug, $msg ) = @_;
|
|
if ( $debug ) {
|
|
die $msg;
|
|
}
|
|
else {
|
|
warn $msg;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
1;
|
|
|
|
# end_of_package InnoDBParser
|
|
|
|
package main;
|
|
|
|
use sigtrap qw(handler finish untrapped normal-signals);
|
|
|
|
use Data::Dumper;
|
|
use DBI;
|
|
use English qw(-no_match_vars);
|
|
use File::Basename qw(dirname);
|
|
use File::Temp;
|
|
use Getopt::Long;
|
|
use List::Util qw(max min maxstr sum);
|
|
use POSIX qw(ceil);
|
|
use Time::HiRes qw(time sleep);
|
|
use Term::ReadKey qw(ReadMode ReadKey);
|
|
|
|
# License and warranty information. {{{1
|
|
# ###########################################################################
|
|
|
|
my $innotop_license = <<"LICENSE";
|
|
|
|
This is innotop version $VERSION, a MySQL and InnoDB monitor.
|
|
|
|
This program is copyright (c) 2006 Baron Schwartz.
|
|
Feedback and improvements are welcome.
|
|
|
|
THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
|
|
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
|
|
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
|
|
|
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, version 2; OR the Perl Artistic License. On UNIX and similar
|
|
systems, you can issue `man perlgpl' or `man perlartistic' to read these
|
|
licenses.
|
|
|
|
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.
|
|
LICENSE
|
|
|
|
# Configuration information and global setup {{{1
|
|
# ###########################################################################
|
|
|
|
# Really, really, super-global variables.
|
|
my @config_versions = (
|
|
"000-000-000", "001-003-000", # config file was one big name-value hash.
|
|
"001-003-000", "001-004-002", # config file contained non-user-defined stuff.
|
|
);
|
|
|
|
my $clear_screen_sub;
|
|
my $dsn_parser = new DSNParser();
|
|
|
|
# This defines expected properties and defaults for the column definitions that
|
|
# eventually end up in tbl_meta.
|
|
my %col_props = (
|
|
hdr => '',
|
|
just => '-',
|
|
dec => 0, # Whether to align the column on the decimal point
|
|
num => 0,
|
|
label => '',
|
|
user => 0,
|
|
src => '',
|
|
tbl => '', # Helps when writing/reading custom columns in config files
|
|
minw => 0,
|
|
maxw => 0,
|
|
trans => [],
|
|
agg => 'first', # Aggregate function
|
|
aggonly => 0, # Whether to show only when tbl_meta->{aggregate} is true
|
|
);
|
|
|
|
# Actual DBI connections to MySQL servers.
|
|
my %dbhs;
|
|
|
|
# Command-line parameters {{{2
|
|
# ###########################################################################
|
|
|
|
my @opt_spec = (
|
|
{ s => 'help', d => 'Show this help message' },
|
|
{ s => 'color|C!', d => 'Use terminal coloring (default)', c => 'color' },
|
|
{ s => 'config|c=s', d => 'Config file to read' },
|
|
{ s => 'nonint|n', d => 'Non-interactive, output tab-separated fields' },
|
|
{ s => 'count=i', d => 'Number of updates before exiting' },
|
|
{ s => 'delay|d=f', d => 'Delay between updates in seconds', c => 'interval' },
|
|
{ s => 'mode|m=s', d => 'Operating mode to start in', c => 'mode' },
|
|
{ s => 'inc|i!', d => 'Measure incremental differences', c => 'status_inc' },
|
|
{ s => 'write|w', d => 'Write running configuration into home directory if no config files were loaded' },
|
|
{ s => 'skipcentral|s', d => 'Skip reading the central configuration file' },
|
|
{ s => 'version', d => 'Output version information and exit' },
|
|
{ s => 'user|u=s', d => 'User for login if not current user' },
|
|
{ s => 'password|p=s', d => 'Password to use for connection' },
|
|
{ s => 'host|h=s', d => 'Connect to host' },
|
|
{ s => 'port|P=i', d => 'Port number to use for connection' },
|
|
);
|
|
|
|
# This is the container for the command-line options' values to be stored in
|
|
# after processing. Initial values are defaults.
|
|
my %opts = (
|
|
n => !( -t STDIN && -t STDOUT ), # If in/out aren't to terminals, we're interactive
|
|
);
|
|
# Post-process...
|
|
my %opt_seen;
|
|
foreach my $spec ( @opt_spec ) {
|
|
my ( $long, $short ) = $spec->{s} =~ m/^(\w+)(?:\|([^!+=]*))?/;
|
|
$spec->{k} = $short || $long;
|
|
$spec->{l} = $long;
|
|
$spec->{t} = $short;
|
|
$spec->{n} = $spec->{s} =~ m/!/;
|
|
$opts{$spec->{k}} = undef unless defined $opts{$spec->{k}};
|
|
die "Duplicate option $spec->{k}" if $opt_seen{$spec->{k}}++;
|
|
}
|
|
|
|
Getopt::Long::Configure('no_ignore_case', 'bundling');
|
|
GetOptions( map { $_->{s} => \$opts{$_->{k}} } @opt_spec) or $opts{help} = 1;
|
|
|
|
if ( $opts{version} ) {
|
|
print "innotop Ver $VERSION\n";
|
|
exit(0);
|
|
}
|
|
|
|
if ( $opts{c} and ! -f $opts{c} ) {
|
|
print $opts{c} . " doesn't exist. Exiting.\n";
|
|
exit(1);
|
|
}
|
|
if ( $opts{'help'} ) {
|
|
print "Usage: innotop <options> <innodb-status-file>\n\n";
|
|
my $maxw = max(map { length($_->{l}) + ($_->{n} ? 4 : 0)} @opt_spec);
|
|
foreach my $spec ( sort { $a->{l} cmp $b->{l} } @opt_spec ) {
|
|
my $long = $spec->{n} ? "[no]$spec->{l}" : $spec->{l};
|
|
my $short = $spec->{t} ? "-$spec->{t}" : '';
|
|
printf(" --%-${maxw}s %-4s %s\n", $long, $short, $spec->{d});
|
|
}
|
|
print <<USAGE;
|
|
|
|
innotop is a MySQL and InnoDB transaction/status monitor, like 'top' for
|
|
MySQL. It displays queries, InnoDB transactions, lock waits, deadlocks,
|
|
foreign key errors, open tables, replication status, buffer information,
|
|
row operations, logs, I/O operations, load graph, and more. You can
|
|
monitor many servers at once with innotop.
|
|
|
|
USAGE
|
|
exit(1);
|
|
}
|
|
|
|
# Meta-data (table definitions etc) {{{2
|
|
# ###########################################################################
|
|
|
|
# Expressions {{{3
|
|
# Convenience so I can copy/paste these in several places...
|
|
# ###########################################################################
|
|
my %exprs = (
|
|
Host => q{my $host = host || hostname || ''; ($host) = $host =~ m/^((?:[\d.]+(?=:))|(?:[a-zA-Z]\w+))/; return $host || ''},
|
|
Port => q{my ($p) = host =~ m/:(.*)$/; return $p || 0},
|
|
OldVersions => q{dulint_to_int(IB_tx_trx_id_counter) - dulint_to_int(IB_tx_purge_done_for)},
|
|
MaxTxnTime => q/max(map{ $_->{active_secs} } @{ IB_tx_transactions }) || 0/,
|
|
NumTxns => q{scalar @{ IB_tx_transactions } },
|
|
DirtyBufs => q{ $cur->{IB_bp_pages_modified} / ($cur->{IB_bp_buf_pool_size} || 1) },
|
|
BufPoolFill => q{ $cur->{IB_bp_pages_total} / ($cur->{IB_bp_buf_pool_size} || 1) },
|
|
ServerLoad => q{ $cur->{Threads_connected}/(Questions||1)/Uptime_hires },
|
|
TxnTimeRemain => q{ defined undo_log_entries && defined $pre->{undo_log_entries} && undo_log_entries < $pre->{undo_log_entries} ? undo_log_entries / (($pre->{undo_log_entries} - undo_log_entries)/((active_secs-$pre->{active_secs})||1))||1 : 0},
|
|
SlaveCatchupRate => ' defined $cur->{seconds_behind_master} && defined $pre->{seconds_behind_master} && $cur->{seconds_behind_master} < $pre->{seconds_behind_master} ? ($pre->{seconds_behind_master}-$cur->{seconds_behind_master})/($cur->{Uptime_hires}-$pre->{Uptime_hires}) : 0',
|
|
QcacheHitRatio => q{(Qcache_hits||0)/(((Com_select||0)+(Qcache_hits||0))||1)},
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Column definitions {{{3
|
|
# Defines every column in every table. A named column has the following
|
|
# properties:
|
|
# * hdr Column header/title
|
|
# * label Documentation for humans.
|
|
# * num Whether it's numeric (for sorting).
|
|
# * just Alignment; generated from num, user-overridable in tbl_meta
|
|
# * minw, maxw Auto-generated, user-overridable.
|
|
# Values from this hash are just copied to tbl_meta, which is where everything
|
|
# else in the program should read from.
|
|
# ###########################################################################
|
|
|
|
my %columns = (
|
|
active_secs => { hdr => 'SecsActive', num => 1, label => 'Seconds transaction has been active', },
|
|
add_pool_alloc => { hdr => 'Add\'l Pool', num => 1, label => 'Additonal pool allocated' },
|
|
attempted_op => { hdr => 'Action', num => 0, label => 'The action that caused the error' },
|
|
awe_mem_alloc => { hdr => 'AWE Memory', num => 1, label => '[Windows] AWE memory allocated' },
|
|
binlog_cache_overflow => { hdr => 'Binlog Cache', num => 1, label => 'Transactions too big for binlog cache that went to disk' },
|
|
binlog_do_db => { hdr => 'Binlog Do DB', num => 0, label => 'binlog-do-db setting' },
|
|
binlog_ignore_db => { hdr => 'Binlog Ignore DB', num => 0, label => 'binlog-ignore-db setting' },
|
|
bps_in => { hdr => 'BpsIn', num => 1, label => 'Bytes per second received by the server', },
|
|
bps_out => { hdr => 'BpsOut', num => 1, label => 'Bytes per second sent by the server', },
|
|
buf_free => { hdr => 'Free Bufs', num => 1, label => 'Buffers free in the buffer pool' },
|
|
buf_pool_hit_rate => { hdr => 'Hit Rate', num => 0, label => 'Buffer pool hit rate' },
|
|
buf_pool_hits => { hdr => 'Hits', num => 1, label => 'Buffer pool hits' },
|
|
buf_pool_reads => { hdr => 'Reads', num => 1, label => 'Buffer pool reads' },
|
|
buf_pool_size => { hdr => 'Size', num => 1, label => 'Buffer pool size' },
|
|
bufs_in_node_heap => { hdr => 'Node Heap Bufs', num => 1, label => 'Buffers in buffer pool node heap' },
|
|
bytes_behind_master => { hdr => 'ByteLag', num => 1, label => 'Bytes the slave lags the master in binlog' },
|
|
cell_event_set => { hdr => 'Ending?', num => 1, label => 'Whether the cell event is set' },
|
|
cell_waiting => { hdr => 'Waiting?', num => 1, label => 'Whether the cell is waiting' },
|
|
child_db => { hdr => 'Child DB', num => 0, label => 'The database of the child table' },
|
|
child_index => { hdr => 'Child Index', num => 0, label => 'The index in the child table' },
|
|
child_table => { hdr => 'Child Table', num => 0, label => 'The child table' },
|
|
cmd => { hdr => 'Cmd', num => 0, label => 'Type of command being executed', },
|
|
cnt => { hdr => 'Cnt', num => 0, label => 'Count', agg => 'count', aggonly => 1 },
|
|
connect_retry => { hdr => 'Connect Retry', num => 1, label => 'Slave connect-retry timeout' },
|
|
cxn => { hdr => 'CXN', num => 0, label => 'Connection from which the data came', },
|
|
db => { hdr => 'DB', num => 0, label => 'Current database', },
|
|
dict_mem_alloc => { hdr => 'Dict Mem', num => 1, label => 'Dictionary memory allocated' },
|
|
dirty_bufs => { hdr => 'Dirty Buf', num => 1, label => 'Dirty buffer pool pages' },
|
|
dl_txn_num => { hdr => 'Num', num => 0, label => 'Deadlocked transaction number', },
|
|
event_set => { hdr => 'Evt Set?', num => 1, label => '[Win32] if a wait event is set', },
|
|
exec_master_log_pos => { hdr => 'Exec Master Log Pos', num => 1, label => 'Exec Master Log Position' },
|
|
fk_name => { hdr => 'Constraint', num => 0, label => 'The name of the FK constraint' },
|
|
free_list_len => { hdr => 'Free List Len', num => 1, label => 'Length of the free list' },
|
|
has_read_view => { hdr => 'Rd View', num => 1, label => 'Whether the transaction has a read view' },
|
|
hash_searches_s => { hdr => 'Hash/Sec', num => 1, label => 'Number of hash searches/sec' },
|
|
hash_table_size => { hdr => 'Size', num => 1, label => 'Number of non-hash searches/sec' },
|
|
heap_no => { hdr => 'Heap', num => 1, label => 'Heap number' },
|
|
heap_size => { hdr => 'Heap', num => 1, label => 'Heap size' },
|
|
history_list_len => { hdr => 'History', num => 1, label => 'History list length' },
|
|
host_and_domain => { hdr => 'Host', num => 0, label => 'Hostname/IP and domain' },
|
|
host_and_port => { hdr => 'Host/IP', num => 0, label => 'Hostname or IP address, and port number', },
|
|
hostname => { hdr => 'Host', num => 0, label => 'Hostname' },
|
|
index => { hdr => 'Index', num => 0, label => 'The index involved' },
|
|
index_ref => { hdr => 'Index Ref', num => 0, label => 'Index referenced' },
|
|
info => { hdr => 'Query', num => 0, label => 'Info or the current query', },
|
|
insert_intention => { hdr => 'Ins Intent', num => 1, label => 'Whether the thread was trying to insert' },
|
|
inserts => { hdr => 'Inserts', num => 1, label => 'Inserts' },
|
|
io_bytes_s => { hdr => 'Bytes/Sec', num => 1, label => 'Average I/O bytes/sec' },
|
|
io_flush_type => { hdr => 'Flush Type', num => 0, label => 'I/O Flush Type' },
|
|
io_fsyncs_s => { hdr => 'fsyncs/sec', num => 1, label => 'I/O fsyncs/sec' },
|
|
io_reads_s => { hdr => 'Reads/Sec', num => 1, label => 'Average I/O reads/sec' },
|
|
io_writes_s => { hdr => 'Writes/Sec', num => 1, label => 'Average I/O writes/sec' },
|
|
ip => { hdr => 'IP', num => 0, label => 'IP address' },
|
|
is_name_locked => { hdr => 'Locked', num => 1, label => 'Whether table is name locked', },
|
|
key_buffer_hit => { hdr => 'KCacheHit', num => 1, label => 'Key cache hit ratio', },
|
|
key_len => { hdr => 'Key Length', num => 1, label => 'Number of bytes used in the key' },
|
|
last_chkp => { hdr => 'Last Checkpoint', num => 0, label => 'Last log checkpoint' },
|
|
last_errno => { hdr => 'Last Errno', num => 1, label => 'Last error number' },
|
|
last_error => { hdr => 'Last Error', num => 0, label => 'Last error' },
|
|
last_s_file_name => { hdr => 'S-File', num => 0, label => 'Filename where last read locked' },
|
|
last_s_line => { hdr => 'S-Line', num => 1, label => 'Line where last read locked' },
|
|
last_x_file_name => { hdr => 'X-File', num => 0, label => 'Filename where last write locked' },
|
|
last_x_line => { hdr => 'X-Line', num => 1, label => 'Line where last write locked' },
|
|
last_pct => { hdr => 'Pct', num => 1, label => 'Last Percentage' },
|
|
last_total => { hdr => 'Last Total', num => 1, label => 'Last Total' },
|
|
last_value => { hdr => 'Last Incr', num => 1, label => 'Last Value' },
|
|
load => { hdr => 'Load', num => 1, label => 'Server load' },
|
|
lock_cfile_name => { hdr => 'Crtd File', num => 0, label => 'Filename where lock created' },
|
|
lock_cline => { hdr => 'Crtd Line', num => 1, label => 'Line where lock created' },
|
|
lock_mem_addr => { hdr => 'Addr', num => 0, label => 'The lock memory address' },
|
|
lock_mode => { hdr => 'Mode', num => 0, label => 'The lock mode' },
|
|
lock_structs => { hdr => 'LStrcts', num => 1, label => 'Number of lock structs' },
|
|
lock_type => { hdr => 'Type', num => 0, label => 'The lock type' },
|
|
lock_var => { hdr => 'Lck Var', num => 1, label => 'The lock variable' },
|
|
lock_wait_time => { hdr => 'Wait', num => 1, label => 'How long txn has waited for a lock' },
|
|
log_flushed_to => { hdr => 'Flushed To', num => 0, label => 'Log position flushed to' },
|
|
log_ios_done => { hdr => 'IO Done', num => 1, label => 'Log I/Os done' },
|
|
log_ios_s => { hdr => 'IO/Sec', num => 1, label => 'Average log I/Os per sec' },
|
|
log_seq_no => { hdr => 'Sequence No.', num => 0, label => 'Log sequence number' },
|
|
main_thread_id => { hdr => 'Main Thread ID', num => 1, label => 'Main thread ID' },
|
|
main_thread_proc_no => { hdr => 'Main Thread Proc', num => 1, label => 'Main thread process number' },
|
|
main_thread_state => { hdr => 'Main Thread State', num => 0, label => 'Main thread state' },
|
|
master_file => { hdr => 'File', num => 0, label => 'Master file' },
|
|
master_host => { hdr => 'Master', num => 0, label => 'Master server hostname' },
|
|
master_log_file => { hdr => 'Master Log File', num => 0, label => 'Master log file' },
|
|
master_port => { hdr => 'Master Port', num => 1, label => 'Master port' },
|
|
master_pos => { hdr => 'Position', num => 1, label => 'Master position' },
|
|
master_ssl_allowed => { hdr => 'Master SSL Allowed', num => 0, label => 'Master SSL Allowed' },
|
|
master_ssl_ca_file => { hdr => 'Master SSL CA File', num => 0, label => 'Master SSL Cert Auth File' },
|
|
master_ssl_ca_path => { hdr => 'Master SSL CA Path', num => 0, label => 'Master SSL Cert Auth Path' },
|
|
master_ssl_cert => { hdr => 'Master SSL Cert', num => 0, label => 'Master SSL Cert' },
|
|
master_ssl_cipher => { hdr => 'Master SSL Cipher', num => 0, label => 'Master SSL Cipher' },
|
|
master_ssl_key => { hdr => 'Master SSL Key', num => 0, label => 'Master SSL Key' },
|
|
master_user => { hdr => 'Master User', num => 0, label => 'Master username' },
|
|
max_txn => { hdr => 'MaxTxnTime', num => 1, label => 'MaxTxn' },
|
|
merged_recs => { hdr => 'Merged Recs', num => 1, label => 'Merged records' },
|
|
merges => { hdr => 'Merges', num => 1, label => 'Merges' },
|
|
mutex_os_waits => { hdr => 'Waits', num => 1, label => 'Mutex OS Waits' },
|
|
mutex_spin_rounds => { hdr => 'Rounds', num => 1, label => 'Mutex Spin Rounds' },
|
|
mutex_spin_waits => { hdr => 'Spins', num => 1, label => 'Mutex Spin Waits' },
|
|
mysql_thread_id => { hdr => 'ID', num => 1, label => 'MySQL connection (thread) ID', },
|
|
name => { hdr => 'Name', num => 0, label => 'Variable Name' },
|
|
n_bits => { hdr => '# Bits', num => 1, label => 'Number of bits' },
|
|
non_hash_searches_s => { hdr => 'Non-Hash/Sec', num => 1, label => 'Non-hash searches/sec' },
|
|
num_deletes => { hdr => 'Del', num => 1, label => 'Number of deletes' },
|
|
num_deletes_sec => { hdr => 'Del/Sec', num => 1, label => 'Number of deletes' },
|
|
num_inserts => { hdr => 'Ins', num => 1, label => 'Number of inserts' },
|
|
num_inserts_sec => { hdr => 'Ins/Sec', num => 1, label => 'Number of inserts' },
|
|
num_readers => { hdr => 'Readers', num => 1, label => 'Number of readers' },
|
|
num_reads => { hdr => 'Read', num => 1, label => 'Number of reads' },
|
|
num_reads_sec => { hdr => 'Read/Sec', num => 1, label => 'Number of reads' },
|
|
num_res_ext => { hdr => 'BTree Extents', num => 1, label => 'Number of extents reserved for B-Tree' },
|
|
num_rows => { hdr => 'Row Count', num => 1, label => 'Number of rows estimated to examine' },
|
|
num_times_open => { hdr => 'In Use', num => 1, label => '# times table is opened', },
|
|
num_txns => { hdr => 'Txns', num => 1, label => 'Number of transactions' },
|
|
num_updates => { hdr => 'Upd', num => 1, label => 'Number of updates' },
|
|
num_updates_sec => { hdr => 'Upd/Sec', num => 1, label => 'Number of updates' },
|
|
os_file_reads => { hdr => 'OS Reads', num => 1, label => 'OS file reads' },
|
|
os_file_writes => { hdr => 'OS Writes', num => 1, label => 'OS file writes' },
|
|
os_fsyncs => { hdr => 'OS fsyncs', num => 1, label => 'OS fsyncs' },
|
|
os_thread_id => { hdr => 'OS Thread', num => 1, label => 'The operating system thread ID' },
|
|
p_aio_writes => { hdr => 'Async Wrt', num => 1, label => 'Pending asynchronous I/O writes' },
|
|
p_buf_pool_flushes => { hdr => 'Buffer Pool Flushes', num => 1, label => 'Pending buffer pool flushes' },
|
|
p_ibuf_aio_reads => { hdr => 'IBuf Async Rds', num => 1, label => 'Pending insert buffer asynch I/O reads' },
|
|
p_log_flushes => { hdr => 'Log Flushes', num => 1, label => 'Pending log flushes' },
|
|
p_log_ios => { hdr => 'Log I/Os', num => 1, label => 'Pending log I/O operations' },
|
|
p_normal_aio_reads => { hdr => 'Async Rds', num => 1, label => 'Pending asynchronous I/O reads' },
|
|
p_preads => { hdr => 'preads', num => 1, label => 'Pending p-reads' },
|
|
p_pwrites => { hdr => 'pwrites', num => 1, label => 'Pending p-writes' },
|
|
p_sync_ios => { hdr => 'Sync I/Os', num => 1, label => 'Pending synchronous I/O operations' },
|
|
page_creates_sec => { hdr => 'Creates/Sec', num => 1, label => 'Page creates/sec' },
|
|
page_no => { hdr => 'Page', num => 1, label => 'Page number' },
|
|
page_reads_sec => { hdr => 'Reads/Sec', num => 1, label => 'Page reads per second' },
|
|
page_writes_sec => { hdr => 'Writes/Sec', num => 1, label => 'Page writes per second' },
|
|
pages_created => { hdr => 'Created', num => 1, label => 'Pages created' },
|
|
pages_modified => { hdr => 'Dirty Pages', num => 1, label => 'Pages modified (dirty)' },
|
|
pages_read => { hdr => 'Reads', num => 1, label => 'Pages read' },
|
|
pages_total => { hdr => 'Pages', num => 1, label => 'Pages total' },
|
|
pages_written => { hdr => 'Writes', num => 1, label => 'Pages written' },
|
|
parent_col => { hdr => 'Parent Column', num => 0, label => 'The referred column in the parent table', },
|
|
parent_db => { hdr => 'Parent DB', num => 0, label => 'The database of the parent table' },
|
|
parent_index => { hdr => 'Parent Index', num => 0, label => 'The referred index in the parent table' },
|
|
parent_table => { hdr => 'Parent Table', num => 0, label => 'The parent table' },
|
|
part_id => { hdr => 'Part ID', num => 1, label => 'Sub-part ID of the query' },
|
|
partitions => { hdr => 'Partitions', num => 0, label => 'Query partitions used' },
|
|
pct => { hdr => 'Pct', num => 1, label => 'Percentage' },
|
|
pending_chkp_writes => { hdr => 'Chkpt Writes', num => 1, label => 'Pending log checkpoint writes' },
|
|
pending_log_writes => { hdr => 'Log Writes', num => 1, label => 'Pending log writes' },
|
|
port => { hdr => 'Port', num => 1, label => 'Client port number', },
|
|
possible_keys => { hdr => 'Poss. Keys', num => 0, label => 'Possible keys' },
|
|
proc_no => { hdr => 'Proc', num => 1, label => 'Process number' },
|
|
q_cache_hit => { hdr => 'QCacheHit', num => 1, label => 'Query cache hit ratio', },
|
|
qps => { hdr => 'QPS', num => 1, label => 'How many queries/sec', },
|
|
queries_in_queue => { hdr => 'Queries Queued', num => 1, label => 'Queries in queue' },
|
|
queries_inside => { hdr => 'Queries Inside', num => 1, label => 'Queries inside InnoDB' },
|
|
query_id => { hdr => 'Query ID', num => 1, label => 'Query ID' },
|
|
query_status => { hdr => 'Query Status', num => 0, label => 'The query status' },
|
|
query_text => { hdr => 'Query Text', num => 0, label => 'The query text' },
|
|
questions => { hdr => 'Questions', num => 1, label => 'How many queries the server has gotten', },
|
|
read_master_log_pos => { hdr => 'Read Master Pos', num => 1, label => 'Read master log position' },
|
|
read_views_open => { hdr => 'Rd Views', num => 1, label => 'Number of read views open' },
|
|
reads_pending => { hdr => 'Pending Reads', num => 1, label => 'Reads pending' },
|
|
relay_log_file => { hdr => 'Relay File', num => 0, label => 'Relay log file' },
|
|
relay_log_pos => { hdr => 'Relay Pos', num => 1, label => 'Relay log position' },
|
|
relay_log_size => { hdr => 'Relay Size', num => 1, label => 'Relay log size' },
|
|
relay_master_log_file => { hdr => 'Relay Master File', num => 0, label => 'Relay master log file' },
|
|
replicate_do_db => { hdr => 'Do DB', num => 0, label => 'Replicate-do-db setting' },
|
|
replicate_do_table => { hdr => 'Do Table', num => 0, label => 'Replicate-do-table setting' },
|
|
replicate_ignore_db => { hdr => 'Ignore DB', num => 0, label => 'Replicate-ignore-db setting' },
|
|
replicate_ignore_table => { hdr => 'Ignore Table', num => 0, label => 'Replicate-do-table setting' },
|
|
replicate_wild_do_table => { hdr => 'Wild Do Table', num => 0, label => 'Replicate-wild-do-table setting' },
|
|
replicate_wild_ignore_table => { hdr => 'Wild Ignore Table', num => 0, label => 'Replicate-wild-ignore-table setting' },
|
|
request_type => { hdr => 'Type', num => 0, label => 'Type of lock the thread waits for' },
|
|
reservation_count => { hdr => 'ResCnt', num => 1, label => 'Reservation Count' },
|
|
row_locks => { hdr => 'RLocks', num => 1, label => 'Number of row locks' },
|
|
rw_excl_os_waits => { hdr => 'RW Waits', num => 1, label => 'R/W Excl. OS Waits' },
|
|
rw_excl_spins => { hdr => 'RW Spins', num => 1, label => 'R/W Excl. Spins' },
|
|
rw_shared_os_waits => { hdr => 'Sh Waits', num => 1, label => 'R/W Shared OS Waits' },
|
|
rw_shared_spins => { hdr => 'Sh Spins', num => 1, label => 'R/W Shared Spins' },
|
|
scan_type => { hdr => 'Type', num => 0, label => 'Scan type in chosen' },
|
|
seg_size => { hdr => 'Seg. Size', num => 1, label => 'Segment size' },
|
|
select_type => { hdr => 'Select Type', num => 0, label => 'Type of select used' },
|
|
signal_count => { hdr => 'Signals', num => 1, label => 'Signal Count' },
|
|
size => { hdr => 'Size', num => 1, label => 'Size of the tablespace' },
|
|
skip_counter => { hdr => 'Skip Counter', num => 1, label => 'Skip counter' },
|
|
slave_catchup_rate => { hdr => 'Catchup', num => 1, label => 'How fast the slave is catching up in the binlog' },
|
|
slave_io_running => { hdr => 'Slave-IO', num => 0, label => 'Whether the slave I/O thread is running' },
|
|
slave_io_state => { hdr => 'Slave IO State', num => 0, label => 'Slave I/O thread state' },
|
|
slave_open_temp_tables => { hdr => 'Temp', num => 1, label => 'Slave open temp tables' },
|
|
slave_sql_running => { hdr => 'Slave-SQL', num => 0, label => 'Whether the slave SQL thread is running' },
|
|
slow => { hdr => 'Slow', num => 1, label => 'How many slow queries', },
|
|
space_id => { hdr => 'Space', num => 1, label => 'Tablespace ID' },
|
|
special => { hdr => 'Special', num => 0, label => 'Special/Other info' },
|
|
state => { hdr => 'State', num => 0, label => 'Connection state', maxw => 18, },
|
|
tables_in_use => { hdr => 'Tbl Used', num => 1, label => 'Number of tables in use' },
|
|
tables_locked => { hdr => 'Tbl Lck', num => 1, label => 'Number of tables locked' },
|
|
tbl => { hdr => 'Table', num => 0, label => 'Table', },
|
|
thread => { hdr => 'Thread', num => 1, label => 'Thread number' },
|
|
thread_decl_inside => { hdr => 'Thread Inside', num => 0, label => 'What the thread is declared inside' },
|
|
thread_purpose => { hdr => 'Purpose', num => 0, label => "The thread's purpose" },
|
|
thread_status => { hdr => 'Thread Status', num => 0, label => 'The thread status' },
|
|
time => { hdr => 'Time', num => 1, label => 'Time since the last event', },
|
|
time_behind_master => { hdr => 'TimeLag', num => 1, label => 'Time slave lags master' },
|
|
timestring => { hdr => 'Timestring', num => 0, label => 'Time the event occurred' },
|
|
total => { hdr => 'Total', num => 1, label => 'Total' },
|
|
total_mem_alloc => { hdr => 'Memory', num => 1, label => 'Total memory allocated' },
|
|
truncates => { hdr => 'Trunc', num => 0, label => 'Whether the deadlock is truncating InnoDB status' },
|
|
txn_doesnt_see_ge => { hdr => "Txn Won't See", num => 0, label => 'Where txn read view is limited' },
|
|
txn_id => { hdr => 'ID', num => 0, label => 'Transaction ID' },
|
|
txn_sees_lt => { hdr => 'Txn Sees', num => 1, label => 'Where txn read view is limited' },
|
|
txn_status => { hdr => 'Txn Status', num => 0, label => 'Transaction status' },
|
|
txn_time_remain => { hdr => 'Remaining', num => 1, label => 'Time until txn rollback/commit completes' },
|
|
undo_log_entries => { hdr => 'Undo', num => 1, label => 'Number of undo log entries' },
|
|
undo_for => { hdr => 'Undo', num => 0, label => 'Undo for' },
|
|
until_condition => { hdr => 'Until Condition', num => 0, label => 'Slave until condition' },
|
|
until_log_file => { hdr => 'Until Log File', num => 0, label => 'Slave until log file' },
|
|
until_log_pos => { hdr => 'Until Log Pos', num => 1, label => 'Slave until log position' },
|
|
used_cells => { hdr => 'Cells Used', num => 1, label => 'Number of cells used' },
|
|
used_bufs => { hdr => 'Used Bufs', num => 1, label => 'Number of buffer pool pages used' },
|
|
user => { hdr => 'User', num => 0, label => 'Database username', },
|
|
value => { hdr => 'Value', num => 1, label => 'Value' },
|
|
versions => { hdr => 'Versions', num => 1, label => 'Number of InnoDB MVCC versions unpurged' },
|
|
victim => { hdr => 'Victim', num => 0, label => 'Whether this txn was the deadlock victim' },
|
|
wait_array_size => { hdr => 'Wait Array Size', num => 1, label => 'Wait Array Size' },
|
|
wait_status => { hdr => 'Lock Status', num => 0, label => 'Status of txn locks' },
|
|
waited_at_filename => { hdr => 'File', num => 0, label => 'Filename at which thread waits' },
|
|
waited_at_line => { hdr => 'Line', num => 1, label => 'Line at which thread waits' },
|
|
waiters_flag => { hdr => 'Waiters', num => 1, label => 'Waiters Flag' },
|
|
waiting => { hdr => 'Waiting', num => 1, label => 'Whether lock is being waited for' },
|
|
when => { hdr => 'When', num => 0, label => 'Time scale' },
|
|
writer_lock_mode => { hdr => 'Wrtr Lck Mode', num => 0, label => 'Writer lock mode' },
|
|
writer_thread => { hdr => 'Wrtr Thread', num => 1, label => 'Writer thread ID' },
|
|
writes_pending => { hdr => 'Writes', num => 1, label => 'Number of writes pending' },
|
|
writes_pending_flush_list => { hdr => 'Flush List Writes', num => 1, label => 'Number of flush list writes pending' },
|
|
writes_pending_lru => { hdr => 'LRU Writes', num => 1, label => 'Number of LRU writes pending' },
|
|
writes_pending_single_page => { hdr => '1-Page Writes', num => 1, label => 'Number of 1-page writes pending' },
|
|
);
|
|
|
|
# Apply a default property or three. By default, columns are not width-constrained,
|
|
# aligned left, and sorted alphabetically, not numerically.
|
|
foreach my $col ( values %columns ) {
|
|
map { $col->{$_} ||= 0 } qw(num minw maxw);
|
|
$col->{just} = $col->{num} ? '' : '-';
|
|
}
|
|
|
|
# Filters {{{3
|
|
# This hash defines every filter that can be applied to a table. These
|
|
# become part of tbl_meta as well. Each filter is just an expression that
|
|
# returns true or false.
|
|
# Properties of each entry:
|
|
# * func: the subroutine
|
|
# * name: the name, repeated
|
|
# * user: whether it's a user-defined filter (saved in config)
|
|
# * text: text of the subroutine
|
|
# * note: explanation
|
|
my %filters = ();
|
|
|
|
# These are pre-processed to live in %filters above, by compiling them.
|
|
my %builtin_filters = (
|
|
hide_self => {
|
|
text => <<' END',
|
|
return ( !$set->{info} || $set->{info} ne 'SHOW FULL PROCESSLIST' )
|
|
&& ( !$set->{query_text} || $set->{query_text} !~ m/INNODB STATUS$/ );
|
|
END
|
|
note => 'Removes the innotop processes from the list',
|
|
tbls => [qw(innodb_transactions processlist)],
|
|
},
|
|
hide_inactive => {
|
|
text => <<' END',
|
|
return ( !defined($set->{txn_status}) || $set->{txn_status} ne 'not started' )
|
|
&& ( !defined($set->{cmd}) || $set->{cmd} !~ m/Sleep|Binlog Dump/ )
|
|
&& ( !defined($set->{info}) || $set->{info} =~ m/\S/ );
|
|
END
|
|
note => 'Removes processes which are not doing anything',
|
|
tbls => [qw(innodb_transactions processlist)],
|
|
},
|
|
hide_slave_io => {
|
|
text => <<' END',
|
|
return !$set->{state} || $set->{state} !~ m/^(?:Waiting for master|Has read all relay)/;
|
|
END
|
|
note => 'Removes slave I/O threads from the list',
|
|
tbls => [qw(processlist slave_io_status)],
|
|
},
|
|
table_is_open => {
|
|
text => <<' END',
|
|
return $set->{num_times_open} + $set->{is_name_locked};
|
|
END
|
|
note => 'Removes tables that are not in use or locked',
|
|
tbls => [qw(open_tables)],
|
|
},
|
|
cxn_is_master => {
|
|
text => <<' END',
|
|
return $set->{master_file} ? 1 : 0;
|
|
END
|
|
note => 'Removes servers that are not masters',
|
|
tbls => [qw(master_status)],
|
|
},
|
|
cxn_is_slave => {
|
|
text => <<' END',
|
|
return $set->{master_host} ? 1 : 0;
|
|
END
|
|
note => 'Removes servers that are not slaves',
|
|
tbls => [qw(slave_io_status slave_sql_status)],
|
|
},
|
|
thd_is_not_waiting => {
|
|
text => <<' END',
|
|
return $set->{thread_status} !~ m#waiting for i/o request#;
|
|
END
|
|
note => 'Removes idle I/O threads',
|
|
tbls => [qw(io_threads)],
|
|
},
|
|
);
|
|
foreach my $key ( keys %builtin_filters ) {
|
|
my ( $sub, $err ) = compile_filter($builtin_filters{$key}->{text});
|
|
$filters{$key} = {
|
|
func => $sub,
|
|
text => $builtin_filters{$key}->{text},
|
|
user => 0,
|
|
name => $key, # useful for later
|
|
note => $builtin_filters{$key}->{note},
|
|
tbls => $builtin_filters{$key}->{tbls},
|
|
}
|
|
}
|
|
|
|
# Variable sets {{{3
|
|
# Sets (arrayrefs) of variables that are used in S mode. They are read/written to
|
|
# the config file.
|
|
my %var_sets = (
|
|
general => {
|
|
text => join(
|
|
', ',
|
|
'set_precision(Questions/Uptime_hires) as QPS',
|
|
'set_precision(Com_commit/Uptime_hires) as Commit_PS',
|
|
'set_precision((Com_rollback||0)/(Com_commit||1)) as Rollback_Commit',
|
|
'set_precision(('
|
|
. join('+', map { "($_||0)" }
|
|
qw(Com_delete Com_delete_multi Com_insert Com_insert_select Com_replace
|
|
Com_replace_select Com_select Com_update Com_update_multi))
|
|
. ')/(Com_commit||1)) as Write_Commit',
|
|
'set_precision((Com_select+(Qcache_hits||0))/(('
|
|
. join('+', map { "($_||0)" }
|
|
qw(Com_delete Com_delete_multi Com_insert Com_insert_select Com_replace
|
|
Com_replace_select Com_select Com_update Com_update_multi))
|
|
. ')||1)) as R_W_Ratio',
|
|
'set_precision(Opened_tables/Uptime_hires) as Opens_PS',
|
|
'percent($cur->{Open_tables}/($cur->{table_cache})) as Table_Cache_Used',
|
|
'set_precision(Threads_created/Uptime_hires) as Threads_PS',
|
|
'percent($cur->{Threads_cached}/($cur->{thread_cache_size}||1)) as Thread_Cache_Used',
|
|
'percent($cur->{Max_used_connections}/($cur->{max_connections}||1)) as CXN_Used_Ever',
|
|
'percent($cur->{Threads_connected}/($cur->{max_connections}||1)) as CXN_Used_Now',
|
|
),
|
|
},
|
|
commands => {
|
|
text => join(
|
|
', ',
|
|
qw(Uptime Questions Com_delete Com_delete_multi Com_insert
|
|
Com_insert_select Com_replace Com_replace_select Com_select Com_update
|
|
Com_update_multi)
|
|
),
|
|
},
|
|
query_status => {
|
|
text => join(
|
|
',',
|
|
qw( Uptime Select_full_join Select_full_range_join Select_range
|
|
Select_range_check Select_scan Slow_queries Sort_merge_passes
|
|
Sort_range Sort_rows Sort_scan)
|
|
),
|
|
},
|
|
innodb => {
|
|
text => join(
|
|
',',
|
|
qw( Uptime Innodb_row_lock_current_waits Innodb_row_lock_time
|
|
Innodb_row_lock_time_avg Innodb_row_lock_time_max Innodb_row_lock_waits
|
|
Innodb_rows_deleted Innodb_rows_inserted Innodb_rows_read
|
|
Innodb_rows_updated)
|
|
),
|
|
},
|
|
txn => {
|
|
text => join(
|
|
',',
|
|
qw( Uptime Com_begin Com_commit Com_rollback Com_savepoint
|
|
Com_xa_commit Com_xa_end Com_xa_prepare Com_xa_recover Com_xa_rollback
|
|
Com_xa_start)
|
|
),
|
|
},
|
|
key_cache => {
|
|
text => join(
|
|
',',
|
|
qw( Uptime Key_blocks_not_flushed Key_blocks_unused Key_blocks_used
|
|
Key_read_requests Key_reads Key_write_requests Key_writes )
|
|
),
|
|
},
|
|
query_cache => {
|
|
text => join(
|
|
',',
|
|
"percent($exprs{QcacheHitRatio}) as Hit_Pct",
|
|
'set_precision((Qcache_hits||0)/(Qcache_inserts||1)) as Hit_Ins',
|
|
'set_precision((Qcache_lowmem_prunes||0)/Uptime_hires) as Lowmem_Prunes_sec',
|
|
'percent(1-((Qcache_free_blocks||0)/(Qcache_total_blocks||1))) as Blocks_used',
|
|
qw( Qcache_free_blocks Qcache_free_memory Qcache_not_cached Qcache_queries_in_cache)
|
|
),
|
|
},
|
|
handler => {
|
|
text => join(
|
|
',',
|
|
qw( Uptime Handler_read_key Handler_read_first Handler_read_next
|
|
Handler_read_prev Handler_read_rnd Handler_read_rnd_next Handler_delete
|
|
Handler_update Handler_write)
|
|
),
|
|
},
|
|
cxns_files_threads => {
|
|
text => join(
|
|
',',
|
|
qw( Uptime Aborted_clients Aborted_connects Bytes_received Bytes_sent
|
|
Compression Connections Created_tmp_disk_tables Created_tmp_files
|
|
Created_tmp_tables Max_used_connections Open_files Open_streams
|
|
Open_tables Opened_tables Table_locks_immediate Table_locks_waited
|
|
Threads_cached Threads_connected Threads_created Threads_running)
|
|
),
|
|
},
|
|
prep_stmt => {
|
|
text => join(
|
|
',',
|
|
qw( Uptime Com_dealloc_sql Com_execute_sql Com_prepare_sql Com_reset
|
|
Com_stmt_close Com_stmt_execute Com_stmt_fetch Com_stmt_prepare
|
|
Com_stmt_reset Com_stmt_send_long_data )
|
|
),
|
|
},
|
|
innodb_health => {
|
|
text => join(
|
|
',',
|
|
"$exprs{OldVersions} as OldVersions",
|
|
qw(IB_sm_mutex_spin_waits IB_sm_mutex_spin_rounds IB_sm_mutex_os_waits),
|
|
"$exprs{NumTxns} as NumTxns",
|
|
"$exprs{MaxTxnTime} as MaxTxnTime",
|
|
qw(IB_ro_queries_inside IB_ro_queries_in_queue),
|
|
"set_precision($exprs{DirtyBufs} * 100) as dirty_bufs",
|
|
"set_precision($exprs{BufPoolFill} * 100) as buf_fill",
|
|
qw(IB_bp_pages_total IB_bp_pages_read IB_bp_pages_written IB_bp_pages_created)
|
|
),
|
|
},
|
|
innodb_health2 => {
|
|
text => join(
|
|
', ',
|
|
'percent(1-((Innodb_buffer_pool_pages_free||0)/($cur->{Innodb_buffer_pool_pages_total}||1))) as BP_page_cache_usage',
|
|
'percent(1-((Innodb_buffer_pool_reads||0)/(Innodb_buffer_pool_read_requests||1))) as BP_cache_hit_ratio',
|
|
'Innodb_buffer_pool_wait_free',
|
|
'Innodb_log_waits',
|
|
),
|
|
},
|
|
slow_queries => {
|
|
text => join(
|
|
', ',
|
|
'set_precision(Slow_queries/Uptime_hires) as Slow_PS',
|
|
'set_precision(Select_full_join/Uptime_hires) as Full_Join_PS',
|
|
'percent(Select_full_join/(Com_select||1)) as Full_Join_Ratio',
|
|
),
|
|
},
|
|
);
|
|
|
|
# Server sets {{{3
|
|
# Defines sets of servers between which the user can quickly switch.
|
|
my %server_groups;
|
|
|
|
# Connections {{{3
|
|
# This hash defines server connections. Each connection is a string that can be passed to
|
|
# the DBI connection. These are saved in the connections section in the config file.
|
|
my %connections;
|
|
# Defines the parts of connections.
|
|
my @conn_parts = qw(user have_user pass have_pass dsn savepass dl_table);
|
|
|
|
# Graph widths {{{3
|
|
# This hash defines the max values seen for various status/variable values, for graphing.
|
|
# These are stored in their own section in the config file. These are just initial values:
|
|
my %mvs = (
|
|
Com_select => 50,
|
|
Com_insert => 50,
|
|
Com_update => 50,
|
|
Com_delete => 50,
|
|
Questions => 100,
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Valid Term::ANSIColor color strings.
|
|
# ###########################################################################
|
|
my %ansicolors = map { $_ => 1 }
|
|
qw( black blink blue bold clear concealed cyan dark green magenta on_black
|
|
on_blue on_cyan on_green on_magenta on_red on_white on_yellow red reset
|
|
reverse underline underscore white yellow);
|
|
|
|
# ###########################################################################
|
|
# Valid comparison operators for color rules
|
|
# ###########################################################################
|
|
my %comp_ops = (
|
|
'==' => 'Numeric equality',
|
|
'>' => 'Numeric greater-than',
|
|
'<' => 'Numeric less-than',
|
|
'>=' => 'Numeric greater-than/equal',
|
|
'<=' => 'Numeric less-than/equal',
|
|
'!=' => 'Numeric not-equal',
|
|
'eq' => 'String equality',
|
|
'gt' => 'String greater-than',
|
|
'lt' => 'String less-than',
|
|
'ge' => 'String greater-than/equal',
|
|
'le' => 'String less-than/equal',
|
|
'ne' => 'String not-equal',
|
|
'=~' => 'Pattern match',
|
|
'!~' => 'Negated pattern match',
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Valid aggregate functions.
|
|
# ###########################################################################
|
|
my %agg_funcs = (
|
|
first => sub {
|
|
return $_[0]
|
|
},
|
|
count => sub {
|
|
return 0 + @_;
|
|
},
|
|
avg => sub {
|
|
my @args = grep { defined $_ } @_;
|
|
return (sum(map { m/([\d\.-]+)/g } @args) || 0) / (scalar(@args) || 1);
|
|
},
|
|
sum => sub {
|
|
my @args = grep { defined $_ } @_;
|
|
return sum(@args);
|
|
}
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Valid functions for transformations.
|
|
# ###########################################################################
|
|
my %trans_funcs = (
|
|
shorten => \&shorten,
|
|
secs_to_time => \&secs_to_time,
|
|
no_ctrl_char => \&no_ctrl_char,
|
|
percent => \&percent,
|
|
commify => \&commify,
|
|
dulint_to_int => \&dulint_to_int,
|
|
set_precision => \&set_precision,
|
|
);
|
|
|
|
# Table definitions {{{3
|
|
# This hash defines every table that can get displayed in every mode. Each
|
|
# table specifies columns and column data sources. The column is
|
|
# defined by the %columns hash.
|
|
#
|
|
# Example: foo => { src => 'bar' } means the foo column (look at
|
|
# $columns{foo} for its definition) gets its data from the 'bar' element of
|
|
# the current data set, whatever that is.
|
|
#
|
|
# These columns are post-processed after being defined, because they get stuff
|
|
# from %columns. After all the config is loaded for columns, there's more
|
|
# post-processing too; the subroutines compiled from src get added to
|
|
# the hash elements for extract_values to use.
|
|
# ###########################################################################
|
|
|
|
my %tbl_meta = (
|
|
adaptive_hash_index => {
|
|
capt => 'Adaptive Hash Index',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
hash_table_size => { src => 'IB_ib_hash_table_size', trans => [qw(shorten)], },
|
|
used_cells => { src => 'IB_ib_used_cells' },
|
|
bufs_in_node_heap => { src => 'IB_ib_bufs_in_node_heap' },
|
|
hash_searches_s => { src => 'IB_ib_hash_searches_s' },
|
|
non_hash_searches_s => { src => 'IB_ib_non_hash_searches_s' },
|
|
},
|
|
visible => [ qw(cxn hash_table_size used_cells bufs_in_node_heap hash_searches_s non_hash_searches_s) ],
|
|
filters => [],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => 'ib',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
buffer_pool => {
|
|
capt => 'Buffer Pool',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
total_mem_alloc => { src => 'IB_bp_total_mem_alloc', trans => [qw(shorten)], },
|
|
awe_mem_alloc => { src => 'IB_bp_awe_mem_alloc', trans => [qw(shorten)], },
|
|
add_pool_alloc => { src => 'IB_bp_add_pool_alloc', trans => [qw(shorten)], },
|
|
buf_pool_size => { src => 'IB_bp_buf_pool_size', trans => [qw(shorten)], },
|
|
buf_free => { src => 'IB_bp_buf_free' },
|
|
buf_pool_hit_rate => { src => 'IB_bp_buf_pool_hit_rate' },
|
|
buf_pool_reads => { src => 'IB_bp_buf_pool_reads' },
|
|
buf_pool_hits => { src => 'IB_bp_buf_pool_hits' },
|
|
dict_mem_alloc => { src => 'IB_bp_dict_mem_alloc' },
|
|
pages_total => { src => 'IB_bp_pages_total' },
|
|
pages_modified => { src => 'IB_bp_pages_modified' },
|
|
reads_pending => { src => 'IB_bp_reads_pending' },
|
|
writes_pending => { src => 'IB_bp_writes_pending' },
|
|
writes_pending_lru => { src => 'IB_bp_writes_pending_lru' },
|
|
writes_pending_flush_list => { src => 'IB_bp_writes_pending_flush_list' },
|
|
writes_pending_single_page => { src => 'IB_bp_writes_pending_single_page' },
|
|
page_creates_sec => { src => 'IB_bp_page_creates_sec' },
|
|
page_reads_sec => { src => 'IB_bp_page_reads_sec' },
|
|
page_writes_sec => { src => 'IB_bp_page_writes_sec' },
|
|
pages_created => { src => 'IB_bp_pages_created' },
|
|
pages_read => { src => 'IB_bp_pages_read' },
|
|
pages_written => { src => 'IB_bp_pages_written' },
|
|
},
|
|
visible => [ qw(cxn buf_pool_size buf_free pages_total pages_modified buf_pool_hit_rate total_mem_alloc add_pool_alloc)],
|
|
filters => [],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => 'bp',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
# TODO: a new step in set_to_tbl: join result to itself, grouped?
|
|
# TODO: this would also enable pulling Q and T data together.
|
|
# TODO: using a SQL-ish language would also allow pivots to be easier -- treat the pivoted data as a view and SELECT from it.
|
|
cmd_summary => {
|
|
capt => 'Command Summary',
|
|
cust => {},
|
|
cols => {
|
|
name => { src => 'name' },
|
|
total => { src => 'total' },
|
|
value => { src => 'value', agg => 'sum'},
|
|
pct => { src => 'value/total', trans => [qw(percent)] },
|
|
last_total => { src => 'last_total' },
|
|
last_value => { src => 'last_value', agg => 'sum'},
|
|
last_pct => { src => 'last_value/last_total', trans => [qw(percent)] },
|
|
},
|
|
visible => [qw(name value pct last_value last_pct)],
|
|
filters => [qw()],
|
|
sort_cols => '-value',
|
|
sort_dir => '1',
|
|
innodb => '',
|
|
group_by => [qw(name)],
|
|
aggregate => 1,
|
|
},
|
|
deadlock_locks => {
|
|
capt => 'Deadlock Locks',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
mysql_thread_id => { src => 'mysql_thread_id' },
|
|
dl_txn_num => { src => 'dl_txn_num' },
|
|
lock_type => { src => 'lock_type' },
|
|
space_id => { src => 'space_id' },
|
|
page_no => { src => 'page_no' },
|
|
heap_no => { src => 'heap_no' },
|
|
n_bits => { src => 'n_bits' },
|
|
index => { src => 'index' },
|
|
db => { src => 'db' },
|
|
tbl => { src => 'table' },
|
|
lock_mode => { src => 'lock_mode' },
|
|
special => { src => 'special' },
|
|
insert_intention => { src => 'insert_intention' },
|
|
waiting => { src => 'waiting' },
|
|
},
|
|
visible => [ qw(cxn mysql_thread_id waiting lock_mode db tbl index special insert_intention)],
|
|
filters => [],
|
|
sort_cols => 'cxn mysql_thread_id',
|
|
sort_dir => '1',
|
|
innodb => 'dl',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
deadlock_transactions => {
|
|
capt => 'Deadlock Transactions',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
active_secs => { src => 'active_secs' },
|
|
dl_txn_num => { src => 'dl_txn_num' },
|
|
has_read_view => { src => 'has_read_view' },
|
|
heap_size => { src => 'heap_size' },
|
|
host_and_domain => { src => 'hostname' },
|
|
hostname => { src => $exprs{Host} },
|
|
ip => { src => 'ip' },
|
|
lock_structs => { src => 'lock_structs' },
|
|
lock_wait_time => { src => 'lock_wait_time', trans => [ qw(secs_to_time) ] },
|
|
mysql_thread_id => { src => 'mysql_thread_id' },
|
|
os_thread_id => { src => 'os_thread_id' },
|
|
proc_no => { src => 'proc_no' },
|
|
query_id => { src => 'query_id' },
|
|
query_status => { src => 'query_status' },
|
|
query_text => { src => 'query_text', trans => [ qw(no_ctrl_char) ] },
|
|
row_locks => { src => 'row_locks' },
|
|
tables_in_use => { src => 'tables_in_use' },
|
|
tables_locked => { src => 'tables_locked' },
|
|
thread_decl_inside => { src => 'thread_decl_inside' },
|
|
thread_status => { src => 'thread_status' },
|
|
'time' => { src => 'active_secs', trans => [ qw(secs_to_time) ] },
|
|
timestring => { src => 'timestring' },
|
|
txn_doesnt_see_ge => { src => 'txn_doesnt_see_ge' },
|
|
txn_id => { src => 'txn_id' },
|
|
txn_sees_lt => { src => 'txn_sees_lt' },
|
|
txn_status => { src => 'txn_status' },
|
|
truncates => { src => 'truncates' },
|
|
undo_log_entries => { src => 'undo_log_entries' },
|
|
user => { src => 'user' },
|
|
victim => { src => 'victim' },
|
|
wait_status => { src => 'lock_wait_status' },
|
|
},
|
|
visible => [ qw(cxn mysql_thread_id timestring user hostname victim time undo_log_entries lock_structs query_text)],
|
|
filters => [],
|
|
sort_cols => 'cxn mysql_thread_id',
|
|
sort_dir => '1',
|
|
innodb => 'dl',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
explain => {
|
|
capt => 'EXPLAIN Results',
|
|
cust => {},
|
|
cols => {
|
|
part_id => { src => 'id' },
|
|
select_type => { src => 'select_type' },
|
|
tbl => { src => 'table' },
|
|
partitions => { src => 'partitions' },
|
|
scan_type => { src => 'type' },
|
|
possible_keys => { src => 'possible_keys' },
|
|
index => { src => 'key' },
|
|
key_len => { src => 'key_len' },
|
|
index_ref => { src => 'ref' },
|
|
num_rows => { src => 'rows' },
|
|
special => { src => 'extra' },
|
|
},
|
|
visible => [ qw(select_type tbl partitions scan_type possible_keys index key_len index_ref num_rows special)],
|
|
filters => [],
|
|
sort_cols => '',
|
|
sort_dir => '1',
|
|
innodb => '',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
file_io_misc => {
|
|
capt => 'File I/O Misc',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
io_bytes_s => { src => 'IB_io_avg_bytes_s' },
|
|
io_flush_type => { src => 'IB_io_flush_type' },
|
|
io_fsyncs_s => { src => 'IB_io_fsyncs_s' },
|
|
io_reads_s => { src => 'IB_io_reads_s' },
|
|
io_writes_s => { src => 'IB_io_writes_s' },
|
|
os_file_reads => { src => 'IB_io_os_file_reads' },
|
|
os_file_writes => { src => 'IB_io_os_file_writes' },
|
|
os_fsyncs => { src => 'IB_io_os_fsyncs' },
|
|
},
|
|
visible => [ qw(cxn os_file_reads os_file_writes os_fsyncs io_reads_s io_writes_s io_bytes_s)],
|
|
filters => [],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => 'io',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
fk_error => {
|
|
capt => 'Foreign Key Error Info',
|
|
cust => {},
|
|
cols => {
|
|
timestring => { src => 'IB_fk_timestring' },
|
|
child_db => { src => 'IB_fk_child_db' },
|
|
child_table => { src => 'IB_fk_child_table' },
|
|
child_index => { src => 'IB_fk_child_index' },
|
|
fk_name => { src => 'IB_fk_fk_name' },
|
|
parent_db => { src => 'IB_fk_parent_db' },
|
|
parent_table => { src => 'IB_fk_parent_table' },
|
|
parent_col => { src => 'IB_fk_parent_col' },
|
|
parent_index => { src => 'IB_fk_parent_index' },
|
|
attempted_op => { src => 'IB_fk_attempted_op' },
|
|
},
|
|
visible => [ qw(timestring child_db child_table child_index parent_db parent_table parent_col parent_index fk_name attempted_op)],
|
|
filters => [],
|
|
sort_cols => '',
|
|
sort_dir => '1',
|
|
innodb => 'fk',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
insert_buffers => {
|
|
capt => 'Insert Buffers',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
inserts => { src => 'IB_ib_inserts' },
|
|
merged_recs => { src => 'IB_ib_merged_recs' },
|
|
merges => { src => 'IB_ib_merges' },
|
|
size => { src => 'IB_ib_size' },
|
|
free_list_len => { src => 'IB_ib_free_list_len' },
|
|
seg_size => { src => 'IB_ib_seg_size' },
|
|
},
|
|
visible => [ qw(cxn inserts merged_recs merges size free_list_len seg_size)],
|
|
filters => [],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => 'ib',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
innodb_locks => {
|
|
capt => 'InnoDB Locks',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
db => { src => 'db' },
|
|
index => { src => 'index' },
|
|
insert_intention => { src => 'insert_intention' },
|
|
lock_mode => { src => 'lock_mode' },
|
|
lock_type => { src => 'lock_type' },
|
|
lock_wait_time => { src => 'lock_wait_time', trans => [ qw(secs_to_time) ] },
|
|
mysql_thread_id => { src => 'mysql_thread_id' },
|
|
n_bits => { src => 'n_bits' },
|
|
page_no => { src => 'page_no' },
|
|
space_id => { src => 'space_id' },
|
|
special => { src => 'special' },
|
|
tbl => { src => 'table' },
|
|
'time' => { src => 'active_secs', hdr => 'Active', trans => [ qw(secs_to_time) ] },
|
|
txn_id => { src => 'txn_id' },
|
|
waiting => { src => 'waiting' },
|
|
},
|
|
visible => [ qw(cxn mysql_thread_id lock_type waiting lock_wait_time time lock_mode db tbl index insert_intention special)],
|
|
filters => [],
|
|
sort_cols => 'cxn -lock_wait_time',
|
|
sort_dir => '1',
|
|
innodb => 'tx',
|
|
colors => [
|
|
{ col => 'lock_wait_time', op => '>', arg => 60, color => 'red' },
|
|
{ col => 'lock_wait_time', op => '>', arg => 30, color => 'yellow' },
|
|
{ col => 'lock_wait_time', op => '>', arg => 10, color => 'green' },
|
|
],
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
innodb_transactions => {
|
|
capt => 'InnoDB Transactions',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
active_secs => { src => 'active_secs' },
|
|
has_read_view => { src => 'has_read_view' },
|
|
heap_size => { src => 'heap_size' },
|
|
hostname => { src => $exprs{Host} },
|
|
ip => { src => 'ip' },
|
|
wait_status => { src => 'lock_wait_status' },
|
|
lock_wait_time => { src => 'lock_wait_time', trans => [ qw(secs_to_time) ] },
|
|
lock_structs => { src => 'lock_structs' },
|
|
mysql_thread_id => { src => 'mysql_thread_id' },
|
|
os_thread_id => { src => 'os_thread_id' },
|
|
proc_no => { src => 'proc_no' },
|
|
query_id => { src => 'query_id' },
|
|
query_status => { src => 'query_status' },
|
|
query_text => { src => 'query_text', trans => [ qw(no_ctrl_char) ] },
|
|
txn_time_remain => { src => $exprs{TxnTimeRemain}, trans => [ qw(secs_to_time) ] },
|
|
row_locks => { src => 'row_locks' },
|
|
tables_in_use => { src => 'tables_in_use' },
|
|
tables_locked => { src => 'tables_locked' },
|
|
thread_decl_inside => { src => 'thread_decl_inside' },
|
|
thread_status => { src => 'thread_status' },
|
|
'time' => { src => 'active_secs', trans => [ qw(secs_to_time) ], agg => 'sum' },
|
|
txn_doesnt_see_ge => { src => 'txn_doesnt_see_ge' },
|
|
txn_id => { src => 'txn_id' },
|
|
txn_sees_lt => { src => 'txn_sees_lt' },
|
|
txn_status => { src => 'txn_status', minw => 10, maxw => 10 },
|
|
undo_log_entries => { src => 'undo_log_entries' },
|
|
user => { src => 'user', maxw => 10 },
|
|
cnt => { src => 'mysql_thread_id', minw => 0 },
|
|
},
|
|
visible => [ qw(cxn cnt mysql_thread_id user hostname txn_status time undo_log_entries query_text)],
|
|
filters => [ qw( hide_self hide_inactive ) ],
|
|
sort_cols => '-active_secs txn_status cxn mysql_thread_id',
|
|
sort_dir => '1',
|
|
innodb => 'tx',
|
|
hide_caption => 1,
|
|
colors => [
|
|
{ col => 'wait_status', op => 'eq', arg => 'LOCK WAIT', color => 'black on_red' },
|
|
{ col => 'time', op => '>', arg => 600, color => 'red' },
|
|
{ col => 'time', op => '>', arg => 300, color => 'yellow' },
|
|
{ col => 'time', op => '>', arg => 60, color => 'green' },
|
|
{ col => 'time', op => '>', arg => 30, color => 'cyan' },
|
|
{ col => 'txn_status', op => 'eq', arg => 'not started', color => 'white' },
|
|
],
|
|
group_by => [ qw(cxn txn_status) ],
|
|
aggregate => 0,
|
|
},
|
|
io_threads => {
|
|
capt => 'I/O Threads',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
thread => { src => 'thread' },
|
|
thread_purpose => { src => 'purpose' },
|
|
event_set => { src => 'event_set' },
|
|
thread_status => { src => 'state' },
|
|
},
|
|
visible => [ qw(cxn thread thread_purpose thread_status)],
|
|
filters => [ qw() ],
|
|
sort_cols => 'cxn thread',
|
|
sort_dir => '1',
|
|
innodb => 'io',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
log_statistics => {
|
|
capt => 'Log Statistics',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
last_chkp => { src => 'IB_lg_last_chkp' },
|
|
log_flushed_to => { src => 'IB_lg_log_flushed_to' },
|
|
log_ios_done => { src => 'IB_lg_log_ios_done' },
|
|
log_ios_s => { src => 'IB_lg_log_ios_s' },
|
|
log_seq_no => { src => 'IB_lg_log_seq_no' },
|
|
pending_chkp_writes => { src => 'IB_lg_pending_chkp_writes' },
|
|
pending_log_writes => { src => 'IB_lg_pending_log_writes' },
|
|
},
|
|
visible => [ qw(cxn log_seq_no log_flushed_to last_chkp log_ios_done log_ios_s)],
|
|
filters => [],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => 'lg',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
master_status => {
|
|
capt => 'Master Status',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
binlog_do_db => { src => 'binlog_do_db' },
|
|
binlog_ignore_db => { src => 'binlog_ignore_db' },
|
|
master_file => { src => 'file' },
|
|
master_pos => { src => 'position' },
|
|
binlog_cache_overflow => { src => '(Binlog_cache_disk_use||0)/(Binlog_cache_use||1)', trans => [ qw(percent) ] },
|
|
},
|
|
visible => [ qw(cxn master_file master_pos binlog_cache_overflow)],
|
|
filters => [ qw(cxn_is_master) ],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => '',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
pending_io => {
|
|
capt => 'Pending I/O',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
p_normal_aio_reads => { src => 'IB_io_pending_normal_aio_reads' },
|
|
p_aio_writes => { src => 'IB_io_pending_aio_writes' },
|
|
p_ibuf_aio_reads => { src => 'IB_io_pending_ibuf_aio_reads' },
|
|
p_sync_ios => { src => 'IB_io_pending_sync_ios' },
|
|
p_buf_pool_flushes => { src => 'IB_io_pending_buffer_pool_flushes' },
|
|
p_log_flushes => { src => 'IB_io_pending_log_flushes' },
|
|
p_log_ios => { src => 'IB_io_pending_log_ios' },
|
|
p_preads => { src => 'IB_io_pending_preads' },
|
|
p_pwrites => { src => 'IB_io_pending_pwrites' },
|
|
},
|
|
visible => [ qw(cxn p_normal_aio_reads p_aio_writes p_ibuf_aio_reads p_sync_ios p_log_flushes p_log_ios)],
|
|
filters => [],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => 'io',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
open_tables => {
|
|
capt => 'Open Tables',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
db => { src => 'database' },
|
|
tbl => { src => 'table' },
|
|
num_times_open => { src => 'in_use' },
|
|
is_name_locked => { src => 'name_locked' },
|
|
},
|
|
visible => [ qw(cxn db tbl num_times_open is_name_locked)],
|
|
filters => [ qw(table_is_open) ],
|
|
sort_cols => '-num_times_open cxn db tbl',
|
|
sort_dir => '1',
|
|
innodb => '',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
page_statistics => {
|
|
capt => 'Page Statistics',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
pages_read => { src => 'IB_bp_pages_read' },
|
|
pages_written => { src => 'IB_bp_pages_written' },
|
|
pages_created => { src => 'IB_bp_pages_created' },
|
|
page_reads_sec => { src => 'IB_bp_page_reads_sec' },
|
|
page_writes_sec => { src => 'IB_bp_page_writes_sec' },
|
|
page_creates_sec => { src => 'IB_bp_page_creates_sec' },
|
|
},
|
|
visible => [ qw(cxn pages_read pages_written pages_created page_reads_sec page_writes_sec page_creates_sec)],
|
|
filters => [],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => 'bp',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
processlist => {
|
|
capt => 'MySQL Process List',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn', minw => 6, maxw => 10 },
|
|
mysql_thread_id => { src => 'id', minw => 6, maxw => 0 },
|
|
user => { src => 'user', minw => 5, maxw => 8 },
|
|
hostname => { src => $exprs{Host}, minw => 13, maxw => 8, },
|
|
port => { src => $exprs{Port}, minw => 0, maxw => 0, },
|
|
host_and_port => { src => 'host', minw => 0, maxw => 0 },
|
|
db => { src => 'db', minw => 6, maxw => 12 },
|
|
cmd => { src => 'command', minw => 5, maxw => 0 },
|
|
time => { src => 'time', minw => 5, maxw => 0, trans => [ qw(secs_to_time) ], agg => 'sum' },
|
|
state => { src => 'state', minw => 0, maxw => 0 },
|
|
info => { src => 'info', minw => 0, maxw => 0, trans => [ qw(no_ctrl_char) ] },
|
|
cnt => { src => 'id', minw => 0, maxw => 0 },
|
|
},
|
|
visible => [ qw(cxn cmd cnt mysql_thread_id state user hostname db time info)],
|
|
filters => [ qw(hide_self hide_inactive hide_slave_io) ],
|
|
sort_cols => '-time cxn hostname mysql_thread_id',
|
|
sort_dir => '1',
|
|
innodb => '',
|
|
hide_caption => 1,
|
|
colors => [
|
|
{ col => 'state', op => 'eq', arg => 'Locked', color => 'black on_red' },
|
|
{ col => 'cmd', op => 'eq', arg => 'Sleep', color => 'white' },
|
|
{ col => 'user', op => 'eq', arg => 'system user', color => 'white' },
|
|
{ col => 'cmd', op => 'eq', arg => 'Connect', color => 'white' },
|
|
{ col => 'cmd', op => 'eq', arg => 'Binlog Dump', color => 'white' },
|
|
{ col => 'time', op => '>', arg => 600, color => 'red' },
|
|
{ col => 'time', op => '>', arg => 120, color => 'yellow' },
|
|
{ col => 'time', op => '>', arg => 60, color => 'green' },
|
|
{ col => 'time', op => '>', arg => 30, color => 'cyan' },
|
|
],
|
|
group_by => [qw(cxn cmd)],
|
|
aggregate => 0,
|
|
},
|
|
|
|
# TODO: some more columns:
|
|
# kb_used=hdr='BufUsed' minw='0' num='0' src='percent(1 - ((Key_blocks_unused * key_cache_block_size) / (key_buffer_size||1)))' dec='0' trans='' tbl='q_header' just='-' user='1' maxw='0' label='User-defined'
|
|
# retries=hdr='Retries' minw='0' num='0' src='Slave_retried_transactions' dec='0' trans='' tbl='slave_sql_status' just='-' user='1' maxw='0' label='User-defined'
|
|
# thd=hdr='Thd' minw='0' num='0' src='Threads_connected' dec='0' trans='' tbl='slave_sql_status' just='-' user='1' maxw='0' label='User-defined'
|
|
|
|
q_header => {
|
|
capt => 'Q-mode Header',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
questions => { src => 'Questions' },
|
|
qps => { src => 'Questions/Uptime_hires', dec => 1, trans => [qw(shorten)] },
|
|
load => { src => $exprs{ServerLoad}, dec => 1, trans => [qw(shorten)] },
|
|
slow => { src => 'Slow_queries', dec => 1, trans => [qw(shorten)] },
|
|
q_cache_hit => { src => $exprs{QcacheHitRatio}, dec => 1, trans => [qw(percent)] },
|
|
key_buffer_hit => { src => '1-(Key_reads/(Key_read_requests||1))', dec => 1, trans => [qw(percent)] },
|
|
bps_in => { src => 'Bytes_received/Uptime_hires', dec => 1, trans => [qw(shorten)] },
|
|
bps_out => { src => 'Bytes_sent/Uptime_hires', dec => 1, trans => [qw(shorten)] },
|
|
when => { src => 'when' },
|
|
},
|
|
visible => [ qw(cxn when load qps slow q_cache_hit key_buffer_hit bps_in bps_out)],
|
|
filters => [],
|
|
sort_cols => 'when cxn',
|
|
sort_dir => '1',
|
|
innodb => '',
|
|
hide_caption => 1,
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
row_operations => {
|
|
capt => 'InnoDB Row Operations',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
num_inserts => { src => 'IB_ro_num_rows_ins' },
|
|
num_updates => { src => 'IB_ro_num_rows_upd' },
|
|
num_reads => { src => 'IB_ro_num_rows_read' },
|
|
num_deletes => { src => 'IB_ro_num_rows_del' },
|
|
num_inserts_sec => { src => 'IB_ro_ins_sec' },
|
|
num_updates_sec => { src => 'IB_ro_upd_sec' },
|
|
num_reads_sec => { src => 'IB_ro_read_sec' },
|
|
num_deletes_sec => { src => 'IB_ro_del_sec' },
|
|
},
|
|
visible => [ qw(cxn num_inserts num_updates num_reads num_deletes num_inserts_sec
|
|
num_updates_sec num_reads_sec num_deletes_sec)],
|
|
filters => [],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => 'ro',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
row_operation_misc => {
|
|
capt => 'Row Operation Misc',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
queries_in_queue => { src => 'IB_ro_queries_in_queue' },
|
|
queries_inside => { src => 'IB_ro_queries_inside' },
|
|
read_views_open => { src => 'IB_ro_read_views_open' },
|
|
main_thread_id => { src => 'IB_ro_main_thread_id' },
|
|
main_thread_proc_no => { src => 'IB_ro_main_thread_proc_no' },
|
|
main_thread_state => { src => 'IB_ro_main_thread_state' },
|
|
num_res_ext => { src => 'IB_ro_n_reserved_extents' },
|
|
},
|
|
visible => [ qw(cxn queries_in_queue queries_inside read_views_open main_thread_state)],
|
|
filters => [],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => 'ro',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
semaphores => {
|
|
capt => 'InnoDB Semaphores',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
mutex_os_waits => { src => 'IB_sm_mutex_os_waits' },
|
|
mutex_spin_rounds => { src => 'IB_sm_mutex_spin_rounds' },
|
|
mutex_spin_waits => { src => 'IB_sm_mutex_spin_waits' },
|
|
reservation_count => { src => 'IB_sm_reservation_count' },
|
|
rw_excl_os_waits => { src => 'IB_sm_rw_excl_os_waits' },
|
|
rw_excl_spins => { src => 'IB_sm_rw_excl_spins' },
|
|
rw_shared_os_waits => { src => 'IB_sm_rw_shared_os_waits' },
|
|
rw_shared_spins => { src => 'IB_sm_rw_shared_spins' },
|
|
signal_count => { src => 'IB_sm_signal_count' },
|
|
wait_array_size => { src => 'IB_sm_wait_array_size' },
|
|
},
|
|
visible => [ qw(cxn mutex_os_waits mutex_spin_waits mutex_spin_rounds
|
|
rw_excl_os_waits rw_excl_spins rw_shared_os_waits rw_shared_spins
|
|
signal_count reservation_count )],
|
|
filters => [],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => 'sm',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
slave_io_status => {
|
|
capt => 'Slave I/O Status',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
connect_retry => { src => 'connect_retry' },
|
|
master_host => { src => 'master_host', hdr => 'Master'},
|
|
master_log_file => { src => 'master_log_file', hdr => 'File' },
|
|
master_port => { src => 'master_port' },
|
|
master_ssl_allowed => { src => 'master_ssl_allowed' },
|
|
master_ssl_ca_file => { src => 'master_ssl_ca_file' },
|
|
master_ssl_ca_path => { src => 'master_ssl_ca_path' },
|
|
master_ssl_cert => { src => 'master_ssl_cert' },
|
|
master_ssl_cipher => { src => 'master_ssl_cipher' },
|
|
master_ssl_key => { src => 'master_ssl_key' },
|
|
master_user => { src => 'master_user' },
|
|
read_master_log_pos => { src => 'read_master_log_pos', hdr => 'Pos' },
|
|
relay_log_size => { src => 'relay_log_space', trans => [qw(shorten)] },
|
|
slave_io_running => { src => 'slave_io_running', hdr => 'On?' },
|
|
slave_io_state => { src => 'slave_io_state', hdr => 'State' },
|
|
},
|
|
visible => [ qw(cxn master_host slave_io_running master_log_file relay_log_size read_master_log_pos slave_io_state)],
|
|
filters => [ qw( cxn_is_slave ) ],
|
|
sort_cols => 'slave_io_running cxn',
|
|
colors => [
|
|
{ col => 'slave_io_running', op => 'ne', arg => 'Yes', color => 'black on_red' },
|
|
],
|
|
sort_dir => '1',
|
|
innodb => '',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
slave_sql_status => {
|
|
capt => 'Slave SQL Status',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
exec_master_log_pos => { src => 'exec_master_log_pos', hdr => 'Master Pos' },
|
|
last_errno => { src => 'last_errno' },
|
|
last_error => { src => 'last_error' },
|
|
master_host => { src => 'master_host', hdr => 'Master' },
|
|
relay_log_file => { src => 'relay_log_file' },
|
|
relay_log_pos => { src => 'relay_log_pos' },
|
|
relay_log_size => { src => 'relay_log_space', trans => [qw(shorten)] },
|
|
relay_master_log_file => { src => 'relay_master_log_file', hdr => 'Master File' },
|
|
replicate_do_db => { src => 'replicate_do_db' },
|
|
replicate_do_table => { src => 'replicate_do_table' },
|
|
replicate_ignore_db => { src => 'replicate_ignore_db' },
|
|
replicate_ignore_table => { src => 'replicate_ignore_table' },
|
|
replicate_wild_do_table => { src => 'replicate_wild_do_table' },
|
|
replicate_wild_ignore_table => { src => 'replicate_wild_ignore_table' },
|
|
skip_counter => { src => 'skip_counter' },
|
|
slave_sql_running => { src => 'slave_sql_running', hdr => 'On?' },
|
|
until_condition => { src => 'until_condition' },
|
|
until_log_file => { src => 'until_log_file' },
|
|
until_log_pos => { src => 'until_log_pos' },
|
|
time_behind_master => { src => 'seconds_behind_master', trans => [ qw(secs_to_time) ] },
|
|
bytes_behind_master => { src => 'master_log_file && master_log_file eq relay_master_log_file ? read_master_log_pos - exec_master_log_pos : 0', trans => [qw(shorten)] },
|
|
slave_catchup_rate => { src => $exprs{SlaveCatchupRate}, trans => [ qw(set_precision) ] },
|
|
slave_open_temp_tables => { src => 'Slave_open_temp_tables' },
|
|
},
|
|
visible => [ qw(cxn master_host slave_sql_running time_behind_master slave_catchup_rate slave_open_temp_tables relay_log_pos last_error)],
|
|
filters => [ qw( cxn_is_slave ) ],
|
|
sort_cols => 'slave_sql_running cxn',
|
|
sort_dir => '1',
|
|
innodb => '',
|
|
colors => [
|
|
{ col => 'slave_sql_running', op => 'ne', arg => 'Yes', color => 'black on_red' },
|
|
{ col => 'time_behind_master', op => '>', arg => 600, color => 'red' },
|
|
{ col => 'time_behind_master', op => '>', arg => 60, color => 'yellow' },
|
|
{ col => 'time_behind_master', op => '==', arg => 0, color => 'white' },
|
|
],
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
t_header => {
|
|
capt => 'T-Mode Header',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
dirty_bufs => { src => $exprs{DirtyBufs}, trans => [qw(percent)] },
|
|
history_list_len => { src => 'IB_tx_history_list_len' },
|
|
lock_structs => { src => 'IB_tx_num_lock_structs' },
|
|
num_txns => { src => $exprs{NumTxns} },
|
|
max_txn => { src => $exprs{MaxTxnTime}, trans => [qw(secs_to_time)] },
|
|
undo_for => { src => 'IB_tx_purge_undo_for' },
|
|
used_bufs => { src => $exprs{BufPoolFill}, trans => [qw(percent)]},
|
|
versions => { src => $exprs{OldVersions} },
|
|
},
|
|
visible => [ qw(cxn history_list_len versions undo_for dirty_bufs used_bufs num_txns max_txn lock_structs)],
|
|
filters => [ ],
|
|
sort_cols => 'cxn',
|
|
sort_dir => '1',
|
|
innodb => '',
|
|
colors => [],
|
|
hide_caption => 1,
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
var_status => {
|
|
capt => 'Variables & Status',
|
|
cust => {},
|
|
cols => {}, # Generated from current varset
|
|
visible => [], # Generated from current varset
|
|
filters => [],
|
|
sort_cols => '',
|
|
sort_dir => 1,
|
|
innodb => '',
|
|
temp => 1, # Do not persist to config file.
|
|
hide_caption => 1,
|
|
pivot => 0,
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
wait_array => {
|
|
capt => 'InnoDB Wait Array',
|
|
cust => {},
|
|
cols => {
|
|
cxn => { src => 'cxn' },
|
|
thread => { src => 'thread' },
|
|
waited_at_filename => { src => 'waited_at_filename' },
|
|
waited_at_line => { src => 'waited_at_line' },
|
|
'time' => { src => 'waited_secs', trans => [ qw(secs_to_time) ] },
|
|
request_type => { src => 'request_type' },
|
|
lock_mem_addr => { src => 'lock_mem_addr' },
|
|
lock_cfile_name => { src => 'lock_cfile_name' },
|
|
lock_cline => { src => 'lock_cline' },
|
|
writer_thread => { src => 'writer_thread' },
|
|
writer_lock_mode => { src => 'writer_lock_mode' },
|
|
num_readers => { src => 'num_readers' },
|
|
lock_var => { src => 'lock_var' },
|
|
waiters_flag => { src => 'waiters_flag' },
|
|
last_s_file_name => { src => 'last_s_file_name' },
|
|
last_s_line => { src => 'last_s_line' },
|
|
last_x_file_name => { src => 'last_x_file_name' },
|
|
last_x_line => { src => 'last_x_line' },
|
|
cell_waiting => { src => 'cell_waiting' },
|
|
cell_event_set => { src => 'cell_event_set' },
|
|
},
|
|
visible => [ qw(cxn thread time waited_at_filename waited_at_line request_type num_readers lock_var waiters_flag cell_waiting cell_event_set)],
|
|
filters => [],
|
|
sort_cols => 'cxn -time',
|
|
sort_dir => '1',
|
|
innodb => 'sm',
|
|
group_by => [],
|
|
aggregate => 0,
|
|
},
|
|
);
|
|
|
|
# Initialize %tbl_meta from %columns and do some checks.
|
|
foreach my $table_name ( keys %tbl_meta ) {
|
|
my $table = $tbl_meta{$table_name};
|
|
my $cols = $table->{cols};
|
|
|
|
foreach my $col_name ( keys %$cols ) {
|
|
my $col_def = $table->{cols}->{$col_name};
|
|
die "I can't find a column named '$col_name' for '$table_name'" unless $columns{$col_name};
|
|
$columns{$col_name}->{referenced} = 1;
|
|
|
|
foreach my $prop ( keys %col_props ) {
|
|
# Each column gets non-existing values set from %columns or defaults from %col_props.
|
|
if ( !$col_def->{$prop} ) {
|
|
$col_def->{$prop}
|
|
= defined($columns{$col_name}->{$prop})
|
|
? $columns{$col_name}->{$prop}
|
|
: $col_props{$prop};
|
|
}
|
|
}
|
|
|
|
# Ensure transformations and aggregate functions are valid
|
|
die "Unknown aggregate function '$col_def->{agg}' "
|
|
. "for column '$col_name' in table '$table_name'"
|
|
unless exists $agg_funcs{$col_def->{agg}};
|
|
foreach my $trans ( @{$col_def->{trans}} ) {
|
|
die "Unknown transformation '$trans' "
|
|
. "for column '$col_name' in table '$table_name'"
|
|
unless exists $trans_funcs{$trans};
|
|
}
|
|
}
|
|
|
|
# Ensure each column in visible and group_by exists in cols
|
|
foreach my $place ( qw(visible group_by) ) {
|
|
foreach my $col_name ( @{$table->{$place}} ) {
|
|
if ( !exists $cols->{$col_name} ) {
|
|
die "Column '$col_name' is listed in '$place' for '$table_name', but doesn't exist";
|
|
}
|
|
}
|
|
}
|
|
|
|
# Compile sort and color subroutines
|
|
$table->{sort_func} = make_sort_func($table);
|
|
$table->{color_func} = make_color_func($table);
|
|
}
|
|
|
|
# This is for code cleanup:
|
|
{
|
|
my @unused_cols = grep { !$columns{$_}->{referenced} } sort keys %columns;
|
|
if ( @unused_cols ) {
|
|
die "The following columns are not used: "
|
|
. join(' ', @unused_cols);
|
|
}
|
|
}
|
|
|
|
# ###########################################################################
|
|
# Operating modes {{{3
|
|
# ###########################################################################
|
|
my %modes = (
|
|
B => {
|
|
hdr => 'InnoDB Buffers',
|
|
cust => {},
|
|
note => 'Shows buffer info from InnoDB',
|
|
action_for => {
|
|
i => {
|
|
action => sub { toggle_config('status_inc') },
|
|
label => 'Toggle incremental status display',
|
|
},
|
|
},
|
|
display_sub => \&display_B,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(buffer_pool page_statistics insert_buffers adaptive_hash_index)],
|
|
visible_tables => [qw(buffer_pool page_statistics insert_buffers adaptive_hash_index)],
|
|
},
|
|
C => {
|
|
hdr => 'Command Summary',
|
|
cust => {},
|
|
note => 'Shows relative magnitude of variables',
|
|
action_for => {
|
|
s => {
|
|
action => sub { get_config_interactive('cmd_filter') },
|
|
label => 'Choose variable prefix',
|
|
},
|
|
},
|
|
display_sub => \&display_C,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(cmd_summary)],
|
|
visible_tables => [qw(cmd_summary)],
|
|
},
|
|
D => {
|
|
hdr => 'InnoDB Deadlocks',
|
|
cust => {},
|
|
note => 'View InnoDB deadlock information',
|
|
action_for => {
|
|
c => {
|
|
action => sub { edit_table('deadlock_transactions') },
|
|
label => 'Choose visible columns',
|
|
},
|
|
w => {
|
|
action => \&create_deadlock,
|
|
label => 'Wipe deadlock status info by creating a deadlock',
|
|
},
|
|
},
|
|
display_sub => \&display_D,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(deadlock_transactions deadlock_locks)],
|
|
visible_tables => [qw(deadlock_transactions deadlock_locks)],
|
|
},
|
|
F => {
|
|
hdr => 'InnoDB FK Err',
|
|
cust => {},
|
|
note => 'View the latest InnoDB foreign key error',
|
|
action_for => {},
|
|
display_sub => \&display_F,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 1,
|
|
tables => [qw(fk_error)],
|
|
visible_tables => [qw(fk_error)],
|
|
},
|
|
I => {
|
|
hdr => 'InnoDB I/O Info',
|
|
cust => {},
|
|
note => 'Shows I/O info (i/o, log...) from InnoDB',
|
|
action_for => {
|
|
i => {
|
|
action => sub { toggle_config('status_inc') },
|
|
label => 'Toggle incremental status display',
|
|
},
|
|
},
|
|
display_sub => \&display_I,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(io_threads pending_io file_io_misc log_statistics)],
|
|
visible_tables => [qw(io_threads pending_io file_io_misc log_statistics)],
|
|
},
|
|
L => {
|
|
hdr => 'Locks',
|
|
cust => {},
|
|
note => 'Shows transaction locks',
|
|
action_for => {
|
|
a => {
|
|
action => sub { send_cmd_to_servers('CREATE TABLE IF NOT EXISTS test.innodb_lock_monitor(a int) ENGINE=InnoDB', 0, '', []); },
|
|
label => 'Start the InnoDB Lock Monitor',
|
|
},
|
|
o => {
|
|
action => sub { send_cmd_to_servers('DROP TABLE IF EXISTS test.innodb_lock_monitor', 0, '', []); },
|
|
label => 'Stop the InnoDB Lock Monitor',
|
|
},
|
|
},
|
|
display_sub => \&display_L,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(innodb_locks)],
|
|
visible_tables => [qw(innodb_locks)],
|
|
},
|
|
M => {
|
|
hdr => 'Replication Status',
|
|
cust => {},
|
|
note => 'Shows replication (master and slave) status',
|
|
action_for => {
|
|
a => {
|
|
action => sub { send_cmd_to_servers('START SLAVE', 0, 'START SLAVE SQL_THREAD UNTIL MASTER_LOG_FILE = ?, MASTER_LOG_POS = ?', []); },
|
|
label => 'Start slave(s)',
|
|
},
|
|
i => {
|
|
action => sub { toggle_config('status_inc') },
|
|
label => 'Toggle incremental status display',
|
|
},
|
|
o => {
|
|
action => sub { send_cmd_to_servers('STOP SLAVE', 0, '', []); },
|
|
label => 'Stop slave(s)',
|
|
},
|
|
b => {
|
|
action => sub { purge_master_logs() },
|
|
label => 'Purge unused master logs',
|
|
},
|
|
},
|
|
display_sub => \&display_M,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(slave_sql_status slave_io_status master_status)],
|
|
visible_tables => [qw(slave_sql_status slave_io_status master_status)],
|
|
},
|
|
O => {
|
|
hdr => 'Open Tables',
|
|
cust => {},
|
|
note => 'Shows open tables in MySQL',
|
|
action_for => {
|
|
r => {
|
|
action => sub { reverse_sort('open_tables'); },
|
|
label => 'Reverse sort order',
|
|
},
|
|
s => {
|
|
action => sub { choose_sort_cols('open_tables'); },
|
|
label => "Choose sort column",
|
|
},
|
|
},
|
|
display_sub => \&display_O,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(open_tables)],
|
|
visible_tables => [qw(open_tables)],
|
|
},
|
|
Q => {
|
|
hdr => 'Query List',
|
|
cust => {},
|
|
note => 'Shows queries from SHOW FULL PROCESSLIST',
|
|
action_for => {
|
|
a => {
|
|
action => sub { toggle_filter('processlist', 'hide_self') },
|
|
label => 'Toggle the innotop process',
|
|
},
|
|
c => {
|
|
action => sub { edit_table('processlist') },
|
|
label => 'Choose visible columns',
|
|
},
|
|
e => {
|
|
action => sub { analyze_query('e'); },
|
|
label => "Explain a thread's query",
|
|
},
|
|
f => {
|
|
action => sub { analyze_query('f'); },
|
|
label => "Show a thread's full query",
|
|
},
|
|
h => {
|
|
action => sub { toggle_visible_table('Q', 'q_header') },
|
|
label => 'Toggle the header on and off',
|
|
},
|
|
i => {
|
|
action => sub { toggle_filter('processlist', 'hide_inactive') },
|
|
label => 'Toggle idle processes',
|
|
},
|
|
k => {
|
|
action => sub { kill_query('CONNECTION') },
|
|
label => "Kill a query's connection",
|
|
},
|
|
r => {
|
|
action => sub { reverse_sort('processlist'); },
|
|
label => 'Reverse sort order',
|
|
},
|
|
s => {
|
|
action => sub { choose_sort_cols('processlist'); },
|
|
label => "Change the display's sort column",
|
|
},
|
|
x => {
|
|
action => sub { kill_query('QUERY') },
|
|
label => "Kill a query",
|
|
},
|
|
},
|
|
display_sub => \&display_Q,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(q_header processlist)],
|
|
visible_tables => [qw(q_header processlist)],
|
|
},
|
|
R => {
|
|
hdr => 'InnoDB Row Ops',
|
|
cust => {},
|
|
note => 'Shows InnoDB row operation and semaphore info',
|
|
action_for => {
|
|
i => {
|
|
action => sub { toggle_config('status_inc') },
|
|
label => 'Toggle incremental status display',
|
|
},
|
|
},
|
|
display_sub => \&display_R,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(row_operations row_operation_misc semaphores wait_array)],
|
|
visible_tables => [qw(row_operations row_operation_misc semaphores wait_array)],
|
|
},
|
|
S => {
|
|
hdr => 'Variables & Status',
|
|
cust => {},
|
|
note => 'Shows query load statistics a la vmstat',
|
|
action_for => {
|
|
'>' => {
|
|
action => sub { switch_var_set('S_set', 1) },
|
|
label => 'Switch to next variable set',
|
|
},
|
|
'<' => {
|
|
action => sub { switch_var_set('S_set', -1) },
|
|
label => 'Switch to prev variable set',
|
|
},
|
|
c => {
|
|
action => sub {
|
|
choose_var_set('S_set');
|
|
start_S_mode();
|
|
},
|
|
label => "Choose which set to display",
|
|
},
|
|
e => {
|
|
action => \&edit_current_var_set,
|
|
label => 'Edit the current set of variables',
|
|
},
|
|
i => {
|
|
action => sub { $clear_screen_sub->(); toggle_config('status_inc') },
|
|
label => 'Toggle incremental status display',
|
|
},
|
|
'-' => {
|
|
action => sub { set_display_precision(-1) },
|
|
label => 'Decrease fractional display precision',
|
|
},
|
|
'+' => {
|
|
action => sub { set_display_precision(1) },
|
|
label => 'Increase fractional display precision',
|
|
},
|
|
g => {
|
|
action => sub { set_s_mode('g') },
|
|
label => 'Switch to graph (tload) view',
|
|
},
|
|
s => {
|
|
action => sub { set_s_mode('s') },
|
|
label => 'Switch to standard (vmstat) view',
|
|
},
|
|
v => {
|
|
action => sub { set_s_mode('v') },
|
|
label => 'Switch to pivoted view',
|
|
},
|
|
},
|
|
display_sub => \&display_S,
|
|
no_clear_screen => 1,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(var_status)],
|
|
visible_tables => [qw(var_status)],
|
|
},
|
|
T => {
|
|
hdr => 'InnoDB Txns',
|
|
cust => {},
|
|
note => 'Shows InnoDB transactions in top-like format',
|
|
action_for => {
|
|
a => {
|
|
action => sub { toggle_filter('innodb_transactions', 'hide_self') },
|
|
label => 'Toggle the innotop process',
|
|
},
|
|
c => {
|
|
action => sub { edit_table('innodb_transactions') },
|
|
label => 'Choose visible columns',
|
|
},
|
|
e => {
|
|
action => sub { analyze_query('e'); },
|
|
label => "Explain a thread's query",
|
|
},
|
|
f => {
|
|
action => sub { analyze_query('f'); },
|
|
label => "Show a thread's full query",
|
|
},
|
|
h => {
|
|
action => sub { toggle_visible_table('T', 't_header') },
|
|
label => 'Toggle the header on and off',
|
|
},
|
|
i => {
|
|
action => sub { toggle_filter('innodb_transactions', 'hide_inactive') },
|
|
label => 'Toggle inactive transactions',
|
|
},
|
|
k => {
|
|
action => sub { kill_query('CONNECTION') },
|
|
label => "Kill a transaction's connection",
|
|
},
|
|
r => {
|
|
action => sub { reverse_sort('innodb_transactions'); },
|
|
label => 'Reverse sort order',
|
|
},
|
|
s => {
|
|
action => sub { choose_sort_cols('innodb_transactions'); },
|
|
label => "Change the display's sort column",
|
|
},
|
|
x => {
|
|
action => sub { kill_query('QUERY') },
|
|
label => "Kill a query",
|
|
},
|
|
},
|
|
display_sub => \&display_T,
|
|
connections => [],
|
|
server_group => '',
|
|
one_connection => 0,
|
|
tables => [qw(t_header innodb_transactions)],
|
|
visible_tables => [qw(t_header innodb_transactions)],
|
|
},
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Global key mappings {{{3
|
|
# Keyed on a single character, which is read from the keyboard. Uppercase
|
|
# letters switch modes. Lowercase letters access commands when in a mode.
|
|
# These can be overridden by action_for in %modes.
|
|
# ###########################################################################
|
|
my %action_for = (
|
|
'$' => {
|
|
action => \&edit_configuration,
|
|
label => 'Edit configuration settings',
|
|
},
|
|
'?' => {
|
|
action => \&display_help,
|
|
label => 'Show help',
|
|
},
|
|
'!' => {
|
|
action => \&display_license,
|
|
label => 'Show license and warranty',
|
|
},
|
|
'^' => {
|
|
action => \&edit_table,
|
|
label => "Edit the displayed table(s)",
|
|
},
|
|
'#' => {
|
|
action => \&choose_server_groups,
|
|
label => 'Select/create server groups',
|
|
},
|
|
'@' => {
|
|
action => \&choose_servers,
|
|
label => 'Select/create server connections',
|
|
},
|
|
'/' => {
|
|
action => \&add_quick_filter,
|
|
label => 'Quickly filter what you see',
|
|
},
|
|
'\\' => {
|
|
action => \&clear_quick_filters,
|
|
label => 'Clear quick-filters',
|
|
},
|
|
'%' => {
|
|
action => \&choose_filters,
|
|
label => 'Choose and edit table filters',
|
|
},
|
|
"\t" => {
|
|
action => \&next_server_group,
|
|
label => 'Switch to the next server group',
|
|
key => 'TAB',
|
|
},
|
|
'=' => {
|
|
action => \&toggle_aggregate,
|
|
label => 'Toggle aggregation',
|
|
},
|
|
# TODO: can these be auto-generated from %modes?
|
|
B => {
|
|
action => sub { switch_mode('B') },
|
|
label => '',
|
|
},
|
|
C => {
|
|
action => sub { switch_mode('C') },
|
|
label => '',
|
|
},
|
|
D => {
|
|
action => sub { switch_mode('D') },
|
|
label => '',
|
|
},
|
|
F => {
|
|
action => sub { switch_mode('F') },
|
|
label => '',
|
|
},
|
|
I => {
|
|
action => sub { switch_mode('I') },
|
|
label => '',
|
|
},
|
|
L => {
|
|
action => sub { switch_mode('L') },
|
|
label => '',
|
|
},
|
|
M => {
|
|
action => sub { switch_mode('M') },
|
|
label => '',
|
|
},
|
|
O => {
|
|
action => sub { switch_mode('O') },
|
|
label => '',
|
|
},
|
|
Q => {
|
|
action => sub { switch_mode('Q') },
|
|
label => '',
|
|
},
|
|
R => {
|
|
action => sub { switch_mode('R') },
|
|
label => '',
|
|
},
|
|
S => {
|
|
action => \&start_S_mode,
|
|
label => '',
|
|
},
|
|
T => {
|
|
action => sub { switch_mode('T') },
|
|
label => '',
|
|
},
|
|
d => {
|
|
action => sub { get_config_interactive('interval') },
|
|
label => 'Change refresh interval',
|
|
},
|
|
n => { action => \&next_server, label => 'Switch to the next connection' },
|
|
p => { action => \&pause, label => 'Pause innotop', },
|
|
q => { action => \&finish, label => 'Quit innotop', },
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Sleep times after certain statements {{{3
|
|
# ###########################################################################
|
|
my %stmt_sleep_time_for = ();
|
|
|
|
# ###########################################################################
|
|
# Config editor key mappings {{{3
|
|
# ###########################################################################
|
|
my %cfg_editor_action = (
|
|
c => {
|
|
note => 'Edit columns, etc in the displayed table(s)',
|
|
func => \&edit_table,
|
|
},
|
|
g => {
|
|
note => 'Edit general configuration',
|
|
func => \&edit_configuration_variables,
|
|
},
|
|
k => {
|
|
note => 'Edit row-coloring rules',
|
|
func => \&edit_color_rules,
|
|
},
|
|
p => {
|
|
note => 'Manage plugins',
|
|
func => \&edit_plugins,
|
|
},
|
|
s => {
|
|
note => 'Edit server groups',
|
|
func => \&edit_server_groups,
|
|
},
|
|
S => {
|
|
note => 'Edit SQL statement sleep delays',
|
|
func => \&edit_stmt_sleep_times,
|
|
},
|
|
t => {
|
|
note => 'Choose which table(s) to display in this mode',
|
|
func => \&choose_mode_tables,
|
|
},
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Color editor key mappings {{{3
|
|
# ###########################################################################
|
|
my %color_editor_action = (
|
|
n => {
|
|
note => 'Create a new color rule',
|
|
func => sub {
|
|
my ( $tbl, $idx ) = @_;
|
|
my $meta = $tbl_meta{$tbl};
|
|
|
|
$clear_screen_sub->();
|
|
my $col;
|
|
do {
|
|
$col = prompt_list(
|
|
'Choose the target column for the rule',
|
|
'',
|
|
sub { return keys %{$meta->{cols}} },
|
|
{ map { $_ => $meta->{cols}->{$_}->{label} } keys %{$meta->{cols}} });
|
|
} while ( !$col );
|
|
( $col ) = grep { $_ } split(/\W+/, $col);
|
|
return $idx unless $col && exists $meta->{cols}->{$col};
|
|
|
|
$clear_screen_sub->();
|
|
my $op;
|
|
do {
|
|
$op = prompt_list(
|
|
'Choose the comparison operator for the rule',
|
|
'',
|
|
sub { return keys %comp_ops },
|
|
{ map { $_ => $comp_ops{$_} } keys %comp_ops } );
|
|
} until ( $op );
|
|
$op =~ s/\s+//g;
|
|
return $idx unless $op && exists $comp_ops{$op};
|
|
|
|
my $arg;
|
|
do {
|
|
$arg = prompt('Specify an argument for the comparison');
|
|
} until defined $arg;
|
|
|
|
my $color;
|
|
do {
|
|
$color = prompt_list(
|
|
'Choose the color(s) the row should be when the rule matches',
|
|
'',
|
|
sub { return keys %ansicolors },
|
|
{ map { $_ => $_ } keys %ansicolors } );
|
|
} until defined $color;
|
|
$color = join(' ', unique(grep { exists $ansicolors{$_} } split(/\W+/, $color)));
|
|
return $idx unless $color;
|
|
|
|
push @{$tbl_meta{$tbl}->{colors}}, {
|
|
col => $col,
|
|
op => $op,
|
|
arg => $arg,
|
|
color => $color
|
|
};
|
|
$tbl_meta{$tbl}->{cust}->{colors} = 1;
|
|
|
|
return $idx;
|
|
},
|
|
},
|
|
d => {
|
|
note => 'Remove the selected rule',
|
|
func => sub {
|
|
my ( $tbl, $idx ) = @_;
|
|
my @rules = @{ $tbl_meta{$tbl}->{colors} };
|
|
return 0 unless @rules > 0 && $idx < @rules && $idx >= 0;
|
|
splice(@{$tbl_meta{$tbl}->{colors}}, $idx, 1);
|
|
$tbl_meta{$tbl}->{cust}->{colors} = 1;
|
|
return $idx == @rules ? $#rules : $idx;
|
|
},
|
|
},
|
|
j => {
|
|
note => 'Move highlight down one',
|
|
func => sub {
|
|
my ( $tbl, $idx ) = @_;
|
|
my $num_rules = scalar @{$tbl_meta{$tbl}->{colors}};
|
|
return ($idx + 1) % $num_rules;
|
|
},
|
|
},
|
|
k => {
|
|
note => 'Move highlight up one',
|
|
func => sub {
|
|
my ( $tbl, $idx ) = @_;
|
|
my $num_rules = scalar @{$tbl_meta{$tbl}->{colors}};
|
|
return ($idx - 1) % $num_rules;
|
|
},
|
|
},
|
|
'+' => {
|
|
note => 'Move selected rule up one',
|
|
func => sub {
|
|
my ( $tbl, $idx ) = @_;
|
|
my $meta = $tbl_meta{$tbl};
|
|
my $dest = $idx == 0 ? scalar(@{$meta->{colors}} - 1) : $idx - 1;
|
|
my $temp = $meta->{colors}->[$idx];
|
|
$meta->{colors}->[$idx] = $meta->{colors}->[$dest];
|
|
$meta->{colors}->[$dest] = $temp;
|
|
$meta->{cust}->{colors} = 1;
|
|
return $dest;
|
|
},
|
|
},
|
|
'-' => {
|
|
note => 'Move selected rule down one',
|
|
func => sub {
|
|
my ( $tbl, $idx ) = @_;
|
|
my $meta = $tbl_meta{$tbl};
|
|
my $dest = $idx == scalar(@{$meta->{colors}} - 1) ? 0 : $idx + 1;
|
|
my $temp = $meta->{colors}->[$idx];
|
|
$meta->{colors}->[$idx] = $meta->{colors}->[$dest];
|
|
$meta->{colors}->[$dest] = $temp;
|
|
$meta->{cust}->{colors} = 1;
|
|
return $dest;
|
|
},
|
|
},
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Plugin editor key mappings {{{3
|
|
# ###########################################################################
|
|
my %plugin_editor_action = (
|
|
'*' => {
|
|
note => 'Toggle selected plugin active/inactive',
|
|
func => sub {
|
|
my ( $plugins, $idx ) = @_;
|
|
my $plugin = $plugins->[$idx];
|
|
$plugin->{active} = $plugin->{active} ? 0 : 1;
|
|
return $idx;
|
|
},
|
|
},
|
|
j => {
|
|
note => 'Move highlight down one',
|
|
func => sub {
|
|
my ( $plugins, $idx ) = @_;
|
|
return ($idx + 1) % scalar(@$plugins);
|
|
},
|
|
},
|
|
k => {
|
|
note => 'Move highlight up one',
|
|
func => sub {
|
|
my ( $plugins, $idx ) = @_;
|
|
return $idx == 0 ? @$plugins - 1 : $idx - 1;
|
|
},
|
|
},
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Table editor key mappings {{{3
|
|
# ###########################################################################
|
|
my %tbl_editor_action = (
|
|
a => {
|
|
note => 'Add a column to the table',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
my @visible_cols = @{ $tbl_meta{$tbl}->{visible} };
|
|
my %all_cols = %{ $tbl_meta{$tbl}->{cols} };
|
|
delete @all_cols{@visible_cols};
|
|
my $choice = prompt_list(
|
|
'Choose a column',
|
|
'',
|
|
sub { return keys %all_cols; },
|
|
{ map { $_ => $all_cols{$_}->{label} || $all_cols{$_}->{hdr} } keys %all_cols });
|
|
if ( $all_cols{$choice} ) {
|
|
push @{$tbl_meta{$tbl}->{visible}}, $choice;
|
|
$tbl_meta{$tbl}->{cust}->{visible} = 1;
|
|
return $choice;
|
|
}
|
|
return $col;
|
|
},
|
|
},
|
|
n => {
|
|
note => 'Create a new column and add it to the table',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
|
|
$clear_screen_sub->();
|
|
print word_wrap("Choose a name for the column. This name is not displayed, and is used only "
|
|
. "for internal reference. It can contain only lowercase letters, numbers, "
|
|
. "and underscores.");
|
|
print "\n\n";
|
|
do {
|
|
$col = prompt("Enter column name");
|
|
$col = '' if $col =~ m/[^a-z0-9_]/;
|
|
} while ( !$col );
|
|
|
|
$clear_screen_sub->();
|
|
my $hdr;
|
|
do {
|
|
$hdr = prompt("Enter column header");
|
|
} while ( !$hdr );
|
|
|
|
$clear_screen_sub->();
|
|
print "Choose a source for the column's data\n\n";
|
|
my ( $src, $sub, $err );
|
|
do {
|
|
if ( $err ) {
|
|
print "Error: $err\n\n";
|
|
}
|
|
$src = prompt("Enter column source");
|
|
if ( $src ) {
|
|
( $sub, $err ) = compile_expr($src);
|
|
}
|
|
} until ( !$err);
|
|
|
|
# TODO: this duplicates %col_props.
|
|
$tbl_meta{$tbl}->{cols}->{$col} = {
|
|
hdr => $hdr,
|
|
src => $src,
|
|
just => '-',
|
|
num => 0,
|
|
label => 'User-defined',
|
|
user => 1,
|
|
tbl => $tbl,
|
|
minw => 0,
|
|
maxw => 0,
|
|
trans => [],
|
|
func => $sub,
|
|
dec => 0,
|
|
agg => 0,
|
|
aggonly => 0,
|
|
};
|
|
|
|
$tbl_meta{$tbl}->{visible} = [ unique(@{$tbl_meta{$tbl}->{visible}}, $col) ];
|
|
$tbl_meta{$tbl}->{cust}->{visible} = 1;
|
|
return $col;
|
|
},
|
|
},
|
|
d => {
|
|
note => 'Remove selected column',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
my @visible_cols = @{ $tbl_meta{$tbl}->{visible} };
|
|
my $idx = 0;
|
|
return $col unless @visible_cols > 1;
|
|
while ( $visible_cols[$idx] ne $col ) {
|
|
$idx++;
|
|
}
|
|
$tbl_meta{$tbl}->{visible} = [ grep { $_ ne $col } @visible_cols ];
|
|
$tbl_meta{$tbl}->{cust}->{visible} = 1;
|
|
return $idx == $#visible_cols ? $visible_cols[$idx - 1] : $visible_cols[$idx + 1];
|
|
},
|
|
},
|
|
e => {
|
|
note => 'Edit selected column',
|
|
func => sub {
|
|
# TODO: make this editor hotkey-driven and give readline support.
|
|
my ( $tbl, $col ) = @_;
|
|
$clear_screen_sub->();
|
|
my $meta = $tbl_meta{$tbl}->{cols}->{$col};
|
|
my @prop = qw(hdr label src just num minw maxw trans agg); # TODO redundant
|
|
|
|
my $answer;
|
|
do {
|
|
# Do what the user asked...
|
|
if ( $answer && grep { $_ eq $answer } @prop ) {
|
|
# Some properties are arrays, others scalars.
|
|
my $ini = ref $col_props{$answer} ? join(' ', @{$meta->{$answer}}) : $meta->{$answer};
|
|
my $val = prompt("New value for $answer", undef, $ini);
|
|
$val = [ split(' ', $val) ] if ref($col_props{$answer});
|
|
if ( $answer eq 'trans' ) {
|
|
$val = [ unique(grep{ exists $trans_funcs{$_} } @$val) ];
|
|
}
|
|
@{$meta}{$answer, 'user', 'tbl' } = ( $val, 1, $tbl );
|
|
}
|
|
|
|
my @display_lines = (
|
|
'',
|
|
"You are editing column $tbl.$col.\n",
|
|
);
|
|
|
|
push @display_lines, create_table2(
|
|
\@prop,
|
|
{ map { $_ => $_ } @prop },
|
|
{ map { $_ => ref $meta->{$_} eq 'ARRAY' ? join(' ', @{$meta->{$_}})
|
|
: ref $meta->{$_} ? '[expression code]'
|
|
: $meta->{$_}
|
|
} @prop
|
|
},
|
|
{ sep => ' ' });
|
|
draw_screen(\@display_lines, { raw => 1 });
|
|
print "\n\n"; # One to add space, one to clear readline artifacts
|
|
$answer = prompt('Edit what? (q to quit)');
|
|
} while ( $answer ne 'q' );
|
|
|
|
return $col;
|
|
},
|
|
},
|
|
j => {
|
|
note => 'Move highlight down one',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
my @visible_cols = @{ $tbl_meta{$tbl}->{visible} };
|
|
my $idx = 0;
|
|
while ( $visible_cols[$idx] ne $col ) {
|
|
$idx++;
|
|
}
|
|
return $visible_cols[ ($idx + 1) % @visible_cols ];
|
|
},
|
|
},
|
|
k => {
|
|
note => 'Move highlight up one',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
my @visible_cols = @{ $tbl_meta{$tbl}->{visible} };
|
|
my $idx = 0;
|
|
while ( $visible_cols[$idx] ne $col ) {
|
|
$idx++;
|
|
}
|
|
return $visible_cols[ $idx - 1 ];
|
|
},
|
|
},
|
|
'+' => {
|
|
note => 'Move selected column up one',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
my $meta = $tbl_meta{$tbl};
|
|
my @visible_cols = @{$meta->{visible}};
|
|
my $idx = 0;
|
|
while ( $visible_cols[$idx] ne $col ) {
|
|
$idx++;
|
|
}
|
|
if ( $idx ) {
|
|
$visible_cols[$idx] = $visible_cols[$idx - 1];
|
|
$visible_cols[$idx - 1] = $col;
|
|
$meta->{visible} = \@visible_cols;
|
|
}
|
|
else {
|
|
shift @{$meta->{visible}};
|
|
push @{$meta->{visible}}, $col;
|
|
}
|
|
$meta->{cust}->{visible} = 1;
|
|
return $col;
|
|
},
|
|
},
|
|
'-' => {
|
|
note => 'Move selected column down one',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
my $meta = $tbl_meta{$tbl};
|
|
my @visible_cols = @{$meta->{visible}};
|
|
my $idx = 0;
|
|
while ( $visible_cols[$idx] ne $col ) {
|
|
$idx++;
|
|
}
|
|
if ( $idx == $#visible_cols ) {
|
|
unshift @{$meta->{visible}}, $col;
|
|
pop @{$meta->{visible}};
|
|
}
|
|
else {
|
|
$visible_cols[$idx] = $visible_cols[$idx + 1];
|
|
$visible_cols[$idx + 1] = $col;
|
|
$meta->{visible} = \@visible_cols;
|
|
}
|
|
$meta->{cust}->{visible} = 1;
|
|
return $col;
|
|
},
|
|
},
|
|
f => {
|
|
note => 'Choose filters',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
choose_filters($tbl);
|
|
return $col;
|
|
},
|
|
},
|
|
o => {
|
|
note => 'Edit color rules',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
edit_color_rules($tbl);
|
|
return $col;
|
|
},
|
|
},
|
|
s => {
|
|
note => 'Choose sort columns',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
choose_sort_cols($tbl);
|
|
return $col;
|
|
},
|
|
},
|
|
g => {
|
|
note => 'Choose group-by (aggregate) columns',
|
|
func => sub {
|
|
my ( $tbl, $col ) = @_;
|
|
choose_group_cols($tbl);
|
|
return $col;
|
|
},
|
|
},
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Global variables and environment {{{2
|
|
# ###########################################################################
|
|
|
|
my @this_term_size; # w_chars, h_chars, w_pix, h_pix
|
|
my @last_term_size; # w_chars, h_chars, w_pix, h_pix
|
|
my $char;
|
|
my $windows = $OSNAME =~ m/MSWin/;
|
|
my $have_color = 0;
|
|
my $MAX_ULONG = 4294967295; # 2^32-1
|
|
my $num_regex = qr/^[+-]?(?=\d|\.)\d*(?:\.\d+)?(?:E[+-]?\d+|)$/i;
|
|
my $int_regex = qr/^\d+$/;
|
|
my $bool_regex = qr/^[01]$/;
|
|
my $term = undef;
|
|
my $file = undef; # File to watch for InnoDB monitor output
|
|
my $file_mtime = undef; # Status of watched file
|
|
my $file_data = undef; # Last chunk of text read from file
|
|
my $innodb_parser = InnoDBParser->new;
|
|
|
|
my $nonfatal_errs = join('|',
|
|
'Access denied for user',
|
|
'Unknown MySQL server host',
|
|
'Unknown database',
|
|
'Can\'t connect to local MySQL server through socket',
|
|
'Can\'t connect to MySQL server on',
|
|
'MySQL server has gone away',
|
|
'Cannot call SHOW INNODB STATUS',
|
|
'Access denied',
|
|
'AutoCommit',
|
|
);
|
|
|
|
if ( !$opts{n} ) {
|
|
require Term::ReadLine;
|
|
$term = Term::ReadLine->new('innotop');
|
|
}
|
|
|
|
# Stores status, variables, innodb status, master/slave status etc.
|
|
# Keyed on connection name. Each entry is a hashref of current and past data sets,
|
|
# keyed on clock tick.
|
|
my %vars;
|
|
my %info_gotten = (); # Which things have been retrieved for the current clock tick.
|
|
|
|
# Stores info on currently displayed queries: cxn, connection ID, query text.
|
|
my @current_queries;
|
|
|
|
my $lines_printed = 0;
|
|
my $clock = 0; # Incremented with every wake-sleep cycle
|
|
my $clearing_deadlocks = 0;
|
|
|
|
# If terminal coloring is available, use it. The only function I want from
|
|
# the module is the colored() function.
|
|
eval {
|
|
if ( !$opts{n} ) {
|
|
if ( $windows ) {
|
|
require Win32::Console::ANSI;
|
|
}
|
|
require Term::ANSIColor;
|
|
import Term::ANSIColor qw(colored);
|
|
$have_color = 1;
|
|
}
|
|
};
|
|
if ( $EVAL_ERROR || $opts{n} ) {
|
|
# If there was an error, manufacture my own colored() function that does no
|
|
# coloring.
|
|
*colored = sub { pop @_; @_; };
|
|
}
|
|
|
|
if ( $opts{n} ) {
|
|
$clear_screen_sub = sub {};
|
|
}
|
|
elsif ( $windows ) {
|
|
$clear_screen_sub = sub { $lines_printed = 0; system("cls") };
|
|
}
|
|
else {
|
|
my $clear = `clear`;
|
|
$clear_screen_sub = sub { $lines_printed = 0; print $clear };
|
|
}
|
|
|
|
# ###########################################################################
|
|
# Config storage. {{{2
|
|
# ###########################################################################
|
|
my %config = (
|
|
color => {
|
|
val => $have_color,
|
|
note => 'Whether to use terminal coloring',
|
|
conf => 'ALL',
|
|
pat => $bool_regex,
|
|
},
|
|
cmd_filter => {
|
|
val => 'Com_',
|
|
note => 'Prefix for values in C mode',
|
|
conf => [qw(C)],
|
|
},
|
|
plugin_dir => {
|
|
val => "$homepath/.innotop/plugins",
|
|
note => 'Directory where plugins can be found',
|
|
conf => 'ALL',
|
|
},
|
|
show_percent => {
|
|
val => 1,
|
|
note => 'Show the % symbol after percentages',
|
|
conf => 'ALL',
|
|
pat => $bool_regex,
|
|
},
|
|
skip_innodb => {
|
|
val => 0,
|
|
note => 'Disable SHOW INNODB STATUS',
|
|
conf => 'ALL',
|
|
pat => $bool_regex,
|
|
},
|
|
S_func => {
|
|
val => 's',
|
|
note => 'What to display in S mode: graph, status, pivoted status',
|
|
conf => [qw(S)],
|
|
pat => qr/^[gsv]$/,
|
|
},
|
|
cxn_timeout => {
|
|
val => 28800,
|
|
note => 'Connection timeout for keeping unused connections alive',
|
|
conf => 'ALL',
|
|
pat => $int_regex,
|
|
},
|
|
graph_char => {
|
|
val => '*',
|
|
note => 'Character for drawing graphs',
|
|
conf => [ qw(S) ],
|
|
pat => qr/^.$/,
|
|
},
|
|
show_cxn_errors_in_tbl => {
|
|
val => 1,
|
|
note => 'Whether to display connection errors as rows in the table',
|
|
conf => 'ALL',
|
|
pat => $bool_regex,
|
|
},
|
|
hide_hdr => {
|
|
val => 0,
|
|
note => 'Whether to show column headers',
|
|
conf => 'ALL',
|
|
pat => $bool_regex,
|
|
},
|
|
show_cxn_errors => {
|
|
val => 1,
|
|
note => 'Whether to print connection errors to STDOUT',
|
|
conf => 'ALL',
|
|
pat => $bool_regex,
|
|
},
|
|
readonly => {
|
|
val => 1,
|
|
note => 'Whether the config file is read-only',
|
|
conf => [ qw() ],
|
|
pat => $bool_regex,
|
|
},
|
|
global => {
|
|
val => 1,
|
|
note => 'Whether to show GLOBAL variables and status',
|
|
conf => 'ALL',
|
|
pat => $bool_regex,
|
|
},
|
|
header_highlight => {
|
|
val => 'bold',
|
|
note => 'How to highlight table column headers',
|
|
conf => 'ALL',
|
|
pat => qr/^(?:bold|underline)$/,
|
|
},
|
|
display_table_captions => {
|
|
val => 1,
|
|
note => 'Whether to put captions on tables',
|
|
conf => 'ALL',
|
|
pat => $bool_regex,
|
|
},
|
|
charset => {
|
|
val => 'ascii',
|
|
note => 'What type of characters should be displayed in queries (ascii, unicode, none)',
|
|
conf => 'ALL',
|
|
pat => qr/^(?:ascii|unicode|none)$/,
|
|
},
|
|
auto_wipe_dl => {
|
|
val => 0,
|
|
note => 'Whether to auto-wipe InnoDB deadlocks',
|
|
conf => 'ALL',
|
|
pat => $bool_regex,
|
|
},
|
|
max_height => {
|
|
val => 30,
|
|
note => '[Win32] Max window height',
|
|
conf => 'ALL',
|
|
},
|
|
debug => {
|
|
val => 0,
|
|
pat => $bool_regex,
|
|
note => 'Debug mode (more verbose errors, uses more memory)',
|
|
conf => 'ALL',
|
|
},
|
|
num_digits => {
|
|
val => 2,
|
|
pat => $int_regex,
|
|
note => 'How many digits to show in fractional numbers and percents',
|
|
conf => 'ALL',
|
|
},
|
|
debugfile => {
|
|
val => "$homepath/.innotop/core_dump",
|
|
note => 'A debug file in case you are interested in error output',
|
|
},
|
|
show_statusbar => {
|
|
val => 1,
|
|
pat => $bool_regex,
|
|
note => 'Whether to show the status bar in the display',
|
|
conf => 'ALL',
|
|
},
|
|
mode => {
|
|
val => "Q",
|
|
note => "Which mode to start in",
|
|
cmdline => 1,
|
|
},
|
|
status_inc => {
|
|
val => 0,
|
|
note => 'Whether to show raw or incremental values for status variables',
|
|
pat => $bool_regex,
|
|
},
|
|
interval => {
|
|
val => 10,
|
|
pat => qr/^(?:(?:\d*?[1-9]\d*(?:\.\d*)?)|(?:\d*\.\d*?[1-9]\d*))$/,
|
|
note => "The interval at which the display will be refreshed. Fractional values allowed.",
|
|
},
|
|
num_status_sets => {
|
|
val => 9,
|
|
pat => $int_regex,
|
|
note => 'How many sets of STATUS and VARIABLES values to show',
|
|
conf => [ qw(S) ],
|
|
},
|
|
S_set => {
|
|
val => 'general',
|
|
pat => qr/^\w+$/,
|
|
note => 'Which set of variables to display in S (Variables & Status) mode',
|
|
conf => [ qw(S) ],
|
|
},
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Config file sections {{{2
|
|
# The configuration file is broken up into sections like a .ini file. This
|
|
# variable defines those sections and the subroutines responsible for reading
|
|
# and writing them.
|
|
# ###########################################################################
|
|
my %config_file_sections = (
|
|
plugins => {
|
|
reader => \&load_config_plugins,
|
|
writer => \&save_config_plugins,
|
|
},
|
|
group_by => {
|
|
reader => \&load_config_group_by,
|
|
writer => \&save_config_group_by,
|
|
},
|
|
filters => {
|
|
reader => \&load_config_filters,
|
|
writer => \&save_config_filters,
|
|
},
|
|
active_filters => {
|
|
reader => \&load_config_active_filters,
|
|
writer => \&save_config_active_filters,
|
|
},
|
|
visible_tables => {
|
|
reader => \&load_config_visible_tables,
|
|
writer => \&save_config_visible_tables,
|
|
},
|
|
sort_cols => {
|
|
reader => \&load_config_sort_cols,
|
|
writer => \&save_config_sort_cols,
|
|
},
|
|
active_columns => {
|
|
reader => \&load_config_active_columns,
|
|
writer => \&save_config_active_columns,
|
|
},
|
|
tbl_meta => {
|
|
reader => \&load_config_tbl_meta,
|
|
writer => \&save_config_tbl_meta,
|
|
},
|
|
general => {
|
|
reader => \&load_config_config,
|
|
writer => \&save_config_config,
|
|
},
|
|
connections => {
|
|
reader => \&load_config_connections,
|
|
writer => \&save_config_connections,
|
|
},
|
|
active_connections => {
|
|
reader => \&load_config_active_connections,
|
|
writer => \&save_config_active_connections,
|
|
},
|
|
server_groups => {
|
|
reader => \&load_config_server_groups,
|
|
writer => \&save_config_server_groups,
|
|
},
|
|
active_server_groups => {
|
|
reader => \&load_config_active_server_groups,
|
|
writer => \&save_config_active_server_groups,
|
|
},
|
|
max_values_seen => {
|
|
reader => \&load_config_mvs,
|
|
writer => \&save_config_mvs,
|
|
},
|
|
varsets => {
|
|
reader => \&load_config_varsets,
|
|
writer => \&save_config_varsets,
|
|
},
|
|
colors => {
|
|
reader => \&load_config_colors,
|
|
writer => \&save_config_colors,
|
|
},
|
|
stmt_sleep_times => {
|
|
reader => \&load_config_stmt_sleep_times,
|
|
writer => \&save_config_stmt_sleep_times,
|
|
},
|
|
);
|
|
|
|
# Config file sections have some dependencies, so they have to be read/written in order.
|
|
my @ordered_config_file_sections = qw(general plugins filters active_filters tbl_meta
|
|
connections active_connections server_groups active_server_groups max_values_seen
|
|
active_columns sort_cols visible_tables varsets colors stmt_sleep_times
|
|
group_by);
|
|
|
|
# All events for which plugins may register themselves. Entries are arrayrefs.
|
|
my %event_listener_for = map { $_ => [] }
|
|
qw(
|
|
extract_values
|
|
set_to_tbl_pre_filter set_to_tbl_pre_sort set_to_tbl_pre_group
|
|
set_to_tbl_pre_colorize set_to_tbl_pre_transform set_to_tbl_pre_pivot
|
|
set_to_tbl_pre_create set_to_tbl_post_create
|
|
draw_screen
|
|
);
|
|
|
|
# All variables to which plugins have access.
|
|
my %pluggable_vars = (
|
|
action_for => \%action_for,
|
|
agg_funcs => \%agg_funcs,
|
|
config => \%config,
|
|
connections => \%connections,
|
|
dbhs => \%dbhs,
|
|
filters => \%filters,
|
|
modes => \%modes,
|
|
server_groups => \%server_groups,
|
|
tbl_meta => \%tbl_meta,
|
|
trans_funcs => \%trans_funcs,
|
|
var_sets => \%var_sets,
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Contains logic to generate prepared statements for a given function for a
|
|
# given DB connection. Returns a $sth.
|
|
# ###########################################################################
|
|
my %stmt_maker_for = (
|
|
INNODB_STATUS => sub {
|
|
my ( $dbh ) = @_;
|
|
return $dbh->prepare(version_ge( $dbh, '5.0.0' )
|
|
? 'SHOW ENGINE INNODB STATUS'
|
|
: 'SHOW INNODB STATUS');
|
|
},
|
|
SHOW_VARIABLES => sub {
|
|
my ( $dbh ) = @_;
|
|
return $dbh->prepare($config{global}->{val} && version_ge( $dbh, '4.0.3' )
|
|
? 'SHOW GLOBAL VARIABLES'
|
|
: 'SHOW VARIABLES');
|
|
},
|
|
SHOW_STATUS => sub {
|
|
my ( $dbh ) = @_;
|
|
return $dbh->prepare($config{global}->{val} && version_ge( $dbh, '5.0.2' )
|
|
? 'SHOW GLOBAL STATUS'
|
|
: 'SHOW STATUS');
|
|
},
|
|
KILL_QUERY => sub {
|
|
my ( $dbh ) = @_;
|
|
return $dbh->prepare(version_ge( $dbh, '5.0.0' )
|
|
? 'KILL QUERY ?'
|
|
: 'KILL ?');
|
|
},
|
|
SHOW_MASTER_LOGS => sub {
|
|
my ( $dbh ) = @_;
|
|
return $dbh->prepare('SHOW MASTER LOGS');
|
|
},
|
|
SHOW_MASTER_STATUS => sub {
|
|
my ( $dbh ) = @_;
|
|
return $dbh->prepare('SHOW MASTER STATUS');
|
|
},
|
|
SHOW_SLAVE_STATUS => sub {
|
|
my ( $dbh ) = @_;
|
|
return $dbh->prepare('SHOW SLAVE STATUS');
|
|
},
|
|
KILL_CONNECTION => sub {
|
|
my ( $dbh ) = @_;
|
|
return $dbh->prepare(version_ge( $dbh, '5.0.0' )
|
|
? 'KILL CONNECTION ?'
|
|
: 'KILL ?');
|
|
},
|
|
OPEN_TABLES => sub {
|
|
my ( $dbh ) = @_;
|
|
return version_ge($dbh, '4.0.0')
|
|
? $dbh->prepare('SHOW OPEN TABLES')
|
|
: undef;
|
|
},
|
|
PROCESSLIST => sub {
|
|
my ( $dbh ) = @_;
|
|
return $dbh->prepare('SHOW FULL PROCESSLIST');
|
|
},
|
|
);
|
|
|
|
# Plugins!
|
|
my %plugins = (
|
|
);
|
|
|
|
# ###########################################################################
|
|
# Run the program {{{1
|
|
# ###########################################################################
|
|
|
|
# This config variable is only useful for MS Windows because its terminal
|
|
# can't tell how tall it is.
|
|
if ( !$windows ) {
|
|
delete $config{max_height};
|
|
}
|
|
|
|
# Try to lower my priority.
|
|
eval { setpriority(0, 0, getpriority(0, 0) + 10); };
|
|
|
|
# Print stuff to the screen immediately, don't wait for a newline.
|
|
$OUTPUT_AUTOFLUSH = 1;
|
|
|
|
# Clear the screen and load the configuration.
|
|
$clear_screen_sub->();
|
|
load_config();
|
|
|
|
# Override config variables with command-line options
|
|
my %cmdline =
|
|
map { $_->{c} => $opts{$_->{k}} }
|
|
grep { exists $_->{c} && exists $opts{$_->{k}} }
|
|
@opt_spec;
|
|
|
|
foreach my $name (keys %cmdline) {
|
|
next if not defined $cmdline{$name};
|
|
my $val = $cmdline{$name};
|
|
if ( exists($config{$name}) and (!$config{$name}->{pat} or $val =~ m/$config{$name}->{pat}/ )) {
|
|
$config{$name}->{val} = $val;
|
|
}
|
|
}
|
|
|
|
post_process_tbl_meta();
|
|
|
|
# Make sure no changes are written to config file in non-interactive mode.
|
|
if ( $opts{n} ) {
|
|
$config{readonly}->{val} = 1;
|
|
}
|
|
|
|
eval {
|
|
|
|
# Open the file for InnoDB status
|
|
if ( @ARGV ) {
|
|
my $filename = shift @ARGV;
|
|
open $file, "<", $filename
|
|
or die "Cannot open '$filename': $OS_ERROR";
|
|
}
|
|
|
|
# In certain modes we might have to collect data for two cycles
|
|
# before printing anything out, so we need to bump up the count one.
|
|
if ( $opts{n} && $opts{count} && $config{status_inc}->{val}
|
|
&& $config{mode}->{val} =~ m/[S]/ )
|
|
{
|
|
$opts{count}++;
|
|
}
|
|
|
|
while (++$clock) {
|
|
|
|
my $mode = $config{mode}->{val} || 'Q';
|
|
if ( !$modes{$mode} ) {
|
|
die "Mode '$mode' doesn't exist; try one of these:\n"
|
|
. join("\n", map { " $_ $modes{$_}->{hdr}" } sort keys %modes)
|
|
. "\n";
|
|
}
|
|
|
|
if ( !$opts{n} ) {
|
|
@last_term_size = @this_term_size;
|
|
@this_term_size = Term::ReadKey::GetTerminalSize(\*STDOUT);
|
|
if ( $windows ) {
|
|
$this_term_size[0]--;
|
|
$this_term_size[1]
|
|
= min($this_term_size[1], $config{max_height}->{val});
|
|
}
|
|
die("Can't read terminal size") unless @this_term_size;
|
|
}
|
|
|
|
# If there's no connection to a database server, we need to fix that...
|
|
if ( !%connections ) {
|
|
print "You have not defined any database connections.\n\n";
|
|
add_new_dsn();
|
|
}
|
|
|
|
# See whether there are any connections defined for this mode. If there's only one
|
|
# connection total, assume the user wants to just use innotop for a single server
|
|
# and don't ask which server to connect to. Also, if we're monitoring from a file,
|
|
# we just use the first connection.
|
|
if ( !get_connections() ) {
|
|
if ( $file || 1 == scalar keys %connections ) {
|
|
$modes{$config{mode}->{val}}->{connections} = [ keys %connections ];
|
|
}
|
|
else {
|
|
choose_connections();
|
|
}
|
|
}
|
|
|
|
# Term::ReadLine might have re-set $OUTPUT_AUTOFLUSH.
|
|
$OUTPUT_AUTOFLUSH = 1;
|
|
|
|
# Prune old data
|
|
my $sets = $config{num_status_sets}->{val};
|
|
foreach my $store ( values %vars ) {
|
|
delete @{$store}{ grep { $_ < $clock - $sets } keys %$store };
|
|
}
|
|
%info_gotten = ();
|
|
|
|
# Call the subroutine to display this mode.
|
|
$modes{$mode}->{display_sub}->();
|
|
|
|
# It may be time to quit now.
|
|
if ( $opts{count} && $clock >= $opts{count} ) {
|
|
finish();
|
|
}
|
|
|
|
# Wait for a bit.
|
|
if ( $opts{n} ) {
|
|
sleep($config{interval}->{val});
|
|
}
|
|
else {
|
|
ReadMode('cbreak');
|
|
$char = ReadKey($config{interval}->{val});
|
|
ReadMode('normal');
|
|
}
|
|
|
|
# Handle whatever action the key indicates.
|
|
do_key_action();
|
|
|
|
}
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
core_dump( $EVAL_ERROR );
|
|
}
|
|
finish();
|
|
|
|
# Subroutines {{{1
|
|
# Mode functions{{{2
|
|
# switch_mode {{{3
|
|
sub switch_mode {
|
|
my $mode = shift;
|
|
$config{mode}->{val} = $mode;
|
|
}
|
|
|
|
# Prompting functions {{{2
|
|
# prompt_list {{{3
|
|
# Prompts the user for a value, given a question, initial value,
|
|
# a completion function and a hashref of hints.
|
|
sub prompt_list {
|
|
die "Can't call in non-interactive mode" if $opts{n};
|
|
my ( $question, $init, $completion, $hints ) = @_;
|
|
if ( $hints ) {
|
|
# Figure out how wide the table will be
|
|
my $max_name = max(map { length($_) } keys %$hints );
|
|
$max_name ||= 0;
|
|
$max_name += 3;
|
|
my @meta_rows = create_table2(
|
|
[ sort keys %$hints ],
|
|
{ map { $_ => $_ } keys %$hints },
|
|
{ map { $_ => trunc($hints->{$_}, $this_term_size[0] - $max_name) } keys %$hints },
|
|
{ sep => ' ' });
|
|
if (@meta_rows > 10) {
|
|
# Try to split and stack the meta rows next to each other
|
|
my $split = int(@meta_rows / 2);
|
|
@meta_rows = stack_next(
|
|
[@meta_rows[0..$split - 1]],
|
|
[@meta_rows[$split..$#meta_rows]],
|
|
{ pad => ' | '},
|
|
);
|
|
}
|
|
print join( "\n",
|
|
'',
|
|
map { ref $_ ? colored(@$_) : $_ } create_caption('Choose from', @meta_rows), ''),
|
|
"\n";
|
|
}
|
|
$term->Attribs->{completion_function} = $completion;
|
|
my $answer = $term->readline("$question: ", $init);
|
|
$OUTPUT_AUTOFLUSH = 1;
|
|
$answer = '' if !defined($answer);
|
|
$answer =~ s/\s+$//;
|
|
return $answer;
|
|
}
|
|
|
|
# prompt {{{3
|
|
# Prints out a prompt and reads from the keyboard, then validates with the
|
|
# validation regex until the input is correct.
|
|
sub prompt {
|
|
die "Can't call in non-interactive mode" if $opts{n};
|
|
my ( $prompt, $regex, $init, $completion ) = @_;
|
|
my $response;
|
|
my $success = 0;
|
|
do {
|
|
if ( $completion ) {
|
|
$term->Attribs->{completion_function} = $completion;
|
|
}
|
|
$response = $term->readline("$prompt: ", $init);
|
|
if ( $regex && $response !~ m/$regex/ ) {
|
|
print "Invalid response.\n\n";
|
|
}
|
|
else {
|
|
$success = 1;
|
|
}
|
|
} while ( !$success );
|
|
$OUTPUT_AUTOFLUSH = 1;
|
|
$response =~ s/\s+$//;
|
|
return $response;
|
|
}
|
|
|
|
# prompt_noecho {{{3
|
|
# Unfortunately, suppressing echo with Term::ReadLine isn't reliable; the user might not
|
|
# have that library, or it might not support that feature.
|
|
sub prompt_noecho {
|
|
my ( $prompt ) = @_;
|
|
print colored("$prompt: ", 'underline');
|
|
my $response;
|
|
ReadMode('noecho');
|
|
$response = <STDIN>;
|
|
chomp($response);
|
|
ReadMode('normal');
|
|
return $response;
|
|
}
|
|
|
|
# do_key_action {{{3
|
|
# Depending on whether a key was read, do something. Keys have certain
|
|
# actions defined in lookup tables. Each mode may have its own lookup table,
|
|
# which trumps the global table -- so keys can be context-sensitive. The key
|
|
# may be read and written in a subroutine, so it's a global.
|
|
sub do_key_action {
|
|
if ( defined $char ) {
|
|
my $mode = $config{mode}->{val};
|
|
my $action
|
|
= defined($modes{$mode}->{action_for}->{$char})
|
|
? $modes{$mode}->{action_for}->{$char}->{action}
|
|
: defined($action_for{$char})
|
|
? $action_for{$char}->{action}
|
|
: sub{};
|
|
$action->();
|
|
}
|
|
}
|
|
|
|
# pause {{{3
|
|
sub pause {
|
|
die "Can't call in non-interactive mode" if $opts{n};
|
|
my $msg = shift;
|
|
print defined($msg) ? "\n$msg" : "\nPress any key to continue";
|
|
ReadMode('cbreak');
|
|
my $char = ReadKey(0);
|
|
ReadMode('normal');
|
|
return $char;
|
|
}
|
|
|
|
# reverse_sort {{{3
|
|
sub reverse_sort {
|
|
my $tbl = shift;
|
|
$tbl_meta{$tbl}->{sort_dir} *= -1;
|
|
}
|
|
|
|
# select_cxn {{{3
|
|
# Selects connection(s). If the mode (or argument list) has only one, returns
|
|
# it without prompt.
|
|
sub select_cxn {
|
|
my ( $prompt, @cxns ) = @_;
|
|
if ( !@cxns ) {
|
|
@cxns = get_connections();
|
|
}
|
|
if ( @cxns == 1 ) {
|
|
return $cxns[0];
|
|
}
|
|
my $choices = prompt_list(
|
|
$prompt,
|
|
$cxns[0],
|
|
sub{ return @cxns },
|
|
{ map { $_ => $connections{$_}->{dsn} } @cxns });
|
|
my @result = unique(grep { my $a = $_; grep { $_ eq $a } @cxns } split(/\s+/, $choices));
|
|
return @result;
|
|
}
|
|
|
|
# kill_query {{{3
|
|
# Kills a connection, or on new versions, optionally a query but not connection.
|
|
sub kill_query {
|
|
my ( $q_or_c ) = @_;
|
|
|
|
my $info = choose_thread(
|
|
sub { 1 },
|
|
'Select a thread to kill the ' . $q_or_c,
|
|
);
|
|
return unless $info;
|
|
return unless pause("Kill $info->{id}?") =~ m/y/i;
|
|
|
|
eval {
|
|
do_stmt($info->{cxn}, $q_or_c eq 'QUERY' ? 'KILL_QUERY' : 'KILL_CONNECTION', $info->{id} );
|
|
};
|
|
|
|
if ( $EVAL_ERROR ) {
|
|
print "\nError: $EVAL_ERROR";
|
|
pause();
|
|
}
|
|
}
|
|
|
|
# set_display_precision {{{3
|
|
sub set_display_precision {
|
|
my $dir = shift;
|
|
$config{num_digits}->{val} = min(9, max(0, $config{num_digits}->{val} + $dir));
|
|
}
|
|
|
|
sub toggle_visible_table {
|
|
my ( $mode, $table ) = @_;
|
|
my $visible = $modes{$mode}->{visible_tables};
|
|
if ( grep { $_ eq $table } @$visible ) {
|
|
$modes{$mode}->{visible_tables} = [ grep { $_ ne $table } @$visible ];
|
|
}
|
|
else {
|
|
unshift @$visible, $table;
|
|
}
|
|
$modes{$mode}->{cust}->{visible_tables} = 1;
|
|
}
|
|
|
|
# toggle_filter{{{3
|
|
sub toggle_filter {
|
|
my ( $tbl, $filter ) = @_;
|
|
my $filters = $tbl_meta{$tbl}->{filters};
|
|
if ( grep { $_ eq $filter } @$filters ) {
|
|
$tbl_meta{$tbl}->{filters} = [ grep { $_ ne $filter } @$filters ];
|
|
}
|
|
else {
|
|
push @$filters, $filter;
|
|
}
|
|
$tbl_meta{$tbl}->{cust}->{filters} = 1;
|
|
}
|
|
|
|
# toggle_config {{{3
|
|
sub toggle_config {
|
|
my ( $key ) = @_;
|
|
$config{$key}->{val} ^= 1;
|
|
}
|
|
|
|
# create_deadlock {{{3
|
|
sub create_deadlock {
|
|
$clear_screen_sub->();
|
|
|
|
print "This function will deliberately cause a small deadlock, "
|
|
. "clearing deadlock information from the InnoDB monitor.\n\n";
|
|
|
|
my $answer = prompt("Are you sure you want to proceed? Say 'y' if you do");
|
|
return 0 unless $answer eq 'y';
|
|
|
|
my ( $cxn ) = select_cxn('Clear on which server? ');
|
|
return unless $cxn && exists($connections{$cxn});
|
|
|
|
clear_deadlock($cxn);
|
|
}
|
|
|
|
# deadlock_thread {{{3
|
|
sub deadlock_thread {
|
|
my ( $id, $tbl, $cxn ) = @_;
|
|
|
|
eval {
|
|
my $dbh = get_new_db_connection($cxn, 1);
|
|
my @stmts = (
|
|
"set transaction isolation level serializable",
|
|
(version_ge($dbh, '4.0.11') ? "start transaction" : 'begin'),
|
|
"select * from $tbl where a = $id",
|
|
"update $tbl set a = $id where a <> $id",
|
|
);
|
|
|
|
foreach my $stmt (@stmts[0..2]) {
|
|
$dbh->do($stmt);
|
|
}
|
|
sleep(1 + $id);
|
|
$dbh->do($stmts[-1]);
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
if ( $EVAL_ERROR !~ m/Deadlock found/ ) {
|
|
die $EVAL_ERROR;
|
|
}
|
|
}
|
|
exit(0);
|
|
}
|
|
|
|
# Purges unused binlogs on the master, up to but not including the latest log.
|
|
# TODO: guess which connections are slaves of a given master.
|
|
sub purge_master_logs {
|
|
my @cxns = get_connections();
|
|
|
|
get_master_slave_status(@cxns);
|
|
|
|
# Toss out the rows that don't have master/slave status...
|
|
my @vars =
|
|
grep { $_ && ($_->{file} || $_->{master_host}) }
|
|
map { $vars{$_}->{$clock} } @cxns;
|
|
@cxns = map { $_->{cxn} } @vars;
|
|
|
|
# Figure out which master to purge ons.
|
|
my @masters = map { $_->{cxn} } grep { $_->{file} } @vars;
|
|
my ( $master ) = select_cxn('Which master?', @masters );
|
|
return unless $master;
|
|
my ($master_status) = grep { $_->{cxn} eq $master } @vars;
|
|
|
|
# Figure out the result order (not lexical order) of master logs.
|
|
my @master_logs = get_master_logs($master);
|
|
my $i = 0;
|
|
my %master_logs = map { $_->{log_name} => $i++ } @master_logs;
|
|
|
|
# Ask which slave(s) are reading from this master.
|
|
my @slave_status = grep { $_->{master_host} } @vars;
|
|
my @slaves = map { $_->{cxn} } @slave_status;
|
|
@slaves = select_cxn("Which slaves are reading from $master?", @slaves);
|
|
@slave_status = grep { my $item = $_; grep { $item->{cxn} eq $_ } @slaves } @slave_status;
|
|
return unless @slave_status;
|
|
|
|
# Find the minimum binary log in use.
|
|
my $min_log = min(map { $master_logs{$_->{master_log_file}} } @slave_status);
|
|
my $log_name = $master_logs[$min_log]->{log_name};
|
|
|
|
my $stmt = "PURGE MASTER LOGS TO '$log_name'";
|
|
send_cmd_to_servers($stmt, 0, 'PURGE {MASTER | BINARY} LOGS {TO "log_name" | BEFORE "date"}', [$master]);
|
|
}
|
|
|
|
sub send_cmd_to_servers {
|
|
my ( $cmd, $all, $hint, $cxns ) = @_;
|
|
if ( $all ) {
|
|
@$cxns = get_connections();
|
|
}
|
|
elsif ( !@$cxns ) {
|
|
@$cxns = select_cxn('Which servers?', @$cxns);
|
|
}
|
|
if ( $hint ) {
|
|
print "\nHint: $hint\n";
|
|
}
|
|
$cmd = prompt('Command to send', undef, $cmd);
|
|
foreach my $cxn ( @$cxns ) {
|
|
eval {
|
|
my $sth = do_query($cxn, $cmd);
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
print "Error from $cxn: $EVAL_ERROR\n";
|
|
}
|
|
else {
|
|
print "Success on $cxn\n";
|
|
}
|
|
}
|
|
pause();
|
|
}
|
|
|
|
# Display functions {{{2
|
|
|
|
sub set_s_mode {
|
|
my ( $func ) = @_;
|
|
$clear_screen_sub->();
|
|
$config{S_func}->{val} = $func;
|
|
}
|
|
|
|
# start_S_mode {{{3
|
|
sub start_S_mode {
|
|
$clear_screen_sub->();
|
|
switch_mode('S');
|
|
}
|
|
|
|
# display_B {{{3
|
|
sub display_B {
|
|
my @display_lines;
|
|
my @cxns = get_connections();
|
|
get_innodb_status(\@cxns);
|
|
|
|
my @buffer_pool;
|
|
my @page_statistics;
|
|
my @insert_buffers;
|
|
my @adaptive_hash_index;
|
|
my %rows_for = (
|
|
buffer_pool => \@buffer_pool,
|
|
page_statistics => \@page_statistics,
|
|
insert_buffers => \@insert_buffers,
|
|
adaptive_hash_index => \@adaptive_hash_index,
|
|
);
|
|
|
|
my @visible = get_visible_tables();
|
|
my %wanted = map { $_ => 1 } @visible;
|
|
|
|
foreach my $cxn ( @cxns ) {
|
|
my $set = $vars{$cxn}->{$clock};
|
|
my $pre = $vars{$cxn}->{$clock-1} || $set;
|
|
|
|
if ( $set->{IB_bp_complete} ) {
|
|
if ( $wanted{buffer_pool} ) {
|
|
push @buffer_pool, extract_values($set, $set, $pre, 'buffer_pool');
|
|
}
|
|
if ( $wanted{page_statistics} ) {
|
|
push @page_statistics, extract_values($set, $set, $pre, 'page_statistics');
|
|
}
|
|
}
|
|
if ( $set->{IB_ib_complete} ) {
|
|
if ( $wanted{insert_buffers} ) {
|
|
push @insert_buffers, extract_values(
|
|
$config{status_inc}->{val} ? inc(0, $cxn) : $set, $set, $pre,
|
|
'insert_buffers');
|
|
}
|
|
if ( $wanted{adaptive_hash_index} ) {
|
|
push @adaptive_hash_index, extract_values($set, $set, $pre, 'adaptive_hash_index');
|
|
}
|
|
}
|
|
}
|
|
|
|
my $first_table = 0;
|
|
foreach my $tbl ( @visible ) {
|
|
push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
|
|
push @display_lines, get_cxn_errors(@cxns)
|
|
if ( $config{debug}->{val} || !$first_table++ );
|
|
}
|
|
|
|
draw_screen(\@display_lines);
|
|
}
|
|
|
|
# display_C {{{3
|
|
sub display_C {
|
|
my @display_lines;
|
|
my @cxns = get_connections();
|
|
get_status_info(@cxns);
|
|
|
|
my @cmd_summary;
|
|
my %rows_for = (
|
|
cmd_summary => \@cmd_summary,
|
|
);
|
|
|
|
my @visible = get_visible_tables();
|
|
my %wanted = map { $_ => 1 } @visible;
|
|
|
|
# For now, I'm manually pulling these variables out and pivoting. Eventually a SQL-ish
|
|
# dialect should let me join a table to a grouped and pivoted table and do this more easily.
|
|
# TODO: make it so.
|
|
my $prefix = qr/^$config{cmd_filter}->{val}/; # TODO: this is a total hack
|
|
my @values;
|
|
my ($total, $last_total) = (0, 0);
|
|
foreach my $cxn ( @cxns ) {
|
|
my $set = $vars{$cxn}->{$clock};
|
|
my $pre = $vars{$cxn}->{$clock-1} || $set;
|
|
foreach my $key ( keys %$set ) {
|
|
next unless $key =~ m/$prefix/i;
|
|
my $val = $set->{$key};
|
|
next unless defined $val && $val =~ m/^\d+$/;
|
|
my $last_val = $val - ($pre->{$key} || 0);
|
|
$total += $val;
|
|
$last_total += $last_val;
|
|
push @values, {
|
|
name => $key,
|
|
value => $val,
|
|
last_value => $last_val,
|
|
};
|
|
}
|
|
}
|
|
|
|
# Add aggregation and turn into a real set TODO: total hack
|
|
if ( $wanted{cmd_summary} ) {
|
|
foreach my $value ( @values ) {
|
|
@{$value}{qw(total last_total)} = ($total, $last_total);
|
|
push @cmd_summary, extract_values($value, $value, $value, 'cmd_summary');
|
|
}
|
|
}
|
|
|
|
my $first_table = 0;
|
|
foreach my $tbl ( @visible ) {
|
|
push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
|
|
push @display_lines, get_cxn_errors(@cxns)
|
|
if ( $config{debug}->{val} || !$first_table++ );
|
|
}
|
|
|
|
draw_screen(\@display_lines);
|
|
}
|
|
|
|
# display_D {{{3
|
|
sub display_D {
|
|
my @display_lines;
|
|
my @cxns = get_connections();
|
|
get_innodb_status(\@cxns);
|
|
|
|
my @deadlock_transactions;
|
|
my @deadlock_locks;
|
|
my %rows_for = (
|
|
deadlock_transactions => \@deadlock_transactions,
|
|
deadlock_locks => \@deadlock_locks,
|
|
);
|
|
|
|
my @visible = get_visible_tables();
|
|
my %wanted = map { $_ => 1 } @visible;
|
|
|
|
foreach my $cxn ( @cxns ) {
|
|
my $innodb_status = $vars{$cxn}->{$clock};
|
|
my $prev_status = $vars{$cxn}->{$clock-1} || $innodb_status;
|
|
|
|
if ( $innodb_status->{IB_dl_timestring} ) {
|
|
|
|
my $victim = $innodb_status->{IB_dl_rolled_back} || 0;
|
|
|
|
if ( %wanted ) {
|
|
foreach my $txn_id ( keys %{$innodb_status->{IB_dl_txns}} ) {
|
|
my $txn = $innodb_status->{IB_dl_txns}->{$txn_id};
|
|
my $pre = $prev_status->{IB_dl_txns}->{$txn_id} || $txn;
|
|
|
|
if ( $wanted{deadlock_transactions} ) {
|
|
my $hash = extract_values($txn->{tx}, $txn->{tx}, $pre->{tx}, 'deadlock_transactions');
|
|
$hash->{cxn} = $cxn;
|
|
$hash->{dl_txn_num} = $txn_id;
|
|
$hash->{victim} = $txn_id == $victim ? 'Yes' : 'No';
|
|
$hash->{timestring} = $innodb_status->{IB_dl_timestring};
|
|
$hash->{truncates} = $innodb_status->{IB_dl_complete} ? 'No' : 'Yes';
|
|
push @deadlock_transactions, $hash;
|
|
}
|
|
|
|
if ( $wanted{deadlock_locks} ) {
|
|
foreach my $lock ( @{$txn->{locks}} ) {
|
|
my $hash = extract_values($lock, $lock, $lock, 'deadlock_locks');
|
|
$hash->{dl_txn_num} = $txn_id;
|
|
$hash->{cxn} = $cxn;
|
|
$hash->{mysql_thread_id} = $txn->{tx}->{mysql_thread_id};
|
|
push @deadlock_locks, $hash;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
my $first_table = 0;
|
|
foreach my $tbl ( @visible ) {
|
|
push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
|
|
push @display_lines, get_cxn_errors(@cxns)
|
|
if ( $config{debug}->{val} || !$first_table++ );
|
|
}
|
|
|
|
draw_screen(\@display_lines);
|
|
}
|
|
|
|
# display_F {{{3
|
|
sub display_F {
|
|
my @display_lines;
|
|
my ( $cxn ) = get_connections();
|
|
get_innodb_status([$cxn]);
|
|
my $innodb_status = $vars{$cxn}->{$clock};
|
|
|
|
if ( $innodb_status->{IB_fk_timestring} ) {
|
|
|
|
push @display_lines, 'Reason: ' . $innodb_status->{IB_fk_reason};
|
|
|
|
# Display FK errors caused by invalid DML.
|
|
if ( $innodb_status->{IB_fk_txn} ) {
|
|
my $txn = $innodb_status->{IB_fk_txn};
|
|
push @display_lines,
|
|
'',
|
|
"User $txn->{user} from $txn->{hostname}, thread $txn->{mysql_thread_id} was executing:",
|
|
'', no_ctrl_char($txn->{query_text});
|
|
}
|
|
|
|
my @fk_table = create_table2(
|
|
$tbl_meta{fk_error}->{visible},
|
|
meta_to_hdr('fk_error'),
|
|
extract_values($innodb_status, $innodb_status, $innodb_status, 'fk_error'),
|
|
{ just => '-', sep => ' '});
|
|
push @display_lines, '', @fk_table;
|
|
|
|
}
|
|
else {
|
|
push @display_lines, '', 'No foreign key error data.';
|
|
}
|
|
draw_screen(\@display_lines, { raw => 1 } );
|
|
}
|
|
|
|
# display_I {{{3
|
|
sub display_I {
|
|
my @display_lines;
|
|
my @cxns = get_connections();
|
|
get_innodb_status(\@cxns);
|
|
|
|
my @io_threads;
|
|
my @pending_io;
|
|
my @file_io_misc;
|
|
my @log_statistics;
|
|
my %rows_for = (
|
|
io_threads => \@io_threads,
|
|
pending_io => \@pending_io,
|
|
file_io_misc => \@file_io_misc,
|
|
log_statistics => \@log_statistics,
|
|
);
|
|
|
|
my @visible = get_visible_tables();
|
|
my %wanted = map { $_ => 1 } @visible;
|
|
|
|
foreach my $cxn ( @cxns ) {
|
|
my $set = $vars{$cxn}->{$clock};
|
|
my $pre = $vars{$cxn}->{$clock-1} || $set;
|
|
|
|
if ( $set->{IB_io_complete} ) {
|
|
if ( $wanted{io_threads} ) {
|
|
my $cur_threads = $set->{IB_io_threads};
|
|
my $pre_threads = $pre->{IB_io_threads} || $cur_threads;
|
|
foreach my $key ( sort keys %$cur_threads ) {
|
|
my $cur_thd = $cur_threads->{$key};
|
|
my $pre_thd = $pre_threads->{$key} || $cur_thd;
|
|
my $hash = extract_values($cur_thd, $cur_thd, $pre_thd, 'io_threads');
|
|
$hash->{cxn} = $cxn;
|
|
push @io_threads, $hash;
|
|
}
|
|
}
|
|
if ( $wanted{pending_io} ) {
|
|
push @pending_io, extract_values($set, $set, $pre, 'pending_io');
|
|
}
|
|
if ( $wanted{file_io_misc} ) {
|
|
push @file_io_misc, extract_values(
|
|
$config{status_inc}->{val} ? inc(0, $cxn) : $set,
|
|
$set, $pre, 'file_io_misc');
|
|
}
|
|
}
|
|
if ( $set->{IB_lg_complete} && $wanted{log_statistics} ) {
|
|
push @log_statistics, extract_values($set, $set, $pre, 'log_statistics');
|
|
}
|
|
}
|
|
|
|
my $first_table = 0;
|
|
foreach my $tbl ( @visible ) {
|
|
push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
|
|
push @display_lines, get_cxn_errors(@cxns)
|
|
if ( $config{debug}->{val} || !$first_table++ );
|
|
}
|
|
|
|
draw_screen(\@display_lines);
|
|
}
|
|
|
|
# display_L {{{3
|
|
sub display_L {
|
|
my @display_lines;
|
|
my @cxns = get_connections();
|
|
get_innodb_status(\@cxns);
|
|
|
|
my @innodb_locks;
|
|
my %rows_for = (
|
|
innodb_locks => \@innodb_locks,
|
|
);
|
|
|
|
my @visible = get_visible_tables();
|
|
my %wanted = map { $_ => 1 } @visible;
|
|
|
|
# Get info on locks
|
|
foreach my $cxn ( @cxns ) {
|
|
my $set = $vars{$cxn}->{$clock} or next;
|
|
my $pre = $vars{$cxn}->{$clock-1} || $set;
|
|
|
|
if ( $wanted{innodb_locks} && defined $set->{IB_tx_transactions} && @{$set->{IB_tx_transactions}} ) {
|
|
|
|
my $cur_txns = $set->{IB_tx_transactions};
|
|
my $pre_txns = $pre->{IB_tx_transactions} || $cur_txns;
|
|
my %cur_txns = map { $_->{mysql_thread_id} => $_ } @$cur_txns;
|
|
my %pre_txns = map { $_->{mysql_thread_id} => $_ } @$pre_txns;
|
|
foreach my $txn ( @$cur_txns ) {
|
|
foreach my $lock ( @{$txn->{locks}} ) {
|
|
my %hash = map { $_ => $txn->{$_} } qw(txn_id mysql_thread_id lock_wait_time active_secs);
|
|
map { $hash{$_} = $lock->{$_} } qw(lock_type space_id page_no n_bits index db table txn_id lock_mode special insert_intention waiting);
|
|
$hash{cxn} = $cxn;
|
|
push @innodb_locks, extract_values(\%hash, \%hash, \%hash, 'innodb_locks');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
my $first_table = 0;
|
|
foreach my $tbl ( @visible ) {
|
|
push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
|
|
push @display_lines, get_cxn_errors(@cxns)
|
|
if ( $config{debug}->{val} || !$first_table++ );
|
|
}
|
|
|
|
draw_screen(\@display_lines);
|
|
}
|
|
|
|
# display_M {{{3
|
|
sub display_M {
|
|
my @display_lines;
|
|
my @cxns = get_connections();
|
|
get_master_slave_status(@cxns);
|
|
get_status_info(@cxns);
|
|
|
|
my @slave_sql_status;
|
|
my @slave_io_status;
|
|
my @master_status;
|
|
my %rows_for = (
|
|
slave_sql_status => \@slave_sql_status,
|
|
slave_io_status => \@slave_io_status,
|
|
master_status => \@master_status,
|
|
);
|
|
|
|
my @visible = get_visible_tables();
|
|
my %wanted = map { $_ => 1 } @visible;
|
|
|
|
foreach my $cxn ( @cxns ) {
|
|
my $set = $config{status_inc}->{val} ? inc(0, $cxn) : $vars{$cxn}->{$clock};
|
|
my $pre = $vars{$cxn}->{$clock - 1} || $set;
|
|
if ( $wanted{slave_sql_status} ) {
|
|
push @slave_sql_status, extract_values($set, $set, $pre, 'slave_sql_status');
|
|
}
|
|
if ( $wanted{slave_io_status} ) {
|
|
push @slave_io_status, extract_values($set, $set, $pre, 'slave_io_status');
|
|
}
|
|
if ( $wanted{master_status} ) {
|
|
push @master_status, extract_values($set, $set, $pre, 'master_status');
|
|
}
|
|
}
|
|
|
|
my $first_table = 0;
|
|
foreach my $tbl ( @visible ) {
|
|
push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
|
|
push @display_lines, get_cxn_errors(@cxns)
|
|
if ( $config{debug}->{val} || !$first_table++ );
|
|
}
|
|
|
|
draw_screen(\@display_lines);
|
|
}
|
|
|
|
# display_O {{{3
|
|
sub display_O {
|
|
my @display_lines = ('');
|
|
my @cxns = get_connections();
|
|
my @open_tables = get_open_tables(@cxns);
|
|
my @tables = map { extract_values($_, $_, $_, 'open_tables') } @open_tables;
|
|
push @display_lines, set_to_tbl(\@tables, 'open_tables'), get_cxn_errors(@cxns);
|
|
draw_screen(\@display_lines);
|
|
}
|
|
|
|
# display_Q {{{3
|
|
sub display_Q {
|
|
my @display_lines;
|
|
|
|
my @q_header;
|
|
my @processlist;
|
|
my %rows_for = (
|
|
q_header => \@q_header,
|
|
processlist => \@processlist,
|
|
);
|
|
|
|
my @visible = $opts{n} ? 'processlist' : get_visible_tables();
|
|
my %wanted = map { $_ => 1 } @visible;
|
|
|
|
# Get the data
|
|
my @cxns = get_connections();
|
|
my @full_processlist = get_full_processlist(@cxns);
|
|
|
|
# Create header
|
|
if ( $wanted{q_header} ) {
|
|
get_status_info(@cxns);
|
|
foreach my $cxn ( @cxns ) {
|
|
my $set = $vars{$cxn}->{$clock};
|
|
my $pre = $vars{$cxn}->{$clock-1} || $set;
|
|
my $hash = extract_values($set, $set, $pre, 'q_header');
|
|
$hash->{cxn} = $cxn;
|
|
$hash->{when} = 'Total';
|
|
push @q_header, $hash;
|
|
|
|
if ( exists $vars{$cxn}->{$clock - 1} ) {
|
|
my $inc = inc(0, $cxn);
|
|
my $hash = extract_values($inc, $set, $pre, 'q_header');
|
|
$hash->{cxn} = $cxn;
|
|
$hash->{when} = 'Now';
|
|
push @q_header, $hash;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( $wanted{processlist} ) {
|
|
# TODO: save prev values
|
|
push @processlist, map { extract_values($_, $_, $_, 'processlist') } @full_processlist;
|
|
}
|
|
|
|
my $first_table = 0;
|
|
foreach my $tbl ( @visible ) {
|
|
next unless $wanted{$tbl};
|
|
push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
|
|
push @display_lines, get_cxn_errors(@cxns)
|
|
if ( $config{debug}->{val} || !$first_table++ );
|
|
}
|
|
|
|
# Save queries in global variable for analysis. The rows in %rows_for have been
|
|
# filtered, etc as a side effect of set_to_tbl(), so they are the same as the rows
|
|
# that get pushed to the screen.
|
|
@current_queries = map {
|
|
my %hash;
|
|
@hash{ qw(cxn id db query secs) } = @{$_}{ qw(cxn mysql_thread_id db info secs) };
|
|
\%hash;
|
|
} @{$rows_for{processlist}};
|
|
|
|
draw_screen(\@display_lines);
|
|
}
|
|
|
|
# display_R {{{3
|
|
sub display_R {
|
|
my @display_lines;
|
|
my @cxns = get_connections();
|
|
get_innodb_status(\@cxns);
|
|
|
|
my @row_operations;
|
|
my @row_operation_misc;
|
|
my @semaphores;
|
|
my @wait_array;
|
|
my %rows_for = (
|
|
row_operations => \@row_operations,
|
|
row_operation_misc => \@row_operation_misc,
|
|
semaphores => \@semaphores,
|
|
wait_array => \@wait_array,
|
|
);
|
|
|
|
my @visible = get_visible_tables();
|
|
my %wanted = map { $_ => 1 } @visible;
|
|
my $incvar = $config{status_inc}->{val};
|
|
|
|
foreach my $cxn ( @cxns ) {
|
|
my $set = $vars{$cxn}->{$clock};
|
|
my $pre = $vars{$cxn}->{$clock-1} || $set;
|
|
my $inc; # Only assigned to if wanted
|
|
|
|
if ( $set->{IB_ro_complete} ) {
|
|
if ( $wanted{row_operations} ) {
|
|
$inc ||= $incvar ? inc(0, $cxn) : $set;
|
|
push @row_operations, extract_values($inc, $set, $pre, 'row_operations');
|
|
}
|
|
if ( $wanted{row_operation_misc} ) {
|
|
push @row_operation_misc, extract_values($set, $set, $pre, 'row_operation_misc'),
|
|
}
|
|
}
|
|
|
|
if ( $set->{IB_sm_complete} && $wanted{semaphores} ) {
|
|
$inc ||= $incvar ? inc(0, $cxn) : $set;
|
|
push @semaphores, extract_values($inc, $set, $pre, 'semaphores');
|
|
}
|
|
|
|
if ( $set->{IB_sm_wait_array_size} && $wanted{wait_array} ) {
|
|
foreach my $wait ( @{$set->{IB_sm_waits}} ) {
|
|
my $hash = extract_values($wait, $wait, $wait, 'wait_array');
|
|
$hash->{cxn} = $cxn;
|
|
push @wait_array, $hash;
|
|
}
|
|
}
|
|
}
|
|
|
|
my $first_table = 0;
|
|
foreach my $tbl ( @visible ) {
|
|
push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
|
|
push @display_lines, get_cxn_errors(@cxns)
|
|
if ( $config{debug}->{val} || !$first_table++ );
|
|
}
|
|
|
|
draw_screen(\@display_lines);
|
|
}
|
|
|
|
# display_T {{{3
|
|
sub display_T {
|
|
my @display_lines;
|
|
|
|
my @t_header;
|
|
my @innodb_transactions;
|
|
my %rows_for = (
|
|
t_header => \@t_header,
|
|
innodb_transactions => \@innodb_transactions,
|
|
);
|
|
|
|
my @visible = $opts{n} ? 'innodb_transactions' : get_visible_tables();
|
|
my %wanted = map { $_ => 1 } @visible;
|
|
|
|
my @cxns = get_connections();
|
|
|
|
# If the header is to be shown, buffer pool data is required.
|
|
get_innodb_status( \@cxns, [ $wanted{t_header} ? qw(bp) : () ] );
|
|
|
|
foreach my $cxn ( get_connections() ) {
|
|
my $set = $vars{$cxn}->{$clock};
|
|
my $pre = $vars{$cxn}->{$clock-1} || $set;
|
|
|
|
next unless $set->{IB_tx_transactions};
|
|
|
|
if ( $wanted{t_header} ) {
|
|
my $hash = extract_values($set, $set, $pre, 't_header');
|
|
push @t_header, $hash;
|
|
}
|
|
|
|
if ( $wanted{innodb_transactions} ) {
|
|
my $cur_txns = $set->{IB_tx_transactions};
|
|
my $pre_txns = $pre->{IB_tx_transactions} || $cur_txns;
|
|
my %cur_txns = map { $_->{mysql_thread_id} => $_ } @$cur_txns;
|
|
my %pre_txns = map { $_->{mysql_thread_id} => $_ } @$pre_txns;
|
|
foreach my $thd_id ( sort keys %cur_txns ) {
|
|
my $cur_txn = $cur_txns{$thd_id};
|
|
my $pre_txn = $pre_txns{$thd_id} || $cur_txn;
|
|
my $hash = extract_values($cur_txn, $cur_txn, $pre_txn, 'innodb_transactions');
|
|
$hash->{cxn} = $cxn;
|
|
push @innodb_transactions, $hash;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
my $first_table = 0;
|
|
foreach my $tbl ( @visible ) {
|
|
push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
|
|
push @display_lines, get_cxn_errors(@cxns)
|
|
if ( $config{debug}->{val} || !$first_table++ );
|
|
}
|
|
|
|
# Save queries in global variable for analysis. The rows in %rows_for have been
|
|
# filtered, etc as a side effect of set_to_tbl(), so they are the same as the rows
|
|
# that get pushed to the screen.
|
|
@current_queries = map {
|
|
my %hash;
|
|
@hash{ qw(cxn id db query secs) } = @{$_}{ qw(cxn mysql_thread_id db query_text active_secs) };
|
|
\%hash;
|
|
} @{$rows_for{innodb_transactions}};
|
|
|
|
draw_screen(\@display_lines);
|
|
}
|
|
|
|
# display_S {{{3
|
|
sub display_S {
|
|
my $fmt = get_var_set('S_set');
|
|
my $func = $config{S_func}->{val};
|
|
my $inc = $func eq 'g' || $config{status_inc}->{val};
|
|
|
|
# The table's meta-data is generated from the compiled var_set.
|
|
my ( $cols, $visible );
|
|
if ( $tbl_meta{var_status}->{fmt} && $fmt eq $tbl_meta{var_status}->{fmt} ) {
|
|
( $cols, $visible ) = @{$tbl_meta{var_status}}{qw(cols visible)};
|
|
}
|
|
else {
|
|
( $cols, $visible ) = compile_select_stmt($fmt);
|
|
|
|
# Apply missing values to columns. Always apply averages across all connections.
|
|
map {
|
|
$_->{agg} = 'avg';
|
|
$_->{label} = $_->{hdr};
|
|
} values %$cols;
|
|
|
|
$tbl_meta{var_status}->{cols} = $cols;
|
|
$tbl_meta{var_status}->{visible} = $visible;
|
|
$tbl_meta{var_status}->{fmt} = $fmt;
|
|
map { $tbl_meta{var_status}->{cols}->{$_}->{just} = ''} @$visible;
|
|
}
|
|
|
|
my @var_status;
|
|
my %rows_for = (
|
|
var_status => \@var_status,
|
|
);
|
|
|
|
my @visible = get_visible_tables();
|
|
my %wanted = map { $_ => 1 } @visible;
|
|
my @cxns = get_connections();
|
|
|
|
get_status_info(@cxns);
|
|
get_innodb_status(\@cxns);
|
|
|
|
# Set up whether to pivot and how many sets to extract.
|
|
$tbl_meta{var_status}->{pivot} = $func eq 'v';
|
|
|
|
my $num_sets
|
|
= $func eq 'v'
|
|
? $config{num_status_sets}->{val}
|
|
: 0;
|
|
foreach my $set ( 0 .. $num_sets ) {
|
|
my @rows;
|
|
foreach my $cxn ( @cxns ) {
|
|
my $vars = $inc ? inc($set, $cxn) : $vars{$cxn}->{$clock - $set};
|
|
my $cur = $vars{$cxn}->{$clock-$set};
|
|
my $pre = $vars{$cxn}->{$clock-$set-1} || $cur;
|
|
next unless $vars && %$vars;
|
|
my $hash = extract_values($vars, $cur, $pre, 'var_status');
|
|
push @rows, $hash;
|
|
}
|
|
@rows = apply_group_by('var_status', [], @rows);
|
|
push @var_status, @rows;
|
|
}
|
|
|
|
# Recompile the sort func. TODO: avoid recompiling at every refresh.
|
|
# Figure out whether the data is all numeric and decide on a sort type.
|
|
# my $cmp
|
|
# = scalar(
|
|
# grep { !defined $_ || $_ !~ m/^\d+$/ }
|
|
# map { my $col = $_; map { $_->{$col} } @var_status }
|
|
# $tbl_meta{var_status}->{sort_cols} =~ m/(\w+)/g)
|
|
# ? 'cmp'
|
|
# : '<=>';
|
|
$tbl_meta{var_status}->{sort_func} = make_sort_func($tbl_meta{var_status});
|
|
|
|
# ################################################################
|
|
# Now there is specific display code based on $config{S_func}
|
|
# ################################################################
|
|
if ( $func =~ m/s|g/ ) {
|
|
my $min_width = 4;
|
|
|
|
# Clear the screen if the display width changed.
|
|
if ( @last_term_size && $this_term_size[0] != $last_term_size[0] ) {
|
|
$lines_printed = 0;
|
|
$clear_screen_sub->();
|
|
}
|
|
|
|
if ( $func eq 's' ) {
|
|
# Decide how wide columns should be.
|
|
my $num_cols = scalar(@$visible);
|
|
my $width = $opts{n} ? 0 : max($min_width, int(($this_term_size[0] - $num_cols + 1) / $num_cols));
|
|
my $g_format = $opts{n} ? ( "%s\t" x $num_cols ) : ( "%-${width}s " x $num_cols );
|
|
|
|
# Print headers every now and then. Headers can get really long, so compact them.
|
|
my @hdr = @$visible;
|
|
if ( $opts{n} ) {
|
|
if ( $lines_printed == 0 ) {
|
|
print join("\t", @hdr), "\n";
|
|
$lines_printed++;
|
|
}
|
|
}
|
|
elsif ( $lines_printed == 0 || $lines_printed > $this_term_size[1] - 2 ) {
|
|
@hdr = map { donut(crunch($_, $width), $width) } @hdr;
|
|
print join(' ', map { sprintf( "%${width}s", donut($_, $width)) } @hdr) . "\n";
|
|
$lines_printed = 1;
|
|
}
|
|
|
|
# Design a column format for the values.
|
|
my $format
|
|
= $opts{n}
|
|
? join("\t", map { '%s' } @$visible) . "\n"
|
|
: join(' ', map { "%${width}s" } @hdr) . "\n";
|
|
|
|
foreach my $row ( @var_status ) {
|
|
printf($format, map { defined $_ ? $_ : '' } @{$row}{ @$visible });
|
|
$lines_printed++;
|
|
}
|
|
}
|
|
else { # 'g' mode
|
|
# Design a column format for the values.
|
|
my $num_cols = scalar(@$visible);
|
|
my $width = $opts{n} ? 0 : int(($this_term_size[0] - $num_cols + 1) / $num_cols);
|
|
my $format = $opts{n} ? ( "%s\t" x $num_cols ) : ( "%-${width}s " x $num_cols );
|
|
$format =~ s/\s$/\n/;
|
|
|
|
# Print headers every now and then.
|
|
if ( $opts{n} ) {
|
|
if ( $lines_printed == 0 ) {
|
|
print join("\t", @$visible), "\n";
|
|
print join("\t", map { shorten($mvs{$_}) } @$visible), "\n";
|
|
}
|
|
}
|
|
elsif ( $lines_printed == 0 || $lines_printed > $this_term_size[1] - 2 ) {
|
|
printf($format, map { donut(crunch($_, $width), $width) } @$visible);
|
|
printf($format, map { shorten($mvs{$_} || 0) } @$visible);
|
|
$lines_printed = 2;
|
|
}
|
|
|
|
# Update the max ever seen, and scale by the max ever seen.
|
|
my $set = $var_status[0];
|
|
foreach my $col ( @$visible ) {
|
|
$set->{$col} = 1 unless defined $set->{$col} && $set->{$col} =~ m/$num_regex/;
|
|
$set->{$col} = ($set->{$col} || 1) / ($set->{Uptime_hires} || 1);
|
|
$mvs{$col} = max($mvs{$col} || 1, $set->{$col});
|
|
$set->{$col} /= $mvs{$col};
|
|
}
|
|
printf($format, map { ( $config{graph_char}->{val} x int( $width * $set->{$_} )) || '.' } @$visible );
|
|
$lines_printed++;
|
|
|
|
}
|
|
}
|
|
else { # 'v'
|
|
my $first_table = 0;
|
|
my @display_lines;
|
|
foreach my $tbl ( @visible ) {
|
|
push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
|
|
push @display_lines, get_cxn_errors(@cxns)
|
|
if ( $config{debug}->{val} || !$first_table++ );
|
|
}
|
|
$clear_screen_sub->();
|
|
draw_screen( \@display_lines );
|
|
}
|
|
}
|
|
|
|
# display_explain {{{3
|
|
sub display_explain {
|
|
my $info = shift;
|
|
my $cxn = $info->{cxn};
|
|
my $db = $info->{db};
|
|
|
|
my ( $mods, $query ) = rewrite_for_explain($info->{query});
|
|
|
|
my @display_lines;
|
|
|
|
if ( $query ) {
|
|
|
|
my $part = version_ge($dbhs{$cxn}->{dbh}, '5.1.5') ? 'PARTITIONS' : '';
|
|
$query = "EXPLAIN $part\n" . $query;
|
|
|
|
eval {
|
|
if ( $db ) {
|
|
do_query($cxn, "use $db");
|
|
}
|
|
my $sth = do_query($cxn, $query);
|
|
|
|
my $res;
|
|
while ( $res = $sth->fetchrow_hashref() ) {
|
|
map { $res->{$_} ||= '' } ( 'partitions', keys %$res);
|
|
my @this_table = create_caption("Sub-Part $res->{id}",
|
|
create_table2(
|
|
$tbl_meta{explain}->{visible},
|
|
meta_to_hdr('explain'),
|
|
extract_values($res, $res, $res, 'explain')));
|
|
@display_lines = stack_next(\@display_lines, \@this_table, { pad => ' ', vsep => 2 });
|
|
}
|
|
};
|
|
|
|
if ( $EVAL_ERROR ) {
|
|
push @display_lines,
|
|
'',
|
|
"The query could not be explained. Only SELECT queries can be "
|
|
. "explained; innotop tries to rewrite certain REPLACE and INSERT queries "
|
|
. "into SELECT, but this doesn't always succeed.";
|
|
}
|
|
|
|
}
|
|
else {
|
|
push @display_lines, '', 'The query could not be explained.';
|
|
}
|
|
|
|
if ( $mods ) {
|
|
push @display_lines, '', '[This query has been re-written to be explainable]';
|
|
}
|
|
|
|
unshift @display_lines, no_ctrl_char($query);
|
|
draw_screen(\@display_lines, { raw => 1 } );
|
|
}
|
|
|
|
# rewrite_for_explain {{{3
|
|
sub rewrite_for_explain {
|
|
my $query = shift;
|
|
|
|
my $mods = 0;
|
|
my $orig = $query;
|
|
$mods += $query =~ s/^\s*(?:replace|insert).*?select/select/is;
|
|
$mods += $query =~ s/^
|
|
\s*create\s+(?:temporary\s+)?table
|
|
\s+(?:\S+\s+)as\s+select/select/xis;
|
|
$mods += $query =~ s/\s+on\s+duplicate\s+key\s+update.*$//is;
|
|
return ( $mods, $query );
|
|
}
|
|
|
|
# show_optimized_query {{{3
|
|
sub show_optimized_query {
|
|
my $info = shift;
|
|
my $cxn = $info->{cxn};
|
|
my $db = $info->{db};
|
|
my $meta = $dbhs{$cxn};
|
|
|
|
my @display_lines;
|
|
|
|
my ( $mods, $query ) = rewrite_for_explain($info->{query});
|
|
|
|
if ( $mods ) {
|
|
push @display_lines, '[This query has been re-written to be explainable]';
|
|
}
|
|
|
|
if ( $query ) {
|
|
push @display_lines, no_ctrl_char($info->{query});
|
|
|
|
eval {
|
|
if ( $db ) {
|
|
do_query($cxn, "use $db");
|
|
}
|
|
do_query( $cxn, 'EXPLAIN EXTENDED ' . $query ) or die "Can't explain query";
|
|
my $sth = do_query($cxn, 'SHOW WARNINGS');
|
|
my $res = $sth->fetchall_arrayref({});
|
|
|
|
if ( $res ) {
|
|
foreach my $result ( @$res ) {
|
|
push @display_lines, 'Note:', no_ctrl_char($result->{message});
|
|
}
|
|
}
|
|
else {
|
|
push @display_lines, '', 'The query optimization could not be generated.';
|
|
}
|
|
};
|
|
|
|
if ( $EVAL_ERROR ) {
|
|
push @display_lines, '', "The optimization could not be generated: $EVAL_ERROR";
|
|
}
|
|
|
|
}
|
|
else {
|
|
push @display_lines, '', 'The query optimization could not be generated.';
|
|
}
|
|
|
|
draw_screen(\@display_lines, { raw => 1 } );
|
|
}
|
|
|
|
# display_help {{{3
|
|
sub display_help {
|
|
my $mode = $config{mode}->{val};
|
|
|
|
# Get globally mapped keys, then overwrite them with mode-specific ones.
|
|
my %keys = map {
|
|
$_ => $action_for{$_}->{label}
|
|
} keys %action_for;
|
|
foreach my $key ( keys %{$modes{$mode}->{action_for}} ) {
|
|
$keys{$key} = $modes{$mode}->{action_for}->{$key}->{label};
|
|
}
|
|
delete $keys{'?'};
|
|
|
|
# Split them into three kinds of keys: MODE keys, action keys, and
|
|
# magic (special character) keys.
|
|
my @modes = sort grep { m/[A-Z]/ } keys %keys;
|
|
my @actions = sort grep { m/[a-z]/ } keys %keys;
|
|
my @magic = sort grep { m/[^A-Z]/i } keys %keys;
|
|
|
|
my @display_lines = ( '', 'Switch to a different mode:' );
|
|
|
|
# Mode keys
|
|
my @all_modes = map { "$_ $modes{$_}->{hdr}" } @modes;
|
|
my @col1 = splice(@all_modes, 0, ceil(@all_modes/3));
|
|
my @col2 = splice(@all_modes, 0, ceil(@all_modes/2));
|
|
my $max1 = max(map {length($_)} @col1);
|
|
my $max2 = max(map {length($_)} @col2);
|
|
while ( @col1 ) {
|
|
push @display_lines, sprintf(" %-${max1}s %-${max2}s %s",
|
|
(shift @col1 || ''),
|
|
(shift @col2 || ''),
|
|
(shift @all_modes || ''));
|
|
}
|
|
|
|
# Action keys
|
|
my @all_actions = map { "$_ $keys{$_}" } @actions;
|
|
@col1 = splice(@all_actions, 0, ceil(@all_actions/2));
|
|
$max1 = max(map {length($_)} @col1);
|
|
push @display_lines, '', 'Actions:';
|
|
while ( @col1 ) {
|
|
push @display_lines, sprintf(" %-${max1}s %s",
|
|
(shift @col1 || ''),
|
|
(shift @all_actions || ''));
|
|
}
|
|
|
|
# Magic keys
|
|
my @all_magic = map { sprintf('%4s', $action_for{$_}->{key} || $_) . " $keys{$_}" } @magic;
|
|
@col1 = splice(@all_magic, 0, ceil(@all_magic/2));
|
|
$max1 = max(map {length($_)} @col1);
|
|
push @display_lines, '', 'Other:';
|
|
while ( @col1 ) {
|
|
push @display_lines, sprintf("%-${max1}s%s",
|
|
(shift @col1 || ''),
|
|
(shift @all_magic || ''));
|
|
}
|
|
|
|
$clear_screen_sub->();
|
|
draw_screen(\@display_lines, { show_all => 1 } );
|
|
pause();
|
|
$clear_screen_sub->();
|
|
}
|
|
|
|
# show_full_query {{{3
|
|
sub show_full_query {
|
|
my $info = shift;
|
|
my @display_lines = no_ctrl_char($info->{query});
|
|
draw_screen(\@display_lines, { raw => 1 });
|
|
}
|
|
|
|
# Formatting functions {{{2
|
|
|
|
# create_table2 {{{3
|
|
# Makes a two-column table, labels on left, data on right.
|
|
# Takes refs of @cols, %labels and %data, %user_prefs
|
|
sub create_table2 {
|
|
my ( $cols, $labels, $data, $user_prefs ) = @_;
|
|
my @rows;
|
|
|
|
if ( @$cols && %$data ) {
|
|
|
|
# Override defaults
|
|
my $p = {
|
|
just => '',
|
|
sep => ':',
|
|
just1 => '-',
|
|
};
|
|
if ( $user_prefs ) {
|
|
map { $p->{$_} = $user_prefs->{$_} } keys %$user_prefs;
|
|
}
|
|
|
|
# Fix undef values
|
|
map { $data->{$_} = '' unless defined $data->{$_} } @$cols;
|
|
|
|
# Format the table
|
|
my $max_l = max(map{ length($labels->{$_}) } @$cols);
|
|
my $max_v = max(map{ length($data->{$_}) } @$cols);
|
|
my $format = "%$p->{just}${max_l}s$p->{sep} %$p->{just1}${max_v}s";
|
|
foreach my $col ( @$cols ) {
|
|
push @rows, sprintf($format, $labels->{$col}, $data->{$col});
|
|
}
|
|
}
|
|
return @rows;
|
|
}
|
|
|
|
# stack_next {{{3
|
|
# Stacks one display section next to the other. Accepts left-hand arrayref,
|
|
# right-hand arrayref, and options hashref. Tries to stack as high as
|
|
# possible, so
|
|
# aaaaaa
|
|
# bbb
|
|
# can stack ccc next to the bbb.
|
|
# NOTE: this DOES modify its arguments, even though it returns a new array.
|
|
sub stack_next {
|
|
my ( $left, $right, $user_prefs ) = @_;
|
|
my @result;
|
|
|
|
my $p = {
|
|
pad => ' ',
|
|
vsep => 0,
|
|
};
|
|
if ( $user_prefs ) {
|
|
map { $p->{$_} = $user_prefs->{$_} } keys %$user_prefs;
|
|
}
|
|
|
|
# Find out how wide the LHS can be and still let the RHS fit next to it.
|
|
my $pad = $p->{pad};
|
|
my $max_r = max( map { length($_) } @$right) || 0;
|
|
my $max_l = $this_term_size[0] - $max_r - length($pad);
|
|
|
|
# Find the minimum row on the LHS that the RHS will fit next to.
|
|
my $i = scalar(@$left) - 1;
|
|
while ( $i >= 0 && length($left->[$i]) <= $max_l ) {
|
|
$i--;
|
|
}
|
|
$i++;
|
|
my $offset = $i;
|
|
|
|
if ( $i < scalar(@$left) ) {
|
|
# Find the max width of the section of the LHS against which the RHS
|
|
# will sit.
|
|
my $max_i_in_common = min($i + scalar(@$right) - 1, scalar(@$left) - 1);
|
|
my $max_width = max( map { length($_) } @{$left}[$i..$max_i_in_common]);
|
|
|
|
# Append the RHS onto the LHS until one runs out.
|
|
while ( $i < @$left && $i - $offset < @$right ) {
|
|
my $format = "%-${max_width}s$pad%${max_r}s";
|
|
$left->[$i] = sprintf($format, $left->[$i], $right->[$i - $offset]);
|
|
$i++;
|
|
}
|
|
while ( $i - $offset < @$right ) {
|
|
# There is more RHS to push on the end of the array
|
|
push @$left,
|
|
sprintf("%${max_width}s$pad%${max_r}s", ' ', $right->[$i - $offset]);
|
|
$i++;
|
|
}
|
|
push @result, @$left;
|
|
}
|
|
else {
|
|
# There is no room to put them side by side. Add them below, with
|
|
# a blank line above them if specified.
|
|
push @result, @$left;
|
|
push @result, (' ' x $this_term_size[0]) if $p->{vsep} && @$left;
|
|
push @result, @$right;
|
|
}
|
|
return @result;
|
|
}
|
|
|
|
# create_caption {{{3
|
|
sub create_caption {
|
|
my ( $caption, @rows ) = @_;
|
|
if ( @rows ) {
|
|
|
|
# Calculate the width of what will be displayed, so it can be centered
|
|
# in that space. When the thing is wider than the display, center the
|
|
# caption in the display.
|
|
my $width = min($this_term_size[0], max(map { length(ref($_) ? $_->[0] : $_) } @rows));
|
|
|
|
my $cap_len = length($caption);
|
|
|
|
# It may be narrow enough to pad the sides with underscores and save a
|
|
# line on the screen.
|
|
if ( $cap_len <= $width - 6 ) {
|
|
my $left = int(($width - 2 - $cap_len) / 2);
|
|
unshift @rows,
|
|
("_" x $left) . " $caption " . ("_" x ($width - $left - $cap_len - 2));
|
|
}
|
|
|
|
# The caption is too wide to add underscores on each side.
|
|
else {
|
|
|
|
# Color is supported, so we can use terminal underlining.
|
|
if ( $config{color}->{val} ) {
|
|
my $left = int(($width - $cap_len) / 2);
|
|
unshift @rows, [
|
|
(" " x $left) . $caption . (" " x ($width - $left - $cap_len)),
|
|
'underline',
|
|
];
|
|
}
|
|
|
|
# Color is not supported, so we have to add a line underneath to separate the
|
|
# caption from whatever it's captioning.
|
|
else {
|
|
my $left = int(($width - $cap_len) / 2);
|
|
unshift @rows, ('-' x $width);
|
|
unshift @rows, (" " x $left) . $caption . (" " x ($width - $left - $cap_len));
|
|
}
|
|
|
|
# The caption is wider than the thing it labels, so we have to pad the
|
|
# thing it labels to a consistent width.
|
|
if ( $cap_len > $width ) {
|
|
@rows = map {
|
|
ref($_)
|
|
? [ sprintf('%-' . $cap_len . 's', $_->[0]), $_->[1] ]
|
|
: sprintf('%-' . $cap_len . 's', $_);
|
|
} @rows;
|
|
}
|
|
|
|
}
|
|
}
|
|
return @rows;
|
|
}
|
|
|
|
# create_table {{{3
|
|
# Input: an arrayref of columns, hashref of col info, and an arrayref of hashes
|
|
# Example: [ 'a', 'b' ]
|
|
# { a => spec, b => spec }
|
|
# [ { a => 1, b => 2}, { a => 3, b => 4 } ]
|
|
# The 'spec' is a hashref of hdr => label, just => ('-' or ''). It also supports min and max-widths
|
|
# vi the minw and maxw params.
|
|
# Output: an array of strings, one per row.
|
|
# Example:
|
|
# Column One Column Two
|
|
# ---------- ----------
|
|
# 1 2
|
|
# 3 4
|
|
sub create_table {
|
|
my ( $cols, $info, $data, $prefs ) = @_;
|
|
$prefs ||= {};
|
|
$prefs->{no_hdr} ||= ($opts{n} && $clock != 1);
|
|
|
|
# Truncate rows that will surely be off screen even if this is the only table.
|
|
if ( !$opts{n} && !$prefs->{raw} && !$prefs->{show_all} && $this_term_size[1] < @$data-1 ) {
|
|
$data = [ @$data[0..$this_term_size[1] - 1] ];
|
|
}
|
|
|
|
my @rows = ();
|
|
|
|
if ( @$cols && %$info ) {
|
|
|
|
# Fix undef values, collapse whitespace.
|
|
foreach my $row ( @$data ) {
|
|
map { $row->{$_} = collapse_ws($row->{$_}) } @$cols;
|
|
}
|
|
|
|
my $col_sep = $opts{n} ? "\t" : ' ';
|
|
|
|
# Find each column's max width.
|
|
my %width_for;
|
|
if ( !$opts{n} ) {
|
|
%width_for = map {
|
|
my $col_name = $_;
|
|
if ( $info->{$_}->{dec} ) {
|
|
# Align along the decimal point
|
|
my $max_rodp = max(0, map { $_->{$col_name} =~ m/([^\s\d-].*)$/ ? length($1) : 0 } @$data);
|
|
foreach my $row ( @$data ) {
|
|
my $col = $row->{$col_name};
|
|
my ( $l, $r ) = $col =~ m/^([\s\d]*)(.*)$/;
|
|
$row->{$col_name} = sprintf("%s%-${max_rodp}s", $l, $r);
|
|
}
|
|
}
|
|
my $max_width = max( length($info->{$_}->{hdr}), map { length($_->{$col_name}) } @$data);
|
|
if ( $info->{$col_name}->{maxw} ) {
|
|
$max_width = min( $max_width, $info->{$col_name}->{maxw} );
|
|
}
|
|
if ( $info->{$col_name}->{minw} ) {
|
|
$max_width = max( $max_width, $info->{$col_name}->{minw} );
|
|
}
|
|
$col_name => $max_width;
|
|
} @$cols;
|
|
}
|
|
|
|
# The table header.
|
|
if ( !$config{hide_hdr}->{val} && !$prefs->{no_hdr} ) {
|
|
push @rows, $opts{n}
|
|
? join( $col_sep, @$cols )
|
|
: join( $col_sep, map { sprintf( "%-$width_for{$_}s", trunc($info->{$_}->{hdr}, $width_for{$_}) ) } @$cols );
|
|
if ( $config{color}->{val} && $config{header_highlight}->{val} ) {
|
|
push @rows, [ pop @rows, $config{header_highlight}->{val} ];
|
|
}
|
|
elsif ( !$opts{n} ) {
|
|
push @rows, join( $col_sep, map { "-" x $width_for{$_} } @$cols );
|
|
}
|
|
}
|
|
|
|
# The table data.
|
|
if ( $opts{n} ) {
|
|
foreach my $item ( @$data ) {
|
|
push @rows, join($col_sep, map { $item->{$_} } @$cols );
|
|
}
|
|
}
|
|
else {
|
|
my $format = join( $col_sep,
|
|
map { "%$info->{$_}->{just}$width_for{$_}s" } @$cols );
|
|
foreach my $item ( @$data ) {
|
|
my $row = sprintf($format, map { trunc($item->{$_}, $width_for{$_}) } @$cols );
|
|
if ( $config{color}->{val} && $item->{_color} ) {
|
|
push @rows, [ $row, $item->{_color} ];
|
|
}
|
|
else {
|
|
push @rows, $row;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return @rows;
|
|
}
|
|
|
|
# Aggregates a table. If $group_by is an arrayref of columns, the grouping key
|
|
# is the specified columns; otherwise it's just the empty string (e.g.
|
|
# everything is grouped as one group).
|
|
sub apply_group_by {
|
|
my ( $tbl, $group_by, @rows ) = @_;
|
|
my $meta = $tbl_meta{$tbl};
|
|
my %is_group = map { $_ => 1 } @$group_by;
|
|
my @non_grp = grep { !$is_group{$_} } keys %{$meta->{cols}};
|
|
|
|
my %temp_table;
|
|
foreach my $row ( @rows ) {
|
|
my $group_key
|
|
= @$group_by
|
|
? '{' . join('}{', map { defined $_ ? $_ : '' } @{$row}{@$group_by}) . '}'
|
|
: '';
|
|
$temp_table{$group_key} ||= [];
|
|
push @{$temp_table{$group_key}}, $row;
|
|
}
|
|
|
|
# Crush the rows together...
|
|
my @new_rows;
|
|
foreach my $key ( sort keys %temp_table ) {
|
|
my $group = $temp_table{$key};
|
|
my %new_row;
|
|
@new_row{@$group_by} = @{$group->[0]}{@$group_by};
|
|
foreach my $col ( @non_grp ) {
|
|
my $agg = $meta->{cols}->{$col}->{agg} || 'first';
|
|
$new_row{$col} = $agg_funcs{$agg}->( map { $_->{$col} } @$group );
|
|
}
|
|
push @new_rows, \%new_row;
|
|
}
|
|
return @new_rows;
|
|
}
|
|
|
|
# set_to_tbl {{{3
|
|
# Unifies all the work of filtering, sorting etc. Alters the input.
|
|
# TODO: pull all the little pieces out into subroutines and stick events in each of them.
|
|
sub set_to_tbl {
|
|
my ( $rows, $tbl ) = @_;
|
|
my $meta = $tbl_meta{$tbl} or die "No such table $tbl in tbl_meta";
|
|
|
|
# don't show cxn if there's only one connection being displayed
|
|
my @visible;
|
|
if (scalar @{$modes{$config{mode}->{val}}->{connections}} == 1) {
|
|
map { push @visible, $_ if $_ !~ /^cxn$/ } @{$meta->{visible}};
|
|
delete $$rows[0]{cxn} if defined $$rows[0]{cxn};
|
|
}
|
|
else {
|
|
@visible = @{$meta->{visible}};
|
|
}
|
|
|
|
if ( !$meta->{pivot} ) {
|
|
|
|
# Hook in event listeners
|
|
foreach my $listener ( @{$event_listener_for{set_to_tbl_pre_filter}} ) {
|
|
$listener->set_to_tbl_pre_filter($rows, $tbl);
|
|
}
|
|
|
|
# Apply filters. Note that if the table is pivoted, filtering and sorting
|
|
# are applied later.
|
|
foreach my $filter ( @{$meta->{filters}} ) {
|
|
eval {
|
|
@$rows = grep { $filters{$filter}->{func}->($_) } @$rows;
|
|
};
|
|
if ( $EVAL_ERROR && $config{debug}->{val} ) {
|
|
die $EVAL_ERROR;
|
|
}
|
|
}
|
|
|
|
foreach my $listener ( @{$event_listener_for{set_to_tbl_pre_sort}} ) {
|
|
$listener->set_to_tbl_pre_sort($rows, $tbl);
|
|
}
|
|
|
|
# Sort. Note that if the table is pivoted, sorting might have the wrong
|
|
# columns and it could crash. This will only be an issue if it's possible
|
|
# to toggle pivoting on and off, which it's not at the moment.
|
|
if ( @$rows && $meta->{sort_func} && !$meta->{aggregate} ) {
|
|
if ( $meta->{sort_dir} > 0 ) {
|
|
@$rows = $meta->{sort_func}->( @$rows );
|
|
}
|
|
else {
|
|
@$rows = reverse $meta->{sort_func}->( @$rows );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
# Stop altering arguments now.
|
|
my @rows = @$rows;
|
|
|
|
foreach my $listener ( @{$event_listener_for{set_to_tbl_pre_group}} ) {
|
|
$listener->set_to_tbl_pre_group(\@rows, $tbl);
|
|
}
|
|
|
|
# Apply group-by.
|
|
if ( $meta->{aggregate} ) {
|
|
@rows = apply_group_by($tbl, $meta->{group_by}, @rows);
|
|
|
|
# Sort. Note that if the table is pivoted, sorting might have the wrong
|
|
# columns and it could crash. This will only be an issue if it's possible
|
|
# to toggle pivoting on and off, which it's not at the moment.
|
|
if ( @rows && $meta->{sort_func} ) {
|
|
if ( $meta->{sort_dir} > 0 ) {
|
|
@rows = $meta->{sort_func}->( @rows );
|
|
}
|
|
else {
|
|
@rows = reverse $meta->{sort_func}->( @rows );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
foreach my $listener ( @{$event_listener_for{set_to_tbl_pre_colorize}} ) {
|
|
$listener->set_to_tbl_pre_colorize(\@rows, $tbl);
|
|
}
|
|
|
|
if ( !$meta->{pivot} ) {
|
|
# Colorize. Adds a _color column to rows.
|
|
if ( @rows && $meta->{color_func} ) {
|
|
eval {
|
|
foreach my $row ( @rows ) {
|
|
$row->{_color} = $meta->{color_func}->($row);
|
|
}
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
pause($EVAL_ERROR);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach my $listener ( @{$event_listener_for{set_to_tbl_pre_transform}} ) {
|
|
$listener->set_to_tbl_pre_transform(\@rows, $tbl);
|
|
}
|
|
|
|
# Apply_transformations.
|
|
if ( @rows ) {
|
|
my $cols = $meta->{cols};
|
|
foreach my $col ( keys %{$rows->[0]} ) {
|
|
# Don't auto-vivify $tbl_meta{tbl}-{cols}->{_color}->{trans}
|
|
next if $col eq '_color';
|
|
foreach my $trans ( @{$cols->{$col}->{trans}} ) {
|
|
map { $_->{$col} = $trans_funcs{$trans}->($_->{$col}) } @rows;
|
|
}
|
|
}
|
|
}
|
|
|
|
my ($fmt_cols, $fmt_meta);
|
|
|
|
# Pivot.
|
|
if ( $meta->{pivot} ) {
|
|
|
|
foreach my $listener ( @{$event_listener_for{set_to_tbl_pre_pivot}} ) {
|
|
$listener->set_to_tbl_pre_pivot(\@rows, $tbl);
|
|
}
|
|
|
|
my @vars = @{$meta->{visible}};
|
|
my @tmp = map { { name => $_ } } @vars;
|
|
my @cols = 'name';
|
|
foreach my $i ( 0..@$rows-1 ) {
|
|
my $col = "set_$i";
|
|
push @cols, $col;
|
|
foreach my $j ( 0..@vars-1 ) {
|
|
$tmp[$j]->{$col} = $rows[$i]->{$vars[$j]};
|
|
}
|
|
}
|
|
$fmt_meta = { map { $_ => { hdr => $_, just => '-' } } @cols };
|
|
$fmt_cols = \@cols;
|
|
@rows = @tmp;
|
|
|
|
# Hook in event listeners
|
|
foreach my $listener ( @{$event_listener_for{set_to_tbl_pre_filter}} ) {
|
|
$listener->set_to_tbl_pre_filter($rows, $tbl);
|
|
}
|
|
|
|
# Apply filters.
|
|
foreach my $filter ( @{$meta->{filters}} ) {
|
|
eval {
|
|
@rows = grep { $filters{$filter}->{func}->($_) } @rows;
|
|
};
|
|
if ( $EVAL_ERROR && $config{debug}->{val} ) {
|
|
die $EVAL_ERROR;
|
|
}
|
|
}
|
|
|
|
foreach my $listener ( @{$event_listener_for{set_to_tbl_pre_sort}} ) {
|
|
$listener->set_to_tbl_pre_sort($rows, $tbl);
|
|
}
|
|
|
|
# Sort.
|
|
if ( @rows && $meta->{sort_func} ) {
|
|
if ( $meta->{sort_dir} > 0 ) {
|
|
@rows = $meta->{sort_func}->( @rows );
|
|
}
|
|
else {
|
|
@rows = reverse $meta->{sort_func}->( @rows );
|
|
}
|
|
}
|
|
|
|
}
|
|
else {
|
|
# If the table isn't pivoted, just show all columns that are supposed to
|
|
# be shown; but eliminate aggonly columns if the table isn't aggregated.
|
|
my $aggregated = $meta->{aggregate};
|
|
$fmt_cols = [ grep { $aggregated || !$meta->{cols}->{$_}->{aggonly} } @visible ];
|
|
$fmt_meta = { map { $_ => $meta->{cols}->{$_} } @$fmt_cols };
|
|
|
|
# If the table is aggregated, re-order the group_by columns to the left of
|
|
# the display.
|
|
if ( $aggregated ) {
|
|
my %is_group = map { $_ => 1 } @{$meta->{group_by}};
|
|
$fmt_cols = [ @{$meta->{group_by}}, grep { !$is_group{$_} } @$fmt_cols ];
|
|
}
|
|
}
|
|
|
|
foreach my $listener ( @{$event_listener_for{set_to_tbl_pre_create}} ) {
|
|
$listener->set_to_tbl_pre_create(\@rows, $tbl);
|
|
}
|
|
|
|
@rows = create_table( $fmt_cols, $fmt_meta, \@rows);
|
|
if ( !$meta->{hide_caption} && !$opts{n} && $config{display_table_captions}->{val} ) {
|
|
@rows = create_caption($meta->{capt}, @rows)
|
|
}
|
|
|
|
foreach my $listener ( @{$event_listener_for{set_to_tbl_post_create}} ) {
|
|
$listener->set_to_tbl_post_create(\@rows, $tbl);
|
|
}
|
|
|
|
return @rows;
|
|
}
|
|
|
|
# meta_to_hdr {{{3
|
|
sub meta_to_hdr {
|
|
my $tbl = shift;
|
|
my $meta = $tbl_meta{$tbl};
|
|
my %labels = map { $_ => $meta->{cols}->{$_}->{hdr} } @{$meta->{visible}};
|
|
return \%labels;
|
|
}
|
|
|
|
# commify {{{3
|
|
# From perlfaq5: add commas.
|
|
sub commify {
|
|
my ( $num ) = @_;
|
|
$num = 0 unless defined $num;
|
|
$num =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g;
|
|
return $num;
|
|
}
|
|
|
|
# set_precision {{{3
|
|
# Trim to desired precision.
|
|
sub set_precision {
|
|
my ( $num, $precision ) = @_;
|
|
$precision = $config{num_digits}->{val} if !defined $precision;
|
|
sprintf("%.${precision}f", $num);
|
|
}
|
|
|
|
# percent {{{3
|
|
# Convert to percent
|
|
sub percent {
|
|
my ( $num ) = @_;
|
|
$num = 0 unless defined $num;
|
|
my $digits = $config{num_digits}->{val};
|
|
return sprintf("%.${digits}f", $num * 100)
|
|
. ($config{show_percent}->{val} ? '%' : '');
|
|
}
|
|
|
|
# shorten {{{3
|
|
sub shorten {
|
|
my ( $num, $opts ) = @_;
|
|
|
|
return $num if !defined($num) || $opts{n} || $num !~ m/$num_regex/;
|
|
|
|
$opts ||= {};
|
|
my $pad = defined $opts->{pad} ? $opts->{pad} : '';
|
|
my $num_digits = defined $opts->{num_digits}
|
|
? $opts->{num_digits}
|
|
: $config{num_digits}->{val};
|
|
my $force = defined $opts->{force};
|
|
|
|
my $n = 0;
|
|
while ( $num >= 1_024 ) {
|
|
$num /= 1_024;
|
|
++$n;
|
|
}
|
|
return sprintf(
|
|
$num =~ m/\./ || $n || $force
|
|
? "%.${num_digits}f%s"
|
|
: '%d',
|
|
$num, ($pad,'k','M','G', 'T')[$n]);
|
|
|
|
}
|
|
|
|
# Utility functions {{{2
|
|
# unique {{{3
|
|
sub unique {
|
|
my %seen;
|
|
return grep { !$seen{$_}++ } @_;
|
|
}
|
|
|
|
# make_color_func {{{3
|
|
sub make_color_func {
|
|
my ( $tbl ) = @_;
|
|
my @criteria;
|
|
foreach my $spec ( @{$tbl->{colors}} ) {
|
|
next unless exists $comp_ops{$spec->{op}};
|
|
my $val = $spec->{op} =~ m/^(?:eq|ne|le|ge|lt|gt)$/ ? "'$spec->{arg}'"
|
|
: $spec->{op} =~ m/^(?:=~|!~)$/ ? "m/" . quotemeta($spec->{arg}) . "/"
|
|
: $spec->{arg};
|
|
push @criteria,
|
|
"( defined \$set->{$spec->{col}} && \$set->{$spec->{col}} $spec->{op} $val ) { return '$spec->{color}'; }";
|
|
}
|
|
return undef unless @criteria;
|
|
my $sub = eval 'sub { my ( $set ) = @_; if ' . join(" elsif ", @criteria) . '}';
|
|
die if $EVAL_ERROR;
|
|
return $sub;
|
|
}
|
|
|
|
# make_sort_func {{{3
|
|
# Gets a list of sort columns from the table, like "+cxn -time" and returns a
|
|
# subroutine that will sort that way.
|
|
sub make_sort_func {
|
|
my ( $tbl ) = @_;
|
|
my @criteria;
|
|
|
|
# Pivoted tables can be sorted by 'name' and set_x columns; others must be
|
|
# sorted by existing columns. TODO: this will crash if you toggle between
|
|
# pivoted and nonpivoted. I have several other 'crash' notes about this if
|
|
# this ever becomes possible.
|
|
|
|
if ( $tbl->{pivot} ) {
|
|
# Sort type is not really possible on pivoted columns, because a 'column'
|
|
# contains data from an entire non-pivoted row, so there could be a mix of
|
|
# numeric and non-numeric data. Thus everything has to be 'cmp' type.
|
|
foreach my $col ( split(/\s+/, $tbl->{sort_cols} ) ) {
|
|
next unless $col;
|
|
my ( $dir, $name ) = $col =~ m/([+-])?(\w+)$/;
|
|
next unless $name && $name =~ m/^(?:name|set_\d+)$/;
|
|
$dir ||= '+';
|
|
my $op = 'cmp';
|
|
my $df = "''";
|
|
push @criteria,
|
|
$dir eq '+'
|
|
? "(\$a->{$name} || $df) $op (\$b->{$name} || $df)"
|
|
: "(\$b->{$name} || $df) $op (\$a->{$name} || $df)";
|
|
}
|
|
}
|
|
else {
|
|
foreach my $col ( split(/\s+/, $tbl->{sort_cols} ) ) {
|
|
next unless $col;
|
|
my ( $dir, $name ) = $col =~ m/([+-])?(\w+)$/;
|
|
next unless $name && $tbl->{cols}->{$name};
|
|
$dir ||= '+';
|
|
my $op = $tbl->{cols}->{$name}->{num} ? "<=>" : "cmp";
|
|
my $df = $tbl->{cols}->{$name}->{num} ? "0" : "''";
|
|
push @criteria,
|
|
$dir eq '+'
|
|
? "(\$a->{$name} || $df) $op (\$b->{$name} || $df)"
|
|
: "(\$b->{$name} || $df) $op (\$a->{$name} || $df)";
|
|
}
|
|
}
|
|
return sub { return @_ } unless @criteria;
|
|
my $sub = eval 'sub { sort {' . join("||", @criteria) . '} @_; }';
|
|
die if $EVAL_ERROR;
|
|
return $sub;
|
|
}
|
|
|
|
# trunc {{{3
|
|
# Shortens text to specified length.
|
|
sub trunc {
|
|
my ( $text, $len ) = @_;
|
|
if ( length($text) <= $len ) {
|
|
return $text;
|
|
}
|
|
return substr($text, 0, $len);
|
|
}
|
|
|
|
# donut {{{3
|
|
# Takes out the middle of text to shorten it.
|
|
sub donut {
|
|
my ( $text, $len ) = @_;
|
|
return $text if length($text) <= $len;
|
|
my $max = length($text) - $len;
|
|
my $min = $max - 1;
|
|
|
|
# Try to remove a single "word" from somewhere in the center
|
|
if ( $text =~ s/_[^_]{$min,$max}_/_/ ) {
|
|
return $text;
|
|
}
|
|
|
|
# Prefer removing the end of a "word"
|
|
if ( $text =~ s/([^_]+)[^_]{$max}_/$1_/ ) {
|
|
return $text;
|
|
}
|
|
|
|
$text = substr($text, 0, int($len/2))
|
|
. "_"
|
|
. substr($text, int($len/2) + $max + 1);
|
|
return $text;
|
|
}
|
|
|
|
# crunch {{{3
|
|
# Removes vowels and compacts repeated letters to shorten text.
|
|
sub crunch {
|
|
my ( $text, $len ) = @_;
|
|
return $text if $len && length($text) <= $len;
|
|
$text =~ s/^IB_\w\w_//;
|
|
$text =~ s/(?<![_ ])[aeiou]//g;
|
|
$text =~ s/(.)\1+/$1/g;
|
|
return $text;
|
|
}
|
|
|
|
# collapse_ws {{{3
|
|
# Collapses all whitespace to a single space.
|
|
sub collapse_ws {
|
|
my ( $text ) = @_;
|
|
return '' unless defined $text;
|
|
$text =~ s/\s+/ /g;
|
|
return $text;
|
|
}
|
|
|
|
# Strips out non-printable characters within fields, which freak terminals out.
|
|
sub no_ctrl_char {
|
|
my ( $text ) = @_;
|
|
return '' unless defined $text;
|
|
my $charset = $config{charset}->{val};
|
|
if ( $charset && $charset eq 'unicode' ) {
|
|
$text =~ s/
|
|
("(?:(?!(?<!\\)").)*" # Double-quoted string
|
|
|'(?:(?!(?<!\\)').)*') # Or single-quoted string
|
|
/$1 =~ m#\p{IsC}# ? "[BINARY]" : $1/egx;
|
|
}
|
|
elsif ( $charset && $charset eq 'none' ) {
|
|
$text =~ s/
|
|
("(?:(?!(?<!\\)").)*"
|
|
|'(?:(?!(?<!\\)').)*')
|
|
/[TEXT]/gx;
|
|
}
|
|
else { # The default is 'ascii'
|
|
$text =~ s/
|
|
("(?:(?!(?<!\\)").)*"
|
|
|'(?:(?!(?<!\\)').)*')
|
|
/$1 =~ m#[^\040-\176]# ? "[BINARY]" : $1/egx;
|
|
}
|
|
return $text;
|
|
}
|
|
|
|
# word_wrap {{{3
|
|
# Wraps text at word boundaries so it fits the screen.
|
|
sub word_wrap {
|
|
my ( $text, $width) = @_;
|
|
$width ||= $this_term_size[0];
|
|
$text =~ s/(.{0,$width})(?:\s+|$)/$1\n/g;
|
|
$text =~ s/ +$//mg;
|
|
return $text;
|
|
}
|
|
|
|
# draw_screen {{{3
|
|
# Prints lines to the screen. The first argument is an arrayref. Each
|
|
# element of the array is either a string or an arrayref. If it's a string it
|
|
# just gets printed. If it's an arrayref, the first element is the string to
|
|
# print, and the second is args to colored().
|
|
sub draw_screen {
|
|
my ( $display_lines, $prefs ) = @_;
|
|
if ( !$opts{n} && $config{show_statusbar}->{val} ) {
|
|
unshift @$display_lines, create_statusbar();
|
|
}
|
|
|
|
foreach my $listener ( @{$event_listener_for{draw_screen}} ) {
|
|
$listener->draw_screen($display_lines);
|
|
}
|
|
|
|
$clear_screen_sub->()
|
|
if $prefs->{clear} || !$modes{$config{mode}->{val}}->{no_clear_screen};
|
|
if ( $opts{n} || $prefs->{raw} ) {
|
|
my $num_lines = 0;
|
|
print join("\n",
|
|
map {
|
|
$num_lines++;
|
|
ref $_
|
|
? colored($_->[0], $_->[1])
|
|
: $_;
|
|
}
|
|
grep { !$opts{n} || $_ } # Suppress empty lines
|
|
@$display_lines);
|
|
if ( $opts{n} && $num_lines ) {
|
|
print "\n";
|
|
}
|
|
}
|
|
else {
|
|
my $max_lines = $prefs->{show_all}
|
|
? scalar(@$display_lines)- 1
|
|
: min(scalar(@$display_lines), $this_term_size[1]);
|
|
print join("\n",
|
|
map {
|
|
ref $_
|
|
? colored(substr($_->[0], 0, $this_term_size[0]), $_->[1])
|
|
: substr($_, 0, $this_term_size[0]);
|
|
} @$display_lines[0..$max_lines - 1]);
|
|
}
|
|
}
|
|
|
|
# secs_to_time {{{3
|
|
sub secs_to_time {
|
|
my ( $secs, $fmt ) = @_;
|
|
$secs ||= 0;
|
|
return '00:00' unless $secs;
|
|
|
|
# Decide what format to use, if not given
|
|
$fmt ||= $secs >= 86_400 ? 'd'
|
|
: $secs >= 3_600 ? 'h'
|
|
: 'm';
|
|
|
|
return
|
|
$fmt eq 'd' ? sprintf(
|
|
"%d+%02d:%02d:%02d",
|
|
int($secs / 86_400),
|
|
int(($secs % 86_400) / 3_600),
|
|
int(($secs % 3_600) / 60),
|
|
$secs % 60)
|
|
: $fmt eq 'h' ? sprintf(
|
|
"%02d:%02d:%02d",
|
|
int(($secs % 86_400) / 3_600),
|
|
int(($secs % 3_600) / 60),
|
|
$secs % 60)
|
|
: sprintf(
|
|
"%02d:%02d",
|
|
int(($secs % 3_600) / 60),
|
|
$secs % 60);
|
|
}
|
|
|
|
# dulint_to_int {{{3
|
|
# Takes a number that InnoDB formats as two ulint integers, like transaction IDs
|
|
# and such, and turns it into a single integer
|
|
sub dulint_to_int {
|
|
my $num = shift;
|
|
return 0 unless $num;
|
|
my ( $high, $low ) = $num =~ m/^(\d+) (\d+)$/;
|
|
return $low unless $high;
|
|
return $low + ( $high * $MAX_ULONG );
|
|
}
|
|
|
|
# create_statusbar {{{3
|
|
sub create_statusbar {
|
|
my $mode = $config{mode}->{val};
|
|
my @cxns = sort { $a cmp $b } get_connections();
|
|
|
|
my $modeline = ( $config{readonly}->{val} ? '[RO] ' : '' )
|
|
. $modes{$mode}->{hdr} . " (? for help)";
|
|
my $mode_width = length($modeline);
|
|
my $remaining_width = $this_term_size[0] - $mode_width - 1;
|
|
my $result;
|
|
|
|
# The thingie in top-right that says what we're monitoring.
|
|
my $cxn = '';
|
|
|
|
if ( 1 == @cxns && $dbhs{$cxns[0]} && $dbhs{$cxns[0]}->{dbh} ) {
|
|
$cxn = $dbhs{$cxns[0]}->{dbh}->{mysql_serverinfo} || '';
|
|
}
|
|
else {
|
|
if ( $modes{$mode}->{server_group} ) {
|
|
$cxn = "Servers: " . $modes{$mode}->{server_group};
|
|
my $err_count = grep { $dbhs{$_} && $dbhs{$_}->{err_count} } @cxns;
|
|
if ( $err_count ) {
|
|
$cxn .= "(" . ( scalar(@cxns) - $err_count ) . "/" . scalar(@cxns) . ")";
|
|
}
|
|
}
|
|
else {
|
|
$cxn = join(' ', map { ($dbhs{$_}->{err_count} ? '!' : '') . $_ }
|
|
grep { $dbhs{$_} } @cxns);
|
|
}
|
|
}
|
|
|
|
if ( 1 == @cxns ) {
|
|
get_driver_status(@cxns);
|
|
my $vars = $vars{$cxns[0]}->{$clock};
|
|
my $inc = inc(0, $cxns[0]);
|
|
|
|
# Format server uptime human-readably, calculate QPS...
|
|
my $uptime = secs_to_time( $vars->{Uptime_hires} );
|
|
my $qps = ($inc->{Questions}||0) / ($inc->{Uptime_hires}||1);
|
|
my $ibinfo = '';
|
|
|
|
if ( exists $vars->{IB_last_secs} ) {
|
|
$ibinfo .= "InnoDB $vars->{IB_last_secs}s ";
|
|
if ( $vars->{IB_got_all} ) {
|
|
if ( ($mode eq 'T' || $mode eq 'W')
|
|
&& $vars->{IB_tx_is_truncated} ) {
|
|
$ibinfo .= ':^|';
|
|
}
|
|
else {
|
|
$ibinfo .= ':-)';
|
|
}
|
|
}
|
|
else {
|
|
$ibinfo .= ':-(';
|
|
}
|
|
}
|
|
$result = sprintf(
|
|
"%-${mode_width}s %${remaining_width}s",
|
|
$modeline,
|
|
join(', ', grep { $_ } (
|
|
$cxns[0],
|
|
$uptime,
|
|
$ibinfo,
|
|
shorten($qps) . " QPS",
|
|
($vars->{Threads} || 0) . "/" . ($vars->{Threads_running} || 0) . "/" . ($vars->{Threads_cached} || 0) . " con/run/cac thds",
|
|
$cxn)));
|
|
}
|
|
else {
|
|
$result = sprintf(
|
|
"%-${mode_width}s %${remaining_width}s",
|
|
$modeline,
|
|
$cxn);
|
|
}
|
|
|
|
return $config{color}->{val} ? [ $result, 'bold reverse' ] : $result;
|
|
}
|
|
|
|
# Database connections {{{3
|
|
sub add_new_dsn {
|
|
my ( $name, $dsn, $dl_table, $have_user, $user, $have_pass, $pass, $savepass ) = @_;
|
|
|
|
if ( defined $name ) {
|
|
$name =~ s/[\s:;]//g;
|
|
}
|
|
|
|
if ( !$name ) {
|
|
print word_wrap("Choose a name for the connection. It cannot contain "
|
|
. "whitespace, colons or semicolons."), "\n\n";
|
|
do {
|
|
$name = prompt("Enter a name");
|
|
$name =~ s/[\s:;]//g;
|
|
} until ( $name );
|
|
}
|
|
|
|
if ( !$dsn ) {
|
|
do {
|
|
$clear_screen_sub->();
|
|
print "Typical DSN strings look like\n DBI:mysql:;host=hostname;port=port\n"
|
|
. "The db and port are optional and can usually be omitted.\n"
|
|
. "If you specify 'mysql_read_default_group=mysql' many options can be read\n"
|
|
. "from your mysql options files (~/.my.cnf, /etc/my.cnf).\n\n";
|
|
$dsn = prompt("Enter a DSN string", undef, "DBI:mysql:;mysql_read_default_group=mysql;host=$name");
|
|
} until ( $dsn );
|
|
}
|
|
if ( !$dl_table ) {
|
|
$clear_screen_sub->();
|
|
my $dl_table = prompt("Optional: enter a table (must not exist) to use when resetting InnoDB deadlock information",
|
|
undef, 'test.innotop_dl');
|
|
}
|
|
|
|
$connections{$name} = {
|
|
dsn => $dsn,
|
|
dl_table => $dl_table,
|
|
have_user => $have_user,
|
|
user => $user,
|
|
have_pass => $have_pass,
|
|
pass => $pass,
|
|
savepass => $savepass
|
|
};
|
|
}
|
|
|
|
sub add_new_server_group {
|
|
my ( $name ) = @_;
|
|
|
|
if ( defined $name ) {
|
|
$name =~ s/[\s:;]//g;
|
|
}
|
|
|
|
if ( !$name ) {
|
|
print word_wrap("Choose a name for the group. It cannot contain "
|
|
. "whitespace, colons or semicolons."), "\n\n";
|
|
do {
|
|
$name = prompt("Enter a name");
|
|
$name =~ s/[\s:;]//g;
|
|
} until ( $name );
|
|
}
|
|
|
|
my @cxns;
|
|
do {
|
|
$clear_screen_sub->();
|
|
@cxns = select_cxn("Choose servers for $name", keys %connections);
|
|
} until ( @cxns );
|
|
|
|
$server_groups{$name} = \@cxns;
|
|
return $name;
|
|
}
|
|
|
|
sub get_var_set {
|
|
my ( $name ) = @_;
|
|
while ( !$name || !exists($var_sets{$config{$name}->{val}}) ) {
|
|
$name = choose_var_set($name);
|
|
}
|
|
return $var_sets{$config{$name}->{val}}->{text};
|
|
}
|
|
|
|
sub add_new_var_set {
|
|
my ( $name ) = @_;
|
|
|
|
if ( defined $name ) {
|
|
$name =~ s/\W//g;
|
|
}
|
|
|
|
if ( !$name ) {
|
|
do {
|
|
$name = prompt("Enter a name");
|
|
$name =~ s/\W//g;
|
|
} until ( $name );
|
|
}
|
|
|
|
my $variables;
|
|
do {
|
|
$clear_screen_sub->();
|
|
$variables = prompt("Enter variables for $name", undef );
|
|
} until ( $variables );
|
|
|
|
$var_sets{$name} = { text => $variables, user => 1 };
|
|
}
|
|
|
|
sub next_server {
|
|
my $mode = $config{mode}->{val};
|
|
my @cxns = sort keys %connections;
|
|
my ($cur) = get_connections($mode);
|
|
$cur ||= $cxns[0];
|
|
my $pos = grep { $_ lt $cur } @cxns;
|
|
my $newpos = ($pos + 1) % @cxns;
|
|
$modes{$mode}->{server_group} = '';
|
|
$modes{$mode}->{connections} = [ $cxns[$newpos] ];
|
|
$clear_screen_sub->();
|
|
}
|
|
|
|
sub next_server_group {
|
|
my $mode = shift || $config{mode}->{val};
|
|
my @grps = sort keys %server_groups;
|
|
my $curr = $modes{$mode}->{server_group};
|
|
|
|
return unless @grps;
|
|
|
|
if ( $curr ) {
|
|
# Find the current group's position.
|
|
my $pos = 0;
|
|
while ( $curr ne $grps[$pos] ) {
|
|
$pos++;
|
|
}
|
|
$modes{$mode}->{server_group} = $grps[ ($pos + 1) % @grps ];
|
|
}
|
|
else {
|
|
$modes{$mode}->{server_group} = $grps[0];
|
|
}
|
|
}
|
|
|
|
# Get a list of connection names used in this mode.
|
|
sub get_connections {
|
|
if ( $file ) {
|
|
return qw(file);
|
|
}
|
|
my $mode = shift || $config{mode}->{val};
|
|
my @connections = $modes{$mode}->{server_group}
|
|
? @{$server_groups{$modes{$mode}->{server_group}}}
|
|
: @{$modes{$mode}->{connections}};
|
|
if ( $modes{$mode}->{one_connection} ) {
|
|
@connections = @connections ? $connections[0] : ();
|
|
}
|
|
return unique(@connections);
|
|
}
|
|
|
|
# Get a list of tables used in this mode. If innotop is running non-interactively, just use the first.
|
|
sub get_visible_tables {
|
|
my $mode = shift || $config{mode}->{val};
|
|
my @tbls = @{$modes{$mode}->{visible_tables}};
|
|
if ( $opts{n} ) {
|
|
return $tbls[0];
|
|
}
|
|
else {
|
|
return @tbls;
|
|
}
|
|
}
|
|
|
|
# Choose from among available connections or server groups.
|
|
# If the mode has a server set in use, prefers that instead.
|
|
sub choose_connections {
|
|
$clear_screen_sub->();
|
|
my $mode = $config{mode}->{val};
|
|
my $meta = { map { $_ => $connections{$_}->{dsn} } keys %connections };
|
|
foreach my $group ( keys %server_groups ) {
|
|
$meta->{"#$group"} = join(' ', @{$server_groups{$group}});
|
|
}
|
|
|
|
my $choices = prompt_list("Choose connections or a group for $mode mode",
|
|
undef, sub { return keys %$meta }, $meta);
|
|
|
|
my @choices = unique(grep { $_ } split(/\s+/, $choices));
|
|
if ( @choices ) {
|
|
if ( $choices[0] =~ s/^#// && exists $server_groups{$choices[0]} ) {
|
|
$modes{$mode}->{server_group} = $choices[0];
|
|
}
|
|
else {
|
|
$modes{$mode}->{connections} = [ grep { exists $connections{$_} } @choices ];
|
|
}
|
|
}
|
|
}
|
|
|
|
# Accepts a DB connection name and the name of a prepared query (e.g. status, kill).
|
|
# Also a list of params for the prepared query. This allows not storing prepared
|
|
# statements globally. Returns a $sth that's been executed.
|
|
# ERROR-HANDLING SEMANTICS: if the statement throws an error, propagate, but if the
|
|
# connection has gone away or can't connect, DO NOT. Just return undef.
|
|
sub do_stmt {
|
|
my ( $cxn, $stmt_name, @args ) = @_;
|
|
|
|
return undef if $file;
|
|
|
|
# Test if the cxn should not even be tried
|
|
return undef if $dbhs{$cxn}
|
|
&& $dbhs{$cxn}->{err_count}
|
|
&& ( !$dbhs{$cxn}->{dbh} || !$dbhs{$cxn}->{dbh}->{Active} || $dbhs{$cxn}->{mode} eq $config{mode}->{val} )
|
|
&& $dbhs{$cxn}->{wake_up} > $clock;
|
|
|
|
my $sth;
|
|
my $retries = 1;
|
|
my $success = 0;
|
|
TRY:
|
|
while ( $retries-- >= 0 && !$success ) {
|
|
|
|
eval {
|
|
my $dbh = connect_to_db($cxn);
|
|
|
|
# If the prepared query doesn't exist, make it.
|
|
if ( !exists $dbhs{$cxn}->{stmts}->{$stmt_name} ) {
|
|
$dbhs{$cxn}->{stmts}->{$stmt_name} = $stmt_maker_for{$stmt_name}->($dbh);
|
|
}
|
|
|
|
$sth = $dbhs{$cxn}->{stmts}->{$stmt_name};
|
|
if ( $sth ) {
|
|
$sth->execute(@args);
|
|
}
|
|
$success = 1;
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
if ( $EVAL_ERROR =~ m/$nonfatal_errs/ ) {
|
|
handle_cxn_error($cxn, $EVAL_ERROR);
|
|
}
|
|
else {
|
|
die "$cxn $stmt_name: $EVAL_ERROR";
|
|
}
|
|
if ( $retries < 0 ) {
|
|
$sth = undef;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( $sth && $sth->{NUM_OF_FIELDS} ) {
|
|
sleep($stmt_sleep_time_for{$stmt_name}) if $stmt_sleep_time_for{$stmt_name};
|
|
return $sth;
|
|
}
|
|
}
|
|
|
|
# Keeps track of error count, sleep times till retries, etc etc.
|
|
# When there's an error we retry the connection every so often, increasing in
|
|
# Fibonacci series to prevent too much banging on the server.
|
|
sub handle_cxn_error {
|
|
my ( $cxn, $err ) = @_;
|
|
my $meta = $dbhs{$cxn};
|
|
$meta->{err_count}++;
|
|
|
|
# This is used so errors that have to do with permissions needed by the current
|
|
# mode will get displayed as long as we're in this mode, but get ignored if the
|
|
# mode changes.
|
|
$meta->{mode} = $config{mode}->{val};
|
|
|
|
# Strip garbage from the error text if possible.
|
|
$err =~ s/\s+/ /g;
|
|
if ( $err =~ m/failed: (.*?) at \S*innotop line/ ) {
|
|
$err = $1;
|
|
}
|
|
|
|
$meta->{last_err} = $err;
|
|
my $sleep_time = $meta->{this_sleep} + $meta->{prev_sleep};
|
|
$meta->{prev_sleep} = $meta->{this_sleep};
|
|
$meta->{this_sleep} = $sleep_time;
|
|
$meta->{wake_up} = $clock + $sleep_time;
|
|
if ( $config{show_cxn_errors}->{val} ) {
|
|
print STDERR "Error at tick $clock $cxn $err" if $config{debug}->{val};
|
|
}
|
|
}
|
|
|
|
# Accepts a DB connection name and a (string) query. Returns a $sth that's been
|
|
# executed.
|
|
sub do_query {
|
|
my ( $cxn, $query ) = @_;
|
|
|
|
return undef if $file;
|
|
|
|
# Test if the cxn should not even be tried
|
|
return undef if $dbhs{$cxn}
|
|
&& $dbhs{$cxn}->{err_count}
|
|
&& ( !$dbhs{$cxn}->{dbh} || !$dbhs{$cxn}->{dbh}->{Active} || $dbhs{$cxn}->{mode} eq $config{mode}->{val} )
|
|
&& $dbhs{$cxn}->{wake_up} > $clock;
|
|
|
|
my $sth;
|
|
my $retries = 1;
|
|
my $success = 0;
|
|
TRY:
|
|
while ( $retries-- >= 0 && !$success ) {
|
|
|
|
eval {
|
|
my $dbh = connect_to_db($cxn);
|
|
|
|
$sth = $dbh->prepare($query);
|
|
$sth->execute();
|
|
$success = 1;
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
if ( $EVAL_ERROR =~ m/$nonfatal_errs/ ) {
|
|
handle_cxn_error($cxn, $EVAL_ERROR);
|
|
}
|
|
else {
|
|
die $EVAL_ERROR;
|
|
}
|
|
if ( $retries < 0 ) {
|
|
$sth = undef;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $sth;
|
|
}
|
|
|
|
sub get_uptime {
|
|
my ( $cxn ) = @_;
|
|
$dbhs{$cxn}->{start_time} ||= time();
|
|
# Avoid dividing by zero
|
|
return (time() - $dbhs{$cxn}->{start_time}) || .001;
|
|
}
|
|
|
|
sub connect_to_db {
|
|
my ( $cxn ) = @_;
|
|
|
|
$dbhs{$cxn} ||= {
|
|
stmts => {}, # bucket for prepared statements.
|
|
prev_sleep => 0,
|
|
this_sleep => 1,
|
|
wake_up => 0,
|
|
start_time => 0,
|
|
dbh => undef,
|
|
};
|
|
my $href = $dbhs{$cxn};
|
|
|
|
if ( !$href->{dbh} || ref($href->{dbh}) !~ m/DBI/ || !$href->{dbh}->ping ) {
|
|
my $dbh = get_new_db_connection($cxn);
|
|
@{$href}{qw(dbh err_count wake_up this_sleep start_time prev_sleep)}
|
|
= ($dbh, 0, 0, 1, 0, 0);
|
|
|
|
# Derive and store the server's start time in hi-res
|
|
my $uptime = $dbh->selectrow_hashref("show status like 'Uptime'")->{value};
|
|
$href->{start_time} = time() - $uptime;
|
|
|
|
# Set timeouts so an unused connection stays alive.
|
|
# For example, a connection might be used in Q mode but idle in T mode.
|
|
if ( version_ge($dbh, '4.0.3')) {
|
|
my $timeout = $config{cxn_timeout}->{val};
|
|
$dbh->do("set session wait_timeout=$timeout, interactive_timeout=$timeout");
|
|
}
|
|
}
|
|
return $href->{dbh};
|
|
}
|
|
|
|
# Compares versions like 5.0.27 and 4.1.15-standard-log
|
|
sub version_ge {
|
|
my ( $dbh, $target ) = @_;
|
|
my $version = sprintf('%03d%03d%03d', $dbh->{mysql_serverinfo} =~ m/(\d+)/g);
|
|
return $version ge sprintf('%03d%03d%03d', $target =~ m/(\d+)/g);
|
|
}
|
|
|
|
# Extracts status values that can be gleaned from the DBD driver without doing a whole query.
|
|
sub get_driver_status {
|
|
my @cxns = @_;
|
|
if ( !$info_gotten{driver_status}++ ) {
|
|
foreach my $cxn ( @cxns ) {
|
|
next unless $dbhs{$cxn} && $dbhs{$cxn}->{dbh} && $dbhs{$cxn}->{dbh}->{Active};
|
|
$vars{$cxn}->{$clock} ||= {};
|
|
my $vars = $vars{$cxn}->{$clock};
|
|
my %res = map { $_ =~ s/ +/_/g; $_ } $dbhs{$cxn}->{dbh}->{mysql_stat} =~ m/(\w[^:]+): ([\d\.]+)/g;
|
|
map { $vars->{$_} ||= $res{$_} } keys %res;
|
|
$vars->{Uptime_hires} ||= get_uptime($cxn);
|
|
$vars->{cxn} = $cxn;
|
|
}
|
|
}
|
|
}
|
|
|
|
sub get_new_db_connection {
|
|
my ( $connection, $destroy ) = @_;
|
|
if ( $file ) {
|
|
die "You can't connect to a MySQL server while monitoring a file. This is probably a bug.";
|
|
}
|
|
|
|
my $dsn = $connections{$connection}
|
|
or die "No connection named '$connection' is defined in your configuration";
|
|
|
|
# don't ask for a username if mysql_read_default_group=client is in the DSN
|
|
if ( !defined $dsn->{have_user} and $dsn->{dsn} !~ /mysql_read_default_group=client/ ) {
|
|
my $answer = prompt("Do you want to specify a username for $connection?", undef, 'n');
|
|
$dsn->{have_user} = $answer && $answer =~ m/1|y/i;
|
|
}
|
|
|
|
# don't ask for a password if mysql_read_default_group=client is in the DSN
|
|
if ( !defined $dsn->{have_pass} and $dsn->{dsn} !~ /mysql_read_default_group=client/ ) {
|
|
my $answer = prompt("Do you want to specify a password for $connection?", undef, 'n');
|
|
$dsn->{have_pass} = $answer && $answer =~ m/1|y/i;
|
|
}
|
|
|
|
if ( !$dsn->{user} && $dsn->{have_user} ) {
|
|
my $user = $ENV{USERNAME} || $ENV{USER} || getlogin() || getpwuid($REAL_USER_ID) || undef;
|
|
$dsn->{user} = prompt("Enter username for $connection", undef, $user);
|
|
}
|
|
|
|
if ( !defined $dsn->{user} ) {
|
|
$dsn->{user} = '';
|
|
}
|
|
|
|
if ( !$dsn->{pass} && !$dsn->{savepass} && $dsn->{have_pass} ) {
|
|
$dsn->{pass} = prompt_noecho("Enter password for '$dsn->{user}' on $connection");
|
|
print "\n";
|
|
if ( !defined($dsn->{savepass}) ) {
|
|
my $answer = prompt("Save password in plain text in the config file?", undef, 'y');
|
|
$dsn->{savepass} = $answer && $answer =~ m/1|y/i;
|
|
}
|
|
}
|
|
|
|
my $dbh = DBI->connect(
|
|
$dsn->{dsn}, $dsn->{user}, $dsn->{pass},
|
|
{ RaiseError => 1, PrintError => 0, AutoCommit => 1 });
|
|
$dbh->{InactiveDestroy} = 1 unless $destroy; # Can't be set in $db_options
|
|
$dbh->{FetchHashKeyName} = 'NAME_lc'; # Lowercases all column names for fetchrow_hashref
|
|
return $dbh;
|
|
}
|
|
|
|
sub get_cxn_errors {
|
|
my @cxns = @_;
|
|
return () unless $config{show_cxn_errors_in_tbl}->{val};
|
|
return
|
|
map { [ $_ . ': ' . $dbhs{$_}->{last_err}, 'red' ] }
|
|
grep { $dbhs{$_} && $dbhs{$_}->{err_count} && $dbhs{$_}->{mode} eq $config{mode}->{val} }
|
|
@cxns;
|
|
}
|
|
|
|
# Setup and tear-down functions {{{2
|
|
|
|
# Takes a string and turns it into a hashref you can apply to %tbl_meta tables. The string
|
|
# can be in the form 'foo, bar, foo/bar, foo as bar' much like a SQL SELECT statement.
|
|
sub compile_select_stmt {
|
|
my ($str) = @_;
|
|
my @exps = $str =~ m/\s*([^,]+(?i:\s+as\s+[^,\s]+)?)\s*(?=,|$)/g;
|
|
my %cols;
|
|
my @visible;
|
|
foreach my $exp ( @exps ) {
|
|
my ( $text, $colname );
|
|
if ( $exp =~ m/as\s+(\w+)\s*/ ) {
|
|
$colname = $1;
|
|
$exp =~ s/as\s+(\w+)\s*//;
|
|
$text = $exp;
|
|
}
|
|
else {
|
|
$text = $colname = $exp;
|
|
}
|
|
my ($func, $err) = compile_expr($text);
|
|
$cols{$colname} = {
|
|
src => $text,
|
|
hdr => $colname,
|
|
num => 0,
|
|
func => $func,
|
|
};
|
|
push @visible, $colname;
|
|
}
|
|
return (\%cols, \@visible);
|
|
}
|
|
|
|
# compile_filter {{{3
|
|
sub compile_filter {
|
|
my ( $text ) = @_;
|
|
my ( $sub, $err );
|
|
eval "\$sub = sub { my \$set = shift; $text }";
|
|
if ( $EVAL_ERROR ) {
|
|
$EVAL_ERROR =~ s/at \(eval.*$//;
|
|
$sub = sub { return $EVAL_ERROR };
|
|
$err = $EVAL_ERROR;
|
|
}
|
|
return ( $sub, $err );
|
|
}
|
|
|
|
# compile_expr {{{3
|
|
sub compile_expr {
|
|
my ( $expr ) = @_;
|
|
# Leave built-in functions alone so they get called as Perl functions, unless
|
|
# they are the only word in $expr, in which case treat them as hash keys.
|
|
if ( $expr =~ m/\W/ ) {
|
|
$expr =~ s/(?<!\{|\$)\b([A-Za-z]\w{2,})\b/is_func($1) ? $1 : "\$set->{$1}"/eg;
|
|
}
|
|
else {
|
|
$expr = "\$set->{$expr}";
|
|
}
|
|
my ( $sub, $err );
|
|
my $quoted = quotemeta($expr);
|
|
eval qq{
|
|
\$sub = sub {
|
|
my (\$set, \$cur, \$pre) = \@_;
|
|
my \$val = eval { $expr };
|
|
if ( \$EVAL_ERROR && \$config{debug}->{val} ) {
|
|
\$EVAL_ERROR =~ s/ at \\(eval.*//s;
|
|
die "\$EVAL_ERROR in expression $quoted";
|
|
}
|
|
return \$val;
|
|
}
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
if ( $config{debug}->{val} ) {
|
|
die $EVAL_ERROR;
|
|
}
|
|
$EVAL_ERROR =~ s/ at \(eval.*$//;
|
|
$sub = sub { return $EVAL_ERROR };
|
|
$err = $EVAL_ERROR;
|
|
}
|
|
return ( $sub, $err );
|
|
}
|
|
|
|
# finish {{{3
|
|
# This is a subroutine because it's called from a key to quit the program.
|
|
sub finish {
|
|
save_config();
|
|
ReadMode('normal') unless $opts{n};
|
|
print "\n";
|
|
exit(0);
|
|
}
|
|
|
|
# core_dump {{{3
|
|
sub core_dump {
|
|
my $msg = shift;
|
|
if ($config{debugfile}->{val} && $config{debug}->{val}) {
|
|
eval {
|
|
open my $file, '>>', $config{debugfile}->{val};
|
|
if ( %vars ) {
|
|
print $file "Current variables:\n" . Dumper(\%vars);
|
|
}
|
|
close $file;
|
|
};
|
|
}
|
|
print $msg;
|
|
}
|
|
|
|
# migrate_config {{{3
|
|
sub migrate_config {
|
|
|
|
my ($old_filename, $new_filename) = @_;
|
|
|
|
# don't proceed if old file doesn't exist
|
|
if ( ! -f $old_filename ) {
|
|
die "Error migrating '$old_filename': file doesn't exist.\n";
|
|
}
|
|
# don't migrate files if new file exists
|
|
elsif ( -f $new_filename ) {
|
|
die "Error migrating '$old_filename' to '$new_filename': new file already exists.\n";
|
|
}
|
|
# if migrating from one file to another in the same directory, just rename them
|
|
if (dirname($old_filename) eq dirname($new_filename)) {
|
|
rename($old_filename, $new_filename)
|
|
or die "Can't rename '$old_filename' to '$new_filename': $OS_ERROR";
|
|
}
|
|
# otherwise, move the existing conf file to a temp file, make the necessary directory structure,
|
|
# and move the temp conf file to its new home
|
|
else {
|
|
my $tmp = File::Temp->new( TEMPLATE => 'innotopXXXXX', DIR => $homepath, SUFFIX => '.conf');
|
|
my $tmp_filename = $tmp->filename;
|
|
my $dirname = dirname($new_filename);
|
|
rename($old_filename, $tmp_filename)
|
|
or die "Can't rename '$old_filename' to '$tmp_filename': $OS_ERROR";
|
|
mkdir($dirname) or die "Can't create directory '$dirname': $OS_ERROR";
|
|
mkdir("$dirname/plugins") or die "Can't create directory '$dirname/plugins': $OS_ERROR";
|
|
rename($tmp_filename, $new_filename)
|
|
or die "Can't rename '$tmp_filename' to '$new_filename': $OS_ERROR";
|
|
}
|
|
}
|
|
|
|
# load_config {{{3
|
|
sub load_config {
|
|
|
|
my ($old_filename, $answer);
|
|
|
|
if ( $opts{u} or $opts{p} or $opts{h} or $opts{P} ) {
|
|
my @params = $dsn_parser->get_cxn_params(\%opts); # dsn=$params[0]
|
|
add_new_dsn($opts{h} || 'localhost', $params[0], 'test.innotop_dl',
|
|
$opts{u} ? 1 : 0, $opts{u}, $opts{p} ? 1 : 0, $opts{p});
|
|
}
|
|
if ($opts{c}) {
|
|
$conf_file = $opts{c};
|
|
}
|
|
# innotop got upgraded and this is an old config file.
|
|
elsif ( -f "$homepath/.innotop" or -f "$homepath/.innotop/innotop.ini" ) {
|
|
$conf_file = $default_home_conf;
|
|
if ( -f "$homepath/.innotop") {
|
|
$old_filename = "$homepath/.innotop";
|
|
}
|
|
elsif ( -f "$homepath/.innotop/innotop.ini" ) {
|
|
$old_filename = "$homepath/.innotop/innotop.ini";
|
|
}
|
|
$answer = pause("Innotop's default config location has moved to '$conf_file'. Move old config file '$old_filename' there now? y/n");
|
|
if ( lc $answer eq 'y' ) {
|
|
migrate_config($old_filename, $conf_file);
|
|
}
|
|
else {
|
|
print "\nInnotop will now exit so you can fix the config file.\n";
|
|
exit(0);
|
|
}
|
|
}
|
|
elsif ( -f $default_home_conf ) {
|
|
$conf_file = $default_home_conf;
|
|
}
|
|
elsif ( -f $default_central_conf and not $opts{s} ) {
|
|
$conf_file = $default_central_conf;
|
|
}
|
|
else {
|
|
# If no config file was loaded, set readonly to 0 if the user wants to
|
|
# write a config
|
|
$config{readonly}->{val} = 0 if $opts{w};
|
|
# If no connections have been defined, connect to a MySQL database
|
|
# on localhost using mysql_read_default_group=client
|
|
if (!%connections) {
|
|
add_new_dsn('localhost',
|
|
'DBI:mysql:;host=localhost;mysql_read_default_group=client',
|
|
'test.innotop_dl');
|
|
}
|
|
}
|
|
|
|
if ( -f "$conf_file" ) {
|
|
open my $file, "<", $conf_file or die("Can't open '$conf_file': $OS_ERROR");
|
|
|
|
# Check config file version. Just ignore if either innotop or the file has
|
|
# garbage in the version number.
|
|
if ( defined(my $line = <$file>) && $VERSION =~ m/\d/ ) {
|
|
chomp $line;
|
|
if ( my ($maj, $min, $rev) = $line =~ m/^version=(\d+)\.(\d+)(?:\.(\d+))?$/ ) {
|
|
$rev ||= 0;
|
|
my $cfg_ver = sprintf('%03d-%03d-%03d', $maj, $min, $rev);
|
|
( $maj, $min, $rev ) = $VERSION =~ m/^(\d+)\.(\d+)(?:\.(\d+))?$/;
|
|
$rev ||= 0;
|
|
my $innotop_ver = sprintf('%03d-%03d-%03d', $maj, $min, $rev);
|
|
|
|
if ( $cfg_ver gt $innotop_ver ) {
|
|
pause("The config file is for a newer version of innotop and may not be read correctly.");
|
|
}
|
|
else {
|
|
my @ver_history = @config_versions;
|
|
while ( my ($start, $end) = splice(@ver_history, 0, 2) ) {
|
|
# If the config file is between the endpoints and innotop is greater than
|
|
# the endpoint, innotop has a newer config file format than the file.
|
|
if ( $cfg_ver ge $start && $cfg_ver lt $end && $innotop_ver ge $end ) {
|
|
my $msg = "innotop's config file format has changed. Overwrite $conf_file? y or n";
|
|
if ( pause($msg) eq 'n' ) {
|
|
$config{readonly}->{val} = 1;
|
|
print "\ninnotop will not save any configuration changes you make.";
|
|
pause();
|
|
print "\n";
|
|
}
|
|
close $file;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next unless $line =~ m/^\[([a-z_]+)\]$/;
|
|
if ( exists $config_file_sections{$1} ) {
|
|
$config_file_sections{$1}->{reader}->($file);
|
|
}
|
|
else {
|
|
warn "Unknown config file section '$1'";
|
|
}
|
|
}
|
|
close $file or die("Can't close $conf_file: $OS_ERROR");
|
|
}
|
|
|
|
}
|
|
|
|
# Do some post-processing on %tbl_meta: compile src properties into func etc.
|
|
sub post_process_tbl_meta {
|
|
foreach my $table ( values %tbl_meta ) {
|
|
foreach my $col_name ( keys %{$table->{cols}} ) {
|
|
my $col_def = $table->{cols}->{$col_name};
|
|
my ( $sub, $err ) = compile_expr($col_def->{src});
|
|
$col_def->{func} = $sub;
|
|
}
|
|
}
|
|
}
|
|
|
|
# load_config_plugins {{{3
|
|
sub load_config_plugins {
|
|
my ( $file ) = @_;
|
|
|
|
# First, find a list of all plugins that exist on disk, and get information about them.
|
|
my $dir = $config{plugin_dir}->{val};
|
|
foreach my $p_file ( <$dir/*.pm> ) {
|
|
my ($package, $desc);
|
|
eval {
|
|
open my $p_in, "<", $p_file or die $OS_ERROR;
|
|
while ( my $line = <$p_in> ) {
|
|
chomp $line;
|
|
if ( $line =~ m/^package\s+(.*?);/ ) {
|
|
$package = $1;
|
|
}
|
|
elsif ( $line =~ m/^# description: (.*)/ ) {
|
|
$desc = $1;
|
|
}
|
|
last if $package && $desc;
|
|
}
|
|
close $p_in;
|
|
};
|
|
if ( $package ) {
|
|
$plugins{$package} = {
|
|
file => $p_file,
|
|
desc => $desc,
|
|
class => $package,
|
|
active => 0,
|
|
};
|
|
if ( $config{debug}->{val} && $EVAL_ERROR ) {
|
|
die $EVAL_ERROR;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Now read which ones the user has activated. Each line simply represents an active plugin.
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
next unless $line && $plugins{$line};
|
|
|
|
my $obj;
|
|
eval {
|
|
require $plugins{$line}->{file};
|
|
$obj = $line->new(%pluggable_vars);
|
|
foreach my $event ( $obj->register_for_events() ) {
|
|
my $queue = $event_listener_for{$event};
|
|
if ( $queue ) {
|
|
push @$queue, $obj;
|
|
}
|
|
}
|
|
};
|
|
if ( $config{debug}->{val} && $EVAL_ERROR ) {
|
|
die $EVAL_ERROR;
|
|
}
|
|
if ( $obj ) {
|
|
$plugins{$line}->{active} = 1;
|
|
$plugins{$line}->{object} = $obj;
|
|
}
|
|
}
|
|
}
|
|
|
|
# save_config_plugins {{{3
|
|
sub save_config_plugins {
|
|
my $file = shift;
|
|
foreach my $class ( sort keys %plugins ) {
|
|
next unless $plugins{$class}->{active};
|
|
print $file "$class\n";
|
|
}
|
|
}
|
|
|
|
# load_config_active_server_groups {{{3
|
|
sub load_config_active_server_groups {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $mode, $group ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $mode && $group
|
|
&& exists $modes{$mode} && exists $server_groups{$group};
|
|
$modes{$mode}->{server_group} = $group;
|
|
}
|
|
}
|
|
|
|
# save_config_active_server_groups {{{3
|
|
sub save_config_active_server_groups {
|
|
my $file = shift;
|
|
foreach my $mode ( sort keys %modes ) {
|
|
print $file "$mode=$modes{$mode}->{server_group}\n";
|
|
}
|
|
}
|
|
|
|
# load_config_server_groups {{{3
|
|
sub load_config_server_groups {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $name, $rest ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $name && $rest;
|
|
my @vars = unique(grep { $_ && exists $connections{$_} } split(/\s+/, $rest));
|
|
next unless @vars;
|
|
$server_groups{$name} = \@vars;
|
|
}
|
|
}
|
|
|
|
# save_config_server_groups {{{3
|
|
sub save_config_server_groups {
|
|
my $file = shift;
|
|
foreach my $set ( sort keys %server_groups ) {
|
|
print $file "$set=", join(' ', @{$server_groups{$set}}), "\n";
|
|
}
|
|
}
|
|
|
|
# load_config_varsets {{{3
|
|
sub load_config_varsets {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $name, $rest ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $name && $rest;
|
|
$var_sets{$name} = {
|
|
text => $rest,
|
|
user => 1,
|
|
};
|
|
}
|
|
}
|
|
|
|
# save_config_varsets {{{3
|
|
sub save_config_varsets {
|
|
my $file = shift;
|
|
foreach my $varset ( sort keys %var_sets ) {
|
|
next unless $var_sets{$varset}->{user};
|
|
print $file "$varset=$var_sets{$varset}->{text}\n";
|
|
}
|
|
}
|
|
|
|
# load_config_group_by {{{3
|
|
sub load_config_group_by {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $tbl , $rest ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $tbl && exists $tbl_meta{$tbl};
|
|
my @parts = unique(grep { exists($tbl_meta{$tbl}->{cols}->{$_}) } split(/\s+/, $rest));
|
|
$tbl_meta{$tbl}->{group_by} = [ @parts ];
|
|
$tbl_meta{$tbl}->{cust}->{group_by} = 1;
|
|
}
|
|
}
|
|
|
|
# save_config_group_by {{{3
|
|
sub save_config_group_by {
|
|
my $file = shift;
|
|
foreach my $tbl ( sort keys %tbl_meta ) {
|
|
next if $tbl_meta{$tbl}->{temp};
|
|
next unless $tbl_meta{$tbl}->{cust}->{group_by};
|
|
my $aref = $tbl_meta{$tbl}->{group_by};
|
|
print $file "$tbl=", join(' ', @$aref), "\n";
|
|
}
|
|
}
|
|
|
|
# load_config_filters {{{3
|
|
sub load_config_filters {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $key, $rest ) = $line =~ m/^(.+?)=(.*)$/;
|
|
next unless $key && $rest;
|
|
|
|
my %parts = $rest =~ m/(\w+)='((?:(?!(?<!\\)').)*)'/g; # Properties are single-quoted
|
|
next unless $parts{text} && $parts{tbls};
|
|
|
|
foreach my $prop ( keys %parts ) {
|
|
# Un-escape escaping
|
|
$parts{$prop} =~ s/\\\\/\\/g;
|
|
$parts{$prop} =~ s/\\'/'/g;
|
|
}
|
|
|
|
my ( $sub, $err ) = compile_filter($parts{text});
|
|
my @tbls = unique(split(/\s+/, $parts{tbls}));
|
|
@tbls = grep { exists $tbl_meta{$_} } @tbls;
|
|
$filters{$key} = {
|
|
func => $sub,
|
|
text => $parts{text},
|
|
user => 1,
|
|
name => $key,
|
|
note => 'User-defined filter',
|
|
tbls => \@tbls,
|
|
}
|
|
}
|
|
}
|
|
|
|
# save_config_filters {{{3
|
|
sub save_config_filters {
|
|
my $file = shift;
|
|
foreach my $key ( sort keys %filters ) {
|
|
next if !$filters{$key}->{user} || $filters{$key}->{quick};
|
|
my $text = $filters{$key}->{text};
|
|
$text =~ s/([\\'])/\\$1/g;
|
|
my $tbls = join(" ", @{$filters{$key}->{tbls}});
|
|
print $file "$key=text='$text' tbls='$tbls'\n";
|
|
}
|
|
}
|
|
|
|
# load_config_visible_tables {{{3
|
|
sub load_config_visible_tables {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $mode, $rest ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $mode && exists $modes{$mode};
|
|
$modes{$mode}->{visible_tables} =
|
|
[ unique(grep { $_ && exists $tbl_meta{$_} } split(/\s+/, $rest)) ];
|
|
$modes{$mode}->{cust}->{visible_tables} = 1;
|
|
}
|
|
}
|
|
|
|
# save_config_visible_tables {{{3
|
|
sub save_config_visible_tables {
|
|
my $file = shift;
|
|
foreach my $mode ( sort keys %modes ) {
|
|
next unless $modes{$mode}->{cust}->{visible_tables};
|
|
my $tables = $modes{$mode}->{visible_tables};
|
|
print $file "$mode=", join(' ', @$tables), "\n";
|
|
}
|
|
}
|
|
|
|
# load_config_sort_cols {{{3
|
|
sub load_config_sort_cols {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $key , $rest ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $key && exists $tbl_meta{$key};
|
|
$tbl_meta{$key}->{sort_cols} = $rest;
|
|
$tbl_meta{$key}->{cust}->{sort_cols} = 1;
|
|
$tbl_meta{$key}->{sort_func} = make_sort_func($tbl_meta{$key});
|
|
}
|
|
}
|
|
|
|
# save_config_sort_cols {{{3
|
|
sub save_config_sort_cols {
|
|
my $file = shift;
|
|
foreach my $tbl ( sort keys %tbl_meta ) {
|
|
next unless $tbl_meta{$tbl}->{cust}->{sort_cols};
|
|
my $col = $tbl_meta{$tbl}->{sort_cols};
|
|
print $file "$tbl=$col\n";
|
|
}
|
|
}
|
|
|
|
# load_config_active_filters {{{3
|
|
sub load_config_active_filters {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $tbl , $rest ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $tbl && exists $tbl_meta{$tbl};
|
|
my @parts = unique(grep { exists($filters{$_}) } split(/\s+/, $rest));
|
|
@parts = grep { grep { $tbl eq $_ } @{$filters{$_}->{tbls}} } @parts;
|
|
$tbl_meta{$tbl}->{filters} = [ @parts ];
|
|
$tbl_meta{$tbl}->{cust}->{filters} = 1;
|
|
}
|
|
}
|
|
|
|
# save_config_active_filters {{{3
|
|
sub save_config_active_filters {
|
|
my $file = shift;
|
|
foreach my $tbl ( sort keys %tbl_meta ) {
|
|
next if $tbl_meta{$tbl}->{temp};
|
|
next unless $tbl_meta{$tbl}->{cust}->{filters};
|
|
my $aref = $tbl_meta{$tbl}->{filters};
|
|
print $file "$tbl=", join(' ', @$aref), "\n";
|
|
}
|
|
}
|
|
|
|
# load_config_active_columns {{{3
|
|
sub load_config_active_columns {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $key , $rest ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $key && exists $tbl_meta{$key};
|
|
my @parts = grep { exists($tbl_meta{$key}->{cols}->{$_}) } unique split(/ /, $rest);
|
|
$tbl_meta{$key}->{visible} = [ @parts ];
|
|
$tbl_meta{$key}->{cust}->{visible} = 1;
|
|
}
|
|
}
|
|
|
|
# save_config_active_columns {{{3
|
|
sub save_config_active_columns {
|
|
my $file = shift;
|
|
foreach my $tbl ( sort keys %tbl_meta ) {
|
|
next unless $tbl_meta{$tbl}->{cust}->{visible};
|
|
my $aref = $tbl_meta{$tbl}->{visible};
|
|
print $file "$tbl=", join(' ', @$aref), "\n";
|
|
}
|
|
}
|
|
|
|
# save_config_tbl_meta {{{3
|
|
sub save_config_tbl_meta {
|
|
my $file = shift;
|
|
foreach my $tbl ( sort keys %tbl_meta ) {
|
|
foreach my $col ( keys %{$tbl_meta{$tbl}->{cols}} ) {
|
|
my $meta = $tbl_meta{$tbl}->{cols}->{$col};
|
|
next unless $meta->{user};
|
|
print $file "$col=", join(
|
|
" ",
|
|
map {
|
|
# Some properties (trans) are arrays, others scalars
|
|
my $val = ref($meta->{$_}) ? join(',', @{$meta->{$_}}) : $meta->{$_};
|
|
$val =~ s/([\\'])/\\$1/g; # Escape backslashes and single quotes
|
|
"$_='$val'"; # Enclose in single quotes
|
|
}
|
|
grep { $_ ne 'func' }
|
|
keys %$meta
|
|
), "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
# save_config_config {{{3
|
|
sub save_config_config {
|
|
my $file = shift;
|
|
foreach my $key ( sort keys %config ) {
|
|
eval {
|
|
if ( $key ne 'password' || $config{savepass}->{val} ) {
|
|
print $file "# $config{$key}->{note}\n"
|
|
or die "Cannot print to file: $OS_ERROR";
|
|
my $val = $config{$key}->{val};
|
|
$val = '' unless defined($val);
|
|
if ( ref( $val ) eq 'ARRAY' ) {
|
|
print $file "$key="
|
|
. join( " ", @$val ) . "\n"
|
|
or die "Cannot print to file: $OS_ERROR";
|
|
}
|
|
elsif ( ref( $val ) eq 'HASH' ) {
|
|
print $file "$key="
|
|
. join( " ",
|
|
map { "$_:$val->{$_}" } keys %$val
|
|
) . "\n";
|
|
}
|
|
else {
|
|
print $file "$key=$val\n";
|
|
}
|
|
}
|
|
};
|
|
if ( $EVAL_ERROR ) { print "$EVAL_ERROR in $key"; };
|
|
}
|
|
|
|
}
|
|
|
|
# load_config_config {{{3
|
|
sub load_config_config {
|
|
my ( $file ) = @_;
|
|
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $name, $val ) = $line =~ m/^(.+?)=(.*)$/;
|
|
next unless defined $name && defined $val;
|
|
|
|
# Validate the incoming values...
|
|
if ( $name && exists( $config{$name} ) ) {
|
|
if ( !$config{$name}->{pat} || $val =~ m/$config{$name}->{pat}/ ) {
|
|
$config{$name}->{val} = $val;
|
|
$config{$name}->{read} = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# load_config_tbl_meta {{{3
|
|
sub load_config_tbl_meta {
|
|
my ( $file ) = @_;
|
|
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
# Each tbl_meta section has all the properties defined in %col_props.
|
|
my ( $col , $rest ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $col;
|
|
my %parts = $rest =~ m/(\w+)='((?:(?!(?<!\\)').)*)'/g; # Properties are single-quoted
|
|
|
|
# Each section read from the config file has one extra property: which table it
|
|
# goes in.
|
|
my $tbl = $parts{tbl} or die "There's no table for tbl_meta $col";
|
|
my $meta = $tbl_meta{$tbl} or die "There's no table in tbl_meta named $tbl";
|
|
|
|
# The section is user-defined by definition (if that makes sense).
|
|
$parts{user} = 1;
|
|
|
|
# The column may already exist in the table, in which case this is just a
|
|
# customization.
|
|
$meta->{cols}->{$col} ||= {};
|
|
|
|
foreach my $prop ( keys %col_props ) {
|
|
if ( !defined($parts{$prop}) ) {
|
|
die "Undefined property $prop for column $col in table $tbl";
|
|
}
|
|
|
|
# Un-escape escaping
|
|
$parts{$prop} =~ s/\\\\/\\/g;
|
|
$parts{$prop} =~ s/\\'/'/g;
|
|
|
|
if ( ref $col_props{$prop} ) {
|
|
if ( $prop eq 'trans' ) {
|
|
$meta->{cols}->{$col}->{trans}
|
|
= [ unique(grep { exists $trans_funcs{$_} } split(',', $parts{$prop})) ];
|
|
}
|
|
else {
|
|
$meta->{cols}->{$col}->{$prop} = [ split(',', $parts{$prop}) ];
|
|
}
|
|
}
|
|
else {
|
|
$meta->{cols}->{$col}->{$prop} = $parts{$prop};
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
# save_config {{{3
|
|
sub save_config {
|
|
print "\n";
|
|
return if $config{readonly}->{val};
|
|
# return if no config file was loaded and -w wasn't specified
|
|
if (not $conf_file) {
|
|
if (not $opts{w}) {
|
|
return;
|
|
}
|
|
else {
|
|
# if no config was loaded but -w was specified,
|
|
# write to $default_home_conf
|
|
$conf_file = $default_home_conf;
|
|
}
|
|
}
|
|
elsif ($conf_file and $opts{w}) {
|
|
print "Loaded config file on start-up, so ignoring -w (see --help)\n"
|
|
}
|
|
|
|
my $dirname = dirname($conf_file);
|
|
|
|
# if directories don't exist, create them. This could cause errors
|
|
# or warnings if a central config doesn't have readonly=1, but being
|
|
# flexible requires giving the user enough rope to hang themselves with.
|
|
if ( ! -d $dirname ) {
|
|
mkdir $dirname
|
|
or die "Can't create directory '$dirname': $OS_ERROR";
|
|
}
|
|
if ( ! -d "$dirname/plugins" ) {
|
|
mkdir "$dirname/plugins"
|
|
or warn "Can't create directory '$dirname/plugins': $OS_ERROR\n";
|
|
}
|
|
|
|
# Save to a temp file first, so a crash doesn't destroy the main config file
|
|
my $tmpfile = File::Temp->new( TEMPLATE => 'innotopXXXXX', DIR => $dirname, SUFFIX => '.conf.tmp');
|
|
open my $file, "+>", $tmpfile
|
|
or die("Can't write to $tmpfile: $OS_ERROR");
|
|
print $file "version=$VERSION\n";
|
|
|
|
foreach my $section ( @ordered_config_file_sections ) {
|
|
die "No such config file section $section" unless $config_file_sections{$section};
|
|
print $file "\n[$section]\n\n";
|
|
$config_file_sections{$section}->{writer}->($file);
|
|
print $file "\n[/$section]\n";
|
|
}
|
|
|
|
# Now clobber the main config file with the temp.
|
|
close $file or die("Can't close $tmpfile: $OS_ERROR");
|
|
rename($tmpfile, $conf_file) or die("Can't rename $tmpfile to $conf_file: $OS_ERROR");
|
|
}
|
|
|
|
# load_config_connections {{{3
|
|
sub load_config_connections {
|
|
return if $opts{u} or $opts{p} or $opts{h} or $opts{P}; # don't load connections if DSN or user/pass options used
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $key , $rest ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $key;
|
|
my %parts = $rest =~ m/(\S+?)=(\S*)/g;
|
|
my %conn = map { $_ => $parts{$_} || '' } @conn_parts;
|
|
$connections{$key} = \%conn;
|
|
}
|
|
}
|
|
|
|
# save_config_connections {{{3
|
|
sub save_config_connections {
|
|
my $file = shift;
|
|
foreach my $conn ( sort keys %connections ) {
|
|
my $href = $connections{$conn};
|
|
my @keys = $href->{savepass} ? @conn_parts : grep { $_ ne 'pass' } @conn_parts;
|
|
print $file "$conn=", join(' ', map { "$_=$href->{$_}" } grep { defined $href->{$_} } @keys), "\n";
|
|
}
|
|
}
|
|
|
|
sub load_config_colors {
|
|
my ( $file ) = @_;
|
|
my %rule_set_for;
|
|
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $tbl, $rule ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $tbl && $rule;
|
|
next unless exists $tbl_meta{$tbl};
|
|
my %parts = $rule =~ m/(\w+)='((?:(?!(?<!\\)').)*)'/g; # Properties are single-quoted
|
|
next unless $parts{col} && exists $tbl_meta{$tbl}->{cols}->{$parts{col}};
|
|
next unless $parts{op} && exists $comp_ops{$parts{op}};
|
|
next unless defined $parts{arg};
|
|
next unless defined $parts{color};
|
|
my @colors = unique(grep { exists $ansicolors{$_} } split(/\W+/, $parts{color}));
|
|
next unless @colors;
|
|
|
|
# Finally! Enough validation...
|
|
$rule_set_for{$tbl} ||= [];
|
|
push @{$rule_set_for{$tbl}}, \%parts;
|
|
}
|
|
|
|
foreach my $tbl ( keys %rule_set_for ) {
|
|
$tbl_meta{$tbl}->{colors} = $rule_set_for{$tbl};
|
|
$tbl_meta{$tbl}->{color_func} = make_color_func($tbl_meta{$tbl});
|
|
$tbl_meta{$tbl}->{cust}->{colors} = 1;
|
|
}
|
|
}
|
|
|
|
# save_config_colors {{{3
|
|
sub save_config_colors {
|
|
my $file = shift;
|
|
foreach my $tbl ( sort keys %tbl_meta ) {
|
|
my $meta = $tbl_meta{$tbl};
|
|
next unless $meta->{cust}->{colors};
|
|
foreach my $rule ( @{$meta->{colors}} ) {
|
|
print $file "$tbl=", join(
|
|
' ',
|
|
map {
|
|
my $val = $rule->{$_};
|
|
$val =~ s/([\\'])/\\$1/g; # Escape backslashes and single quotes
|
|
"$_='$val'"; # Enclose in single quotes
|
|
}
|
|
qw(col op arg color)
|
|
), "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
# load_config_active_connections {{{3
|
|
sub load_config_active_connections {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $key , $rest ) = $line =~ m/^(.*?)=(.*)$/;
|
|
next unless $key && exists $modes{$key};
|
|
my @parts = grep { exists $connections{$_} } split(/ /, $rest);
|
|
$modes{$key}->{connections} = [ @parts ] if exists $modes{$key};
|
|
}
|
|
}
|
|
|
|
# save_config_active_connections {{{3
|
|
sub save_config_active_connections {
|
|
my $file = shift;
|
|
foreach my $mode ( sort keys %modes ) {
|
|
my @connections = get_connections($mode);
|
|
print $file "$mode=", join(' ', @connections), "\n";
|
|
}
|
|
}
|
|
|
|
# load_config_stmt_sleep_times {{{3
|
|
sub load_config_stmt_sleep_times {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $key , $val ) = split('=', $line);
|
|
next unless $key && defined $val && $val =~ m/$num_regex/;
|
|
$stmt_sleep_time_for{$key} = $val;
|
|
}
|
|
}
|
|
|
|
# save_config_stmt_sleep_times {{{3
|
|
sub save_config_stmt_sleep_times {
|
|
my $file = shift;
|
|
foreach my $key ( sort keys %stmt_sleep_time_for ) {
|
|
print $file "$key=$stmt_sleep_time_for{$key}\n";
|
|
}
|
|
}
|
|
|
|
# load_config_mvs {{{3
|
|
sub load_config_mvs {
|
|
my ( $file ) = @_;
|
|
while ( my $line = <$file> ) {
|
|
chomp $line;
|
|
next if $line =~ m/^#/;
|
|
last if $line =~ m/^\[/;
|
|
|
|
my ( $key , $val ) = split('=', $line);
|
|
next unless $key && defined $val && $val =~ m/$num_regex/;
|
|
$mvs{$key} = $val;
|
|
}
|
|
}
|
|
|
|
# save_config_mvs {{{3
|
|
sub save_config_mvs {
|
|
my $file = shift;
|
|
foreach my $key ( sort keys %mvs ) {
|
|
print $file "$key=$mvs{$key}\n";
|
|
}
|
|
}
|
|
|
|
# edit_configuration {{{3
|
|
sub edit_configuration {
|
|
my $key = '';
|
|
while ( $key ne 'q' ) {
|
|
$clear_screen_sub->();
|
|
my @display_lines = '';
|
|
|
|
if ( $key && $cfg_editor_action{$key} ) {
|
|
$cfg_editor_action{$key}->{func}->();
|
|
}
|
|
|
|
# Show help
|
|
push @display_lines, create_caption('What configuration do you want to edit?',
|
|
create_table2(
|
|
[ sort keys %cfg_editor_action ],
|
|
{ map { $_ => $_ } keys %cfg_editor_action },
|
|
{ map { $_ => $cfg_editor_action{$_}->{note} } keys %cfg_editor_action },
|
|
{ sep => ' ' }));
|
|
|
|
draw_screen(\@display_lines);
|
|
$key = pause('');
|
|
}
|
|
}
|
|
|
|
# edit_configuration_variables {{{3
|
|
sub edit_configuration_variables {
|
|
$clear_screen_sub->();
|
|
my $mode = $config{mode}->{val};
|
|
|
|
my %config_choices
|
|
= map { $_ => $config{$_}->{note} || '' }
|
|
# Only config values that are marked as applying to this mode.
|
|
grep {
|
|
my $key = $_;
|
|
$config{$key}->{conf} &&
|
|
( $config{$key}->{conf} eq 'ALL'
|
|
|| grep { $mode eq $_ } @{$config{$key}->{conf}} )
|
|
} keys %config;
|
|
|
|
my $key = prompt_list(
|
|
"Enter the name of the variable you wish to configure",
|
|
'',
|
|
sub{ return keys %config_choices },
|
|
\%config_choices);
|
|
|
|
if ( exists($config_choices{$key}) ) {
|
|
get_config_interactive($key);
|
|
}
|
|
}
|
|
|
|
# edit_color_rules {{{3
|
|
sub edit_color_rules {
|
|
my ( $tbl ) = @_;
|
|
$clear_screen_sub->();
|
|
$tbl ||= choose_visible_table();
|
|
if ( $tbl && exists($tbl_meta{$tbl}) ) {
|
|
my $meta = $tbl_meta{$tbl};
|
|
my @cols = ('', qw(col op arg color));
|
|
my $info = { map { $_ => { hdr => $_, just => '-', } } @cols };
|
|
$info->{label}->{maxw} = 30;
|
|
my $key;
|
|
my $selected_rule;
|
|
|
|
# This loop builds a tabular view of the rules.
|
|
do {
|
|
|
|
# Show help
|
|
if ( $key && $key eq '?' ) {
|
|
my @display_lines = '';
|
|
push @display_lines, create_caption('Editor key mappings',
|
|
create_table2(
|
|
[ sort keys %color_editor_action ],
|
|
{ map { $_ => $_ } keys %color_editor_action },
|
|
{ map { $_ => $color_editor_action{$_}->{note} } keys %color_editor_action },
|
|
{ sep => ' ' }));
|
|
draw_screen(\@display_lines);
|
|
pause();
|
|
$key = '';
|
|
}
|
|
else {
|
|
|
|
# Do the action specified
|
|
$selected_rule ||= 0;
|
|
if ( $key && $color_editor_action{$key} ) {
|
|
$selected_rule = $color_editor_action{$key}->{func}->($tbl, $selected_rule);
|
|
$selected_rule ||= 0;
|
|
}
|
|
|
|
# Build the table of rules. If the terminal has color, the selected rule
|
|
# will be highlighted; otherwise a > at the left will indicate.
|
|
my $data = $meta->{colors} || [];
|
|
foreach my $i ( 0..@$data - 1 ) {
|
|
$data->[$i]->{''} = $i == $selected_rule ? '>' : '';
|
|
}
|
|
my @display_lines = create_table(\@cols, $info, $data);
|
|
|
|
# Highlight selected entry
|
|
for my $i ( 0 .. $#display_lines ) {
|
|
if ( $display_lines[$i] =~ m/^>/ ) {
|
|
$display_lines[$i] = [ $display_lines[$i], 'reverse' ];
|
|
}
|
|
}
|
|
|
|
# Draw the screen and wait for a command.
|
|
unshift @display_lines, '',
|
|
"Editing color rules for $meta->{capt}. Press ? for help, q to "
|
|
. "quit.", '';
|
|
draw_screen(\@display_lines);
|
|
print "\n\n", word_wrap('Rules are applied in order from top to '
|
|
. 'bottom. The first matching rule wins and prevents the '
|
|
. 'rest of the rules from being applied.');
|
|
$key = pause('');
|
|
}
|
|
} while ( $key ne 'q' );
|
|
$meta->{color_func} = make_color_func($meta);
|
|
}
|
|
}
|
|
|
|
# add_quick_filter {{{3
|
|
sub add_quick_filter {
|
|
my $tbl = choose_visible_table();
|
|
if ( $tbl && exists($tbl_meta{$tbl}) ) {
|
|
print "\n";
|
|
my $response = prompt_list(
|
|
"Enter column name and filter text",
|
|
'',
|
|
sub { return keys %{$tbl_meta{$tbl}->{cols}} },
|
|
()
|
|
);
|
|
my ( $col, $text ) = split(/\s+/, $response, 2);
|
|
|
|
# You can't filter on a nonexistent column. But if you filter on a pivoted
|
|
# table, the columns are different, so on a pivoted table, allow filtering
|
|
# on the 'name' column.
|
|
# NOTE: if a table is pivoted and un-pivoted, this will likely cause crashes.
|
|
# Currently not an issue since there's no way to toggle pivot/nopivot.
|
|
return unless $col && $text &&
|
|
(exists($tbl_meta{$tbl}->{cols}->{$col})
|
|
|| ($tbl_meta{$tbl}->{pivot} && $col eq 'name'));
|
|
|
|
my ( $sub, $err ) = compile_filter( "defined \$set->{$col} && \$set->{$col} =~ m/$text/" );
|
|
return if !$sub || $err;
|
|
my $name = "quick_$tbl.$col";
|
|
$filters{$name} = {
|
|
func => $sub,
|
|
text => $text,
|
|
user => 1,
|
|
quick => 1,
|
|
name => $name,
|
|
note => 'Quick-filter',
|
|
tbls => [$tbl],
|
|
};
|
|
push @{$tbl_meta{$tbl}->{filters}}, $name;
|
|
}
|
|
}
|
|
|
|
# clear_quick_filters {{{3
|
|
sub clear_quick_filters {
|
|
my $tbl = choose_visible_table(
|
|
# Only tables that have quick-filters
|
|
sub {
|
|
my ( $tbl ) = @_;
|
|
return scalar grep { $filters{$_}->{quick} } @{ $tbl_meta{$tbl}->{filters} };
|
|
}
|
|
);
|
|
if ( $tbl && exists($tbl_meta{$tbl}) ) {
|
|
my @current = @{$tbl_meta{$tbl}->{filters}};
|
|
@current = grep { !$filters{$_}->{quick} } @current;
|
|
$tbl_meta{$tbl}->{filters} = \@current;
|
|
}
|
|
}
|
|
|
|
sub edit_plugins {
|
|
$clear_screen_sub->();
|
|
|
|
my @cols = ('', qw(class desc active));
|
|
my $info = { map { $_ => { hdr => $_, just => '-', } } @cols };
|
|
my @rows = map { $plugins{$_} } sort keys %plugins;
|
|
my $key;
|
|
my $selected;
|
|
|
|
# This loop builds a tabular view of the plugins.
|
|
do {
|
|
|
|
# Show help
|
|
if ( $key && $key eq '?' ) {
|
|
my @display_lines = '';
|
|
push @display_lines, create_caption('Editor key mappings',
|
|
create_table2(
|
|
[ sort keys %plugin_editor_action ],
|
|
{ map { $_ => $_ } keys %plugin_editor_action },
|
|
{ map { $_ => $plugin_editor_action{$_}->{note} } keys %plugin_editor_action },
|
|
{ sep => ' ' }));
|
|
draw_screen(\@display_lines);
|
|
pause();
|
|
$key = '';
|
|
}
|
|
|
|
# Do the action specified
|
|
else {
|
|
$selected ||= 0;
|
|
if ( $key && $plugin_editor_action{$key} ) {
|
|
$selected = $plugin_editor_action{$key}->{func}->(\@rows, $selected);
|
|
$selected ||= 0;
|
|
}
|
|
|
|
# Build the table of plugins.
|
|
foreach my $row ( 0.. $#rows ) {
|
|
$rows[$row]->{''} = $row eq $selected ? '>' : ' ';
|
|
}
|
|
my @display_lines = create_table(\@cols, $info, \@rows);
|
|
|
|
# Highlight selected entry
|
|
for my $i ( 0 .. $#display_lines ) {
|
|
if ( $display_lines[$i] =~ m/^>/ ) {
|
|
$display_lines[$i] = [ $display_lines[$i], 'reverse' ];
|
|
}
|
|
}
|
|
|
|
# Draw the screen and wait for a command.
|
|
unshift @display_lines, '',
|
|
"Plugin Management. Press ? for help, q to quit.", '';
|
|
draw_screen(\@display_lines);
|
|
$key = pause('');
|
|
}
|
|
} while ( $key ne 'q' );
|
|
}
|
|
|
|
# edit_table {{{3
|
|
sub edit_table {
|
|
$clear_screen_sub->();
|
|
my ( $tbl ) = @_;
|
|
$tbl ||= choose_visible_table();
|
|
if ( $tbl && exists($tbl_meta{$tbl}) ) {
|
|
my $meta = $tbl_meta{$tbl};
|
|
my @cols = ('', qw(name hdr label src));
|
|
my $info = { map { $_ => { hdr => $_, just => '-', } } @cols };
|
|
$info->{label}->{maxw} = 30;
|
|
my $key;
|
|
my $selected_column;
|
|
|
|
# This loop builds a tabular view of the tbl_meta's structure, showing each column
|
|
# in the entry as a row.
|
|
do {
|
|
|
|
# Show help
|
|
if ( $key && $key eq '?' ) {
|
|
my @display_lines = '';
|
|
push @display_lines, create_caption('Editor key mappings',
|
|
create_table2(
|
|
[ sort keys %tbl_editor_action ],
|
|
{ map { $_ => $_ } keys %tbl_editor_action },
|
|
{ map { $_ => $tbl_editor_action{$_}->{note} } keys %tbl_editor_action },
|
|
{ sep => ' ' }));
|
|
draw_screen(\@display_lines);
|
|
pause();
|
|
$key = '';
|
|
}
|
|
else {
|
|
|
|
# Do the action specified
|
|
$selected_column ||= $meta->{visible}->[0];
|
|
if ( $key && $tbl_editor_action{$key} ) {
|
|
$selected_column = $tbl_editor_action{$key}->{func}->($tbl, $selected_column);
|
|
$selected_column ||= $meta->{visible}->[0];
|
|
}
|
|
|
|
# Build the pivoted view of the table's meta-data. If the terminal has color,
|
|
# The selected row will be highlighted; otherwise a > at the left will indicate.
|
|
my $data = [];
|
|
foreach my $row ( @{$meta->{visible}} ) {
|
|
my %hash;
|
|
@hash{ @cols } = @{$meta->{cols}->{$row}}{@cols};
|
|
$hash{src} = '' if ref $hash{src};
|
|
$hash{name} = $row;
|
|
$hash{''} = $row eq $selected_column ? '>' : ' ';
|
|
push @$data, \%hash;
|
|
}
|
|
my @display_lines = create_table(\@cols, $info, $data);
|
|
|
|
# Highlight selected entry
|
|
for my $i ( 0 .. $#display_lines ) {
|
|
if ( $display_lines[$i] =~ m/^>/ ) {
|
|
$display_lines[$i] = [ $display_lines[$i], 'reverse' ];
|
|
}
|
|
}
|
|
|
|
# Draw the screen and wait for a command.
|
|
unshift @display_lines, '',
|
|
"Editing table definition for $meta->{capt}. Press ? for help, q to quit.", '';
|
|
draw_screen(\@display_lines, { clear => 1 });
|
|
$key = pause('');
|
|
}
|
|
} while ( $key ne 'q' );
|
|
}
|
|
}
|
|
|
|
# choose_mode_tables {{{3
|
|
# Choose which table(s), and in what order, to display in a given mode.
|
|
sub choose_mode_tables {
|
|
my $mode = $config{mode}->{val};
|
|
my @tbls = @{$modes{$mode}->{visible_tables}};
|
|
my $new = prompt_list(
|
|
"Choose tables to display",
|
|
join(' ', @tbls),
|
|
sub { return @{$modes{$mode}->{tables}} },
|
|
{ map { $_ => $tbl_meta{$_}->{capt} } @{$modes{$mode}->{tables}} }
|
|
);
|
|
$modes{$mode}->{visible_tables} =
|
|
[ unique(grep { $_ && exists $tbl_meta{$_} } split(/\s+/, $new)) ];
|
|
$modes{$mode}->{cust}->{visible_tables} = 1;
|
|
}
|
|
|
|
# choose_visible_table {{{3
|
|
sub choose_visible_table {
|
|
my ( $grep_cond ) = @_;
|
|
my $mode = $config{mode}->{val};
|
|
my @tbls
|
|
= grep { $grep_cond ? $grep_cond->($_) : 1 }
|
|
@{$modes{$mode}->{visible_tables}};
|
|
my $tbl = $tbls[0];
|
|
if ( @tbls > 1 ) {
|
|
$tbl = prompt_list(
|
|
"Choose a table",
|
|
'',
|
|
sub { return @tbls },
|
|
{ map { $_ => $tbl_meta{$_}->{capt} } @tbls }
|
|
);
|
|
}
|
|
return $tbl;
|
|
}
|
|
|
|
sub toggle_aggregate {
|
|
my ( $tbl ) = @_;
|
|
$tbl ||= choose_visible_table();
|
|
return unless $tbl && exists $tbl_meta{$tbl};
|
|
my $meta = $tbl_meta{$tbl};
|
|
$meta->{aggregate} ^= 1;
|
|
}
|
|
|
|
sub choose_filters {
|
|
my ( $tbl ) = @_;
|
|
$tbl ||= choose_visible_table();
|
|
return unless $tbl && exists $tbl_meta{$tbl};
|
|
my $meta = $tbl_meta{$tbl};
|
|
$clear_screen_sub->();
|
|
|
|
print "Choose filters for $meta->{capt}:\n";
|
|
|
|
my $ini = join(' ', @{$meta->{filters}});
|
|
my $val = prompt_list(
|
|
'Choose filters',
|
|
$ini,
|
|
sub { return keys %filters },
|
|
{
|
|
map { $_ => $filters{$_}->{note} }
|
|
grep { grep { $tbl eq $_ } @{$filters{$_}->{tbls}} }
|
|
keys %filters
|
|
}
|
|
);
|
|
|
|
my @choices = unique(split(/\s+/, $val));
|
|
foreach my $new ( grep { !exists($filters{$_}) } @choices ) {
|
|
my $answer = prompt("There is no filter called '$new'. Create it?", undef, 'y');
|
|
if ( $answer eq 'y' ) {
|
|
create_new_filter($new, $tbl);
|
|
}
|
|
}
|
|
@choices = grep { exists $filters{$_} } @choices;
|
|
@choices = grep { grep { $tbl eq $_ } @{$filters{$_}->{tbls}} } @choices;
|
|
$meta->{filters} = [ @choices ];
|
|
$meta->{cust}->{filters} = 1;
|
|
}
|
|
|
|
sub choose_group_cols {
|
|
my ( $tbl ) = @_;
|
|
$tbl ||= choose_visible_table();
|
|
return unless $tbl && exists $tbl_meta{$tbl};
|
|
$clear_screen_sub->();
|
|
my $meta = $tbl_meta{$tbl};
|
|
my $curr = join(', ', @{$meta->{group_by}});
|
|
my $val = prompt_list(
|
|
'Group-by columns',
|
|
$curr,
|
|
sub { return keys %{$meta->{cols}} },
|
|
{ map { $_ => $meta->{cols}->{$_}->{label} } keys %{$meta->{cols}} });
|
|
if ( $curr ne $val ) {
|
|
$meta->{group_by} = [ grep { exists $meta->{cols}->{$_} } $val =~ m/(\w+)/g ];
|
|
$meta->{cust}->{group_by} = 1;
|
|
}
|
|
}
|
|
|
|
sub choose_sort_cols {
|
|
my ( $tbl ) = @_;
|
|
$tbl ||= choose_visible_table();
|
|
return unless $tbl && exists $tbl_meta{$tbl};
|
|
$clear_screen_sub->();
|
|
my $meta = $tbl_meta{$tbl};
|
|
|
|
my ( $cols, $hints );
|
|
if ( $meta->{pivot} ) {
|
|
$cols = sub { qw(name set_0) };
|
|
$hints = { name => 'name', set_0 => 'set_0' };
|
|
}
|
|
else {
|
|
$cols = sub { return keys %{$meta->{cols}} };
|
|
$hints = { map { $_ => $meta->{cols}->{$_}->{label} } keys %{$meta->{cols}} };
|
|
}
|
|
|
|
my $val = prompt_list(
|
|
'Sort columns (reverse sort with -col)',
|
|
$meta->{sort_cols},
|
|
$cols,
|
|
$hints );
|
|
if ( $meta->{sort_cols} ne $val ) {
|
|
$meta->{sort_cols} = $val;
|
|
$meta->{cust}->{sort_cols} = 1;
|
|
$tbl_meta{$tbl}->{sort_func} = make_sort_func($tbl_meta{$tbl});
|
|
}
|
|
}
|
|
|
|
# create_new_filter {{{3
|
|
sub create_new_filter {
|
|
my ( $filter, $tbl ) = @_;
|
|
$clear_screen_sub->();
|
|
|
|
if ( !$filter || $filter =~ m/\W/ ) {
|
|
print word_wrap("Choose a name for the filter. This name is not displayed, and is only used "
|
|
. "for internal reference. It can only contain lowercase letters, numbers, and underscores.");
|
|
print "\n\n";
|
|
do {
|
|
$filter = prompt("Enter filter name");
|
|
} while ( !$filter || $filter =~ m/\W/ );
|
|
}
|
|
|
|
my $completion = sub { keys %{$tbl_meta{$tbl}->{cols}} };
|
|
my ( $err, $sub, $body );
|
|
do {
|
|
$clear_screen_sub->();
|
|
print word_wrap("A filter is a Perl subroutine that accepts a hashref of columns "
|
|
. "called \$set, and returns a true value if the filter accepts the row. Example:\n"
|
|
. " \$set->{active_secs} > 5\n"
|
|
. "will only allow rows if their active_secs column is greater than 5.");
|
|
print "\n\n";
|
|
if ( $err ) {
|
|
print "There's an error in your filter expression: $err\n\n";
|
|
}
|
|
$body = prompt("Enter subroutine body", undef, undef, $completion);
|
|
( $sub, $err ) = compile_filter($body);
|
|
} while ( $err );
|
|
|
|
$filters{$filter} = {
|
|
func => $sub,
|
|
text => $body,
|
|
user => 1,
|
|
name => $filter,
|
|
note => 'User-defined filter',
|
|
tbls => [$tbl],
|
|
};
|
|
}
|
|
|
|
# get_config_interactive {{{3
|
|
sub get_config_interactive {
|
|
my $key = shift;
|
|
$clear_screen_sub->();
|
|
|
|
# Print help first.
|
|
print "Enter a new value for '$key' ($config{$key}->{note}).\n";
|
|
|
|
my $current = ref($config{$key}->{val}) ? join(" ", @{$config{$key}->{val}}) : $config{$key}->{val};
|
|
|
|
my $new_value = prompt('Enter a value', $config{$key}->{pat}, $current);
|
|
$config{$key}->{val} = $new_value;
|
|
}
|
|
|
|
sub edit_current_var_set {
|
|
my $mode = $config{mode}->{val};
|
|
my $name = $config{"${mode}_set"}->{val};
|
|
my $variables = $var_sets{$name}->{text};
|
|
|
|
my $new = $variables;
|
|
do {
|
|
$clear_screen_sub->();
|
|
$new = prompt("Enter variables for $name", undef, $variables);
|
|
} until ( $new );
|
|
|
|
if ( $new ne $variables ) {
|
|
@{$var_sets{$name}}{qw(text user)} = ( $new, 1);
|
|
}
|
|
}
|
|
|
|
|
|
sub choose_var_set {
|
|
my ( $key ) = @_;
|
|
$clear_screen_sub->();
|
|
|
|
my $new_value = prompt_list(
|
|
'Choose a set of values to display, or enter the name of a new one',
|
|
$config{$key}->{val},
|
|
sub { return keys %var_sets },
|
|
{ map { $_ => $var_sets{$_}->{text} } keys %var_sets });
|
|
|
|
if ( !exists $var_sets{$new_value} ) {
|
|
add_new_var_set($new_value);
|
|
}
|
|
|
|
$config{$key}->{val} = $new_value if exists $var_sets{$new_value};
|
|
}
|
|
|
|
sub switch_var_set {
|
|
my ( $cfg_var, $dir ) = @_;
|
|
my @var_sets = sort keys %var_sets;
|
|
my $cur = $config{$cfg_var}->{val};
|
|
my $pos = grep { $_ lt $cur } @var_sets;
|
|
my $newpos = ($pos + $dir) % @var_sets;
|
|
$config{$cfg_var}->{val} = $var_sets[$newpos];
|
|
$clear_screen_sub->();
|
|
}
|
|
|
|
# Online configuration and prompting functions {{{2
|
|
|
|
# edit_stmt_sleep_times {{{3
|
|
sub edit_stmt_sleep_times {
|
|
$clear_screen_sub->();
|
|
my $stmt = prompt_list('Specify a statement', '', sub { return sort keys %stmt_maker_for });
|
|
return unless $stmt && exists $stmt_maker_for{$stmt};
|
|
$clear_screen_sub->();
|
|
my $curr_val = $stmt_sleep_time_for{$stmt} || 0;
|
|
my $new_val = prompt('Specify a sleep delay after calling this SQL', $num_regex, $curr_val);
|
|
if ( $new_val ) {
|
|
$stmt_sleep_time_for{$stmt} = $new_val;
|
|
}
|
|
else {
|
|
delete $stmt_sleep_time_for{$stmt};
|
|
}
|
|
}
|
|
|
|
# edit_server_groups {{{3
|
|
# Choose which server connections are in a server group. First choose a group,
|
|
# then choose which connections are in it.
|
|
sub edit_server_groups {
|
|
$clear_screen_sub->();
|
|
my $mode = $config{mode}->{val};
|
|
my $group = $modes{$mode}->{server_group};
|
|
my %curr = %server_groups;
|
|
my $new = choose_or_create_server_group($group, 'to edit');
|
|
$clear_screen_sub->();
|
|
if ( exists $curr{$new} ) {
|
|
# Don't do this step if the user just created a new server group,
|
|
# because part of that process was to choose connections.
|
|
my $cxns = join(' ', @{$server_groups{$new}});
|
|
my @conns = choose_or_create_connection($cxns, 'for this group');
|
|
$server_groups{$new} = \@conns;
|
|
}
|
|
}
|
|
|
|
# choose_server_groups {{{3
|
|
sub choose_server_groups {
|
|
$clear_screen_sub->();
|
|
my $mode = $config{mode}->{val};
|
|
my $group = $modes{$mode}->{server_group};
|
|
my $new = choose_or_create_server_group($group, 'for this mode');
|
|
$modes{$mode}->{server_group} = $new if exists $server_groups{$new};
|
|
}
|
|
|
|
sub choose_or_create_server_group {
|
|
my ( $group, $prompt ) = @_;
|
|
my $new = '';
|
|
|
|
my @available = sort keys %server_groups;
|
|
|
|
if ( @available ) {
|
|
print "You can enter the name of a new group to create it.\n";
|
|
|
|
$new = prompt_list(
|
|
"Choose a server group $prompt",
|
|
$group,
|
|
sub { return @available },
|
|
{ map { $_ => join(' ', @{$server_groups{$_}}) } @available });
|
|
|
|
$new =~ s/\s.*//;
|
|
|
|
if ( !exists $server_groups{$new} ) {
|
|
my $answer = prompt("There is no server group called '$new'. Create it?", undef, "y");
|
|
if ( $answer eq 'y' ) {
|
|
add_new_server_group($new);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$new = add_new_server_group();
|
|
}
|
|
return $new;
|
|
}
|
|
|
|
sub choose_or_create_connection {
|
|
my ( $cxns, $prompt ) = @_;
|
|
print "You can enter the name of a new connection to create it.\n";
|
|
|
|
my @available = sort keys %connections;
|
|
my $new_cxns = prompt_list(
|
|
"Choose connections $prompt",
|
|
$cxns,
|
|
sub { return @available },
|
|
{ map { $_ => $connections{$_}->{dsn} } @available });
|
|
|
|
my @new = unique(grep { !exists $connections{$_} } split(/\s+/, $new_cxns));
|
|
foreach my $new ( @new ) {
|
|
my $answer = prompt("There is no connection called '$new'. Create it?", undef, "y");
|
|
if ( $answer eq 'y' ) {
|
|
add_new_dsn($new);
|
|
}
|
|
}
|
|
|
|
return unique(grep { exists $connections{$_} } split(/\s+/, $new_cxns));
|
|
}
|
|
|
|
# choose_servers {{{3
|
|
sub choose_servers {
|
|
$clear_screen_sub->();
|
|
my $mode = $config{mode}->{val};
|
|
my $cxns = join(' ', get_connections());
|
|
my @chosen = choose_or_create_connection($cxns, 'for this mode');
|
|
$modes{$mode}->{connections} = \@chosen;
|
|
$modes{$mode}->{server_group} = ''; # Clear this because it overrides {connections}
|
|
}
|
|
|
|
# display_license {{{3
|
|
sub display_license {
|
|
$clear_screen_sub->();
|
|
|
|
print $innotop_license;
|
|
|
|
pause();
|
|
}
|
|
|
|
# Data-retrieval functions {{{2
|
|
# get_status_info {{{3
|
|
# Get SHOW STATUS and SHOW VARIABLES together.
|
|
sub get_status_info {
|
|
my @cxns = @_;
|
|
if ( !$info_gotten{status}++ ) {
|
|
foreach my $cxn ( @cxns ) {
|
|
$vars{$cxn}->{$clock} ||= {};
|
|
my $vars = $vars{$cxn}->{$clock};
|
|
|
|
my $sth = do_stmt($cxn, 'SHOW_STATUS') or next;
|
|
my $res = $sth->fetchall_arrayref();
|
|
map { $vars->{$_->[0]} = $_->[1] || 0 } @$res;
|
|
|
|
# Calculate hi-res uptime and add cxn to the hash. This duplicates get_driver_status,
|
|
# but it's most important to have consistency.
|
|
$vars->{Uptime_hires} ||= get_uptime($cxn);
|
|
$vars->{cxn} = $cxn;
|
|
|
|
# Add SHOW VARIABLES to the hash
|
|
$sth = do_stmt($cxn, 'SHOW_VARIABLES') or next;
|
|
$res = $sth->fetchall_arrayref();
|
|
map { $vars->{$_->[0]} = $_->[1] || 0 } @$res;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Chooses a thread for explaining, killing, etc...
|
|
# First arg is a func that can be called in grep.
|
|
sub choose_thread {
|
|
my ( $grep_cond, $prompt ) = @_;
|
|
|
|
# Narrow the list to queries that can be explained.
|
|
my %thread_for = map {
|
|
# Eliminate innotop's own threads.
|
|
$_ => $dbhs{$_}->{dbh} ? $dbhs{$_}->{dbh}->{mysql_thread_id} : 0
|
|
} keys %connections;
|
|
|
|
my @candidates = grep {
|
|
$_->{id} != $thread_for{$_->{cxn}} && $grep_cond->($_)
|
|
} @current_queries;
|
|
return unless @candidates;
|
|
|
|
# Find out which server.
|
|
my @cxns = unique map { $_->{cxn} } @candidates;
|
|
my ( $cxn ) = select_cxn('On which server', @cxns);
|
|
return unless $cxn && exists($connections{$cxn});
|
|
|
|
# Re-filter the list of candidates to only those on this server
|
|
@candidates = grep { $_->{cxn} eq $cxn } @candidates;
|
|
|
|
# Find out which thread to do.
|
|
my $info;
|
|
if ( @candidates > 1 ) {
|
|
|
|
# Sort longest-active first, then longest-idle.
|
|
my $sort_func = sub {
|
|
my ( $a, $b ) = @_;
|
|
return $a->{query} && !$b->{query} ? 1
|
|
: $b->{query} && !$a->{query} ? -1
|
|
: ($a->{time} || 0) <=> ($b->{time} || 0);
|
|
};
|
|
my @threads = map { $_->{id} } reverse sort { $sort_func->($a, $b) } @candidates;
|
|
|
|
print "\n";
|
|
my $thread = prompt_list($prompt,
|
|
$threads[0],
|
|
sub { return @threads });
|
|
return unless $thread && $thread =~ m/$int_regex/;
|
|
|
|
# Find the info hash of that query on that server.
|
|
( $info ) = grep { $thread == $_->{id} } @candidates;
|
|
}
|
|
else {
|
|
$info = $candidates[0];
|
|
}
|
|
return $info;
|
|
}
|
|
|
|
# analyze_query {{{3
|
|
# Allows the user to show fulltext, explain, show optimized...
|
|
sub analyze_query {
|
|
my ( $action ) = @_;
|
|
|
|
my $info = choose_thread(
|
|
sub { $_[0]->{query} },
|
|
'Select a thread to analyze',
|
|
);
|
|
return unless $info;
|
|
|
|
my %actions = (
|
|
e => \&display_explain,
|
|
f => \&show_full_query,
|
|
o => \&show_optimized_query,
|
|
);
|
|
do {
|
|
$actions{$action}->($info);
|
|
print "\n";
|
|
$action = pause('Press e to explain, f for full query, o for optimized query');
|
|
} while ( exists($actions{$action}) );
|
|
}
|
|
|
|
# inc {{{3
|
|
# Returns the difference between two sets of variables/status/innodb stuff.
|
|
sub inc {
|
|
my ( $offset, $cxn ) = @_;
|
|
my $vars = $vars{$cxn};
|
|
if ( $offset < 0 ) {
|
|
return $vars->{$clock};
|
|
}
|
|
elsif ( exists $vars{$clock - $offset} && !exists $vars->{$clock - $offset - 1} ) {
|
|
return $vars->{$clock - $offset};
|
|
}
|
|
my $cur = $vars->{$clock - $offset};
|
|
my $pre = $vars->{$clock - $offset - 1};
|
|
return {
|
|
# Numeric variables get subtracted, non-numeric get passed straight through.
|
|
map {
|
|
$_ =>
|
|
( (defined $cur->{$_} && $cur->{$_} =~ m/$num_regex/)
|
|
? $cur->{$_} - ($pre->{$_} || 0)
|
|
: $cur->{$_} )
|
|
} keys %{$cur}
|
|
};
|
|
}
|
|
|
|
# extract_values {{{3
|
|
# Arguments are a set of values (which may be incremental, derived from
|
|
# current and previous), current, and previous values.
|
|
# TODO: there are a few places that don't remember prev set so can't pass it.
|
|
sub extract_values {
|
|
my ( $set, $cur, $pre, $tbl ) = @_;
|
|
|
|
# Hook in event listeners
|
|
foreach my $listener ( @{$event_listener_for{extract_values}} ) {
|
|
$listener->extract_values($set, $cur, $pre, $tbl);
|
|
}
|
|
|
|
my $result = {};
|
|
my $meta = $tbl_meta{$tbl};
|
|
my $cols = $meta->{cols};
|
|
foreach my $key ( keys %$cols ) {
|
|
my $info = $cols->{$key}
|
|
or die "Column '$key' doesn't exist in $tbl";
|
|
die "No func defined for '$key' in $tbl"
|
|
unless $info->{func};
|
|
eval {
|
|
$result->{$key} = $info->{func}->($set, $cur, $pre)
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
if ( $config{debug}->{val} ) {
|
|
die $EVAL_ERROR;
|
|
}
|
|
$result->{$key} = $info->{num} ? 0 : '';
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
# get_full_processlist {{{3
|
|
sub get_full_processlist {
|
|
my @cxns = @_;
|
|
my @result;
|
|
foreach my $cxn ( @cxns ) {
|
|
my $stmt = do_stmt($cxn, 'PROCESSLIST') or next;
|
|
my $arr = $stmt->fetchall_arrayref({});
|
|
push @result, map { $_->{cxn} = $cxn; $_ } @$arr;
|
|
}
|
|
return @result;
|
|
}
|
|
|
|
# get_open_tables {{{3
|
|
sub get_open_tables {
|
|
my @cxns = @_;
|
|
my @result;
|
|
foreach my $cxn ( @cxns ) {
|
|
my $stmt = do_stmt($cxn, 'OPEN_TABLES') or next;
|
|
my $arr = $stmt->fetchall_arrayref({});
|
|
push @result, map { $_->{cxn} = $cxn; $_ } @$arr;
|
|
}
|
|
return @result;
|
|
}
|
|
|
|
# get_innodb_status {{{3
|
|
sub get_innodb_status {
|
|
my ( $cxns, $addl_sections ) = @_;
|
|
if ( !$config{skip_innodb}->{val} && !$info_gotten{innodb_status}++ ) {
|
|
|
|
# Determine which sections need to be parsed
|
|
my %sections_required =
|
|
map { $tbl_meta{$_}->{innodb} => 1 }
|
|
grep { $_ && $tbl_meta{$_}->{innodb} }
|
|
get_visible_tables();
|
|
|
|
# Add in any other sections the caller requested.
|
|
foreach my $sec ( @$addl_sections ) {
|
|
$sections_required{$sec} = 1;
|
|
}
|
|
|
|
foreach my $cxn ( @$cxns ) {
|
|
my $innodb_status_text;
|
|
|
|
if ( $file ) { # Try to fetch status text from the file.
|
|
my @stat = stat($file);
|
|
|
|
# Initialize the file.
|
|
if ( !$file_mtime ) {
|
|
# Initialize to 130k from the end of the file (because the limit
|
|
# on the size of innodb status is 128k even with Google's patches)
|
|
# and try to grab the last status from the file.
|
|
sysseek($file, (-128 * 1_024), 2);
|
|
}
|
|
|
|
# Read from the file.
|
|
my $buffer;
|
|
if ( !$file_mtime || $file_mtime != $stat[9] ) {
|
|
$file_data = '';
|
|
while ( sysread($file, $buffer, 4096) ) {
|
|
$file_data .= $buffer;
|
|
}
|
|
$file_mtime = $stat[9];
|
|
}
|
|
|
|
# Delete everything but the last InnoDB status text from the file.
|
|
$file_data =~ s/\A.*(?=^=====================================\n...... ........ INNODB MONITOR OUTPUT)//ms;
|
|
$innodb_status_text = $file_data;
|
|
}
|
|
|
|
else {
|
|
my $stmt = do_stmt($cxn, 'INNODB_STATUS') or next;
|
|
$innodb_status_text = $stmt->fetchrow_hashref()->{status};
|
|
}
|
|
|
|
next unless $innodb_status_text
|
|
&& substr($innodb_status_text, 0, 100) =~ m/INNODB MONITOR OUTPUT/;
|
|
|
|
# Parse and merge into %vars storage
|
|
my %innodb_status = (
|
|
$innodb_parser->get_status_hash(
|
|
$innodb_status_text,
|
|
$config{debug}->{val},
|
|
\%sections_required,
|
|
0, # don't parse full lock information
|
|
)
|
|
);
|
|
if ( !$innodb_status{IB_got_all} && $config{auto_wipe_dl}->{val} ) {
|
|
clear_deadlock($cxn);
|
|
}
|
|
|
|
# Merge using a hash slice, which is the fastest way
|
|
$vars{$cxn}->{$clock} ||= {};
|
|
my $hash = $vars{$cxn}->{$clock};
|
|
@{$hash}{ keys %innodb_status } = values %innodb_status;
|
|
$hash->{cxn} = $cxn;
|
|
$hash->{Uptime_hires} ||= get_uptime($cxn);
|
|
}
|
|
}
|
|
}
|
|
|
|
# clear_deadlock {{{3
|
|
sub clear_deadlock {
|
|
my ( $cxn ) = @_;
|
|
return if $clearing_deadlocks++;
|
|
my $tbl = $connections{$cxn}->{dl_table};
|
|
return unless $tbl;
|
|
|
|
eval {
|
|
# Set up the table for creating a deadlock.
|
|
my $engine = version_ge($dbhs{$cxn}->{dbh}, '4.1.2') ? 'engine' : 'type';
|
|
return unless do_query($cxn, "drop table if exists $tbl");
|
|
return unless do_query($cxn, "create table $tbl(a int) $engine=innodb");
|
|
return unless do_query($cxn, "delete from $tbl");
|
|
return unless do_query($cxn, "insert into $tbl(a) values(0), (1)");
|
|
return unless do_query($cxn, "commit"); # Or the children will block against the parent
|
|
|
|
# Fork off two children to deadlock against each other.
|
|
my %children;
|
|
foreach my $child ( 0..1 ) {
|
|
my $pid = fork();
|
|
if ( defined($pid) && $pid == 0 ) { # I am a child
|
|
deadlock_thread( $child, $tbl, $cxn );
|
|
}
|
|
elsif ( !defined($pid) ) {
|
|
die("Unable to fork for clearing deadlocks!\n");
|
|
}
|
|
# I already exited if I'm a child, so I'm the parent.
|
|
$children{$child} = $pid;
|
|
}
|
|
|
|
# Wait for the children to exit.
|
|
foreach my $child ( keys %children ) {
|
|
my $pid = waitpid($children{$child}, 0);
|
|
}
|
|
|
|
# Clean up.
|
|
do_query($cxn, "drop table $tbl");
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
print $EVAL_ERROR;
|
|
pause();
|
|
}
|
|
|
|
$clearing_deadlocks = 0;
|
|
}
|
|
|
|
sub get_master_logs {
|
|
my @cxns = @_;
|
|
my @result;
|
|
if ( !$info_gotten{master_logs}++ ) {
|
|
foreach my $cxn ( @cxns ) {
|
|
my $stmt = do_stmt($cxn, 'SHOW_MASTER_LOGS') or next;
|
|
push @result, @{$stmt->fetchall_arrayref({})};
|
|
}
|
|
}
|
|
return @result;
|
|
}
|
|
|
|
# get_master_slave_status {{{3
|
|
sub get_master_slave_status {
|
|
my @cxns = @_;
|
|
if ( !$info_gotten{replication_status}++ ) {
|
|
foreach my $cxn ( @cxns ) {
|
|
$vars{$cxn}->{$clock} ||= {};
|
|
my $vars = $vars{$cxn}->{$clock};
|
|
$vars->{cxn} = $cxn;
|
|
|
|
my $stmt = do_stmt($cxn, 'SHOW_MASTER_STATUS') or next;
|
|
my $res = $stmt->fetchall_arrayref({})->[0];
|
|
@{$vars}{ keys %$res } = values %$res;
|
|
$stmt = do_stmt($cxn, 'SHOW_SLAVE_STATUS') or next;
|
|
$res = $stmt->fetchall_arrayref({})->[0];
|
|
@{$vars}{ keys %$res } = values %$res;
|
|
$vars->{Uptime_hires} ||= get_uptime($cxn);
|
|
}
|
|
}
|
|
}
|
|
|
|
sub is_func {
|
|
my ( $word ) = @_;
|
|
return defined(&$word)
|
|
|| eval "my \$x= sub { $word }; 1"
|
|
|| $EVAL_ERROR !~ m/^Bareword/;
|
|
}
|
|
|
|
# Documentation {{{1
|
|
# ############################################################################
|
|
# I put this last as per the Dog book.
|
|
# ############################################################################
|
|
=pod
|
|
|
|
=head1 NAME
|
|
|
|
innotop - MySQL and InnoDB transaction/status monitor.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
To monitor servers normally:
|
|
|
|
innotop
|
|
|
|
To monitor InnoDB status information from a file:
|
|
|
|
innotop /var/log/mysql/mysqld.err
|
|
|
|
To run innotop non-interactively in a pipe-and-filter configuration:
|
|
|
|
innotop --count 5 -d 1 -n
|
|
|
|
To monitor a database on another system using a particular username and password:
|
|
|
|
innotop -u <username> -p <password> -h <hostname>
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
innotop monitors MySQL servers. Each of its modes shows you a different aspect
|
|
of what's happening in the server. For example, there's a mode for monitoring
|
|
replication, one for queries, and one for transactions. innotop refreshes its
|
|
data periodically, so you see an updating view.
|
|
|
|
innotop has lots of features for power users, but you can start and run it with
|
|
virtually no configuration. If you're just getting started, see
|
|
L<"QUICK-START">. Press '?' at any time while running innotop for
|
|
context-sensitive help.
|
|
|
|
=head1 QUICK-START
|
|
|
|
To start innotop, open a terminal or command prompt. If you have installed
|
|
innotop on your system, you should be able to just type "innotop" and press
|
|
Enter; otherwise, you will need to change to innotop's directory and type "perl
|
|
innotop".
|
|
|
|
With no options specified, innotop will attempt to connect to a MySQL server on
|
|
localhost using mysql_read_default_group=client for other connection
|
|
parameters. If you need to specify a different username and password, use the
|
|
-u and -p options, respectively. To monitor a MySQL database on another
|
|
host, use the -h option.
|
|
|
|
After you've connected, innotop should show you something like the following:
|
|
|
|
[RO] Query List (? for help) localhost, 01:11:19, 449.44 QPS, 14/7/163 con/run
|
|
|
|
CXN When Load QPS Slow QCacheHit KCacheHit BpsIn BpsOut
|
|
localhost Total 0.00 1.07k 697 0.00% 98.17% 476.83k 242.83k
|
|
|
|
CXN Cmd ID User Host DB Time Query
|
|
localhost Query 766446598 test 10.0.0.1 foo 00:02 INSERT INTO table (
|
|
|
|
|
|
(This sample is truncated at the right so it will fit on a terminal when running
|
|
'man innotop')
|
|
|
|
If your server is busy, you'll see more output. Notice the first line on the
|
|
screen, which tells you that readonly is set to true ([RO]), what mode you're
|
|
in and what server you're connected to. You can change to other modes with
|
|
keystrokes; press 'T' to switch to a list of InnoDB transactions, for example.
|
|
|
|
Press the '?' key to see what keys are active in the current mode. You can
|
|
press any of these keys and innotop will either take the requested action or
|
|
prompt you for more input. If your system has Term::ReadLine support, you can
|
|
use TAB and other keys to auto-complete and edit input.
|
|
|
|
To quit innotop, press the 'q' key.
|
|
|
|
=head1 OPTIONS
|
|
|
|
innotop is mostly configured via its configuration file, but some of the
|
|
configuration options can come from the command line. You can also specify a
|
|
file to monitor for InnoDB status output; see L<"MONITORING A FILE"> for more
|
|
details.
|
|
|
|
You can negate some options by prefixing the option name with --no. For
|
|
example, --noinc (or --no-inc) negates L<"--inc">.
|
|
|
|
=over
|
|
|
|
=item --color
|
|
|
|
Enable or disable terminal coloring. Corresponds to the L<"color"> config file
|
|
setting.
|
|
|
|
=item --config
|
|
|
|
Specifies a configuration file to read. This option is non-sticky, that is to
|
|
say it does not persist to the configuration file itself.
|
|
|
|
=item --count
|
|
|
|
Refresh only the specified number of times (ticks) before exiting. Each refresh
|
|
is a pause for L<"interval"> seconds, followed by requesting data from MySQL
|
|
connections and printing it to the terminal.
|
|
|
|
=item --delay
|
|
|
|
Specifies the amount of time to pause between ticks (refreshes). Corresponds to
|
|
the configuration option L<"interval">.
|
|
|
|
=item --help
|
|
|
|
Print a summary of command-line usage and exit.
|
|
|
|
=item --host
|
|
|
|
Host to connect to.
|
|
|
|
=item --inc
|
|
|
|
Specifies whether innotop should display absolute numbers or relative numbers
|
|
(offsets from their previous values). Corresponds to the configuration option
|
|
L<"status_inc">.
|
|
|
|
=item --mode
|
|
|
|
Specifies the mode in which innotop should start. Corresponds to the
|
|
configuration option L<"mode">.
|
|
|
|
=item --nonint
|
|
|
|
Enable non-interactive operation. See L<"NON-INTERACTIVE OPERATION"> for more.
|
|
|
|
=item --password
|
|
|
|
Password to use for connection.
|
|
|
|
=item --port
|
|
|
|
Port to use for connection.
|
|
|
|
=item --skipcentral
|
|
|
|
Don't read the central configuration file.
|
|
|
|
=item --user
|
|
|
|
User to use for connection.
|
|
|
|
=item --version
|
|
|
|
Output version information and exit.
|
|
|
|
=item --write
|
|
|
|
Sets the configuration option L<"readonly"> to 0, making innotop write the
|
|
running configuration to ~/.innotop/innotop.conf on exit, if no configuration
|
|
file was loaded at start-up.
|
|
|
|
=back
|
|
|
|
=head1 HOTKEYS
|
|
|
|
innotop is interactive, and you control it with key-presses.
|
|
|
|
=over
|
|
|
|
=item *
|
|
|
|
Uppercase keys switch between modes.
|
|
|
|
=item *
|
|
|
|
Lowercase keys initiate some action within the current mode.
|
|
|
|
=item *
|
|
|
|
Other keys do something special like change configuration or show the
|
|
innotop license.
|
|
|
|
=back
|
|
|
|
Press '?' at any time to see the currently active keys and what they do.
|
|
|
|
=head1 MODES
|
|
|
|
Each of innotop's modes retrieves and displays a particular type of data from
|
|
the servers you're monitoring. You switch between modes with uppercase keys.
|
|
The following is a brief description of each mode, in alphabetical order. To
|
|
switch to the mode, press the key listed in front of its heading in the
|
|
following list:
|
|
|
|
=over
|
|
|
|
=item B: InnoDB Buffers
|
|
|
|
This mode displays information about the InnoDB buffer pool, page statistics,
|
|
insert buffer, and adaptive hash index. The data comes from SHOW INNODB STATUS.
|
|
|
|
This mode contains the L<"buffer_pool">, L<"page_statistics">,
|
|
L<"insert_buffers">, and L<"adaptive_hash_index"> tables by default.
|
|
|
|
=item C: Command Summary
|
|
|
|
This mode is similar to mytop's Command Summary mode. It shows the
|
|
L<"cmd_summary"> table, which looks something like the following:
|
|
|
|
Command Summary (? for help) localhost, 25+07:16:43, 2.45 QPS, 3 thd, 5.0.40
|
|
_____________________ Command Summary _____________________
|
|
Name Value Pct Last Incr Pct
|
|
Select_scan 3244858 69.89% 2 100.00%
|
|
Select_range 1354177 29.17% 0 0.00%
|
|
Select_full_join 39479 0.85% 0 0.00%
|
|
Select_full_range_join 4097 0.09% 0 0.00%
|
|
Select_range_check 0 0.00% 0 0.00%
|
|
|
|
The command summary table is built by extracting variables from
|
|
L<"STATUS_VARIABLES">. The variables must be numeric and must match the prefix
|
|
given by the L<"cmd_filter"> configuration variable. The variables are then
|
|
sorted by value descending and compared to the last variable, as shown above.
|
|
The percentage columns are percentage of the total of all variables in the
|
|
table, so you can see the relative weight of the variables.
|
|
|
|
The example shows what you see if the prefix is "Select_". The default
|
|
prefix is "Com_". You can choose a prefix with the 's' key.
|
|
|
|
It's rather like running SHOW VARIABLES LIKE "prefix%" with memory and
|
|
nice formatting.
|
|
|
|
Values are aggregated across all servers. The Pct columns are not correctly
|
|
aggregated across multiple servers. This is a known limitation of the grouping
|
|
algorithm that may be fixed in the future.
|
|
|
|
=item D: InnoDB Deadlocks
|
|
|
|
This mode shows the transactions involved in the last InnoDB deadlock. A second
|
|
table shows the locks each transaction held and waited for. A deadlock is
|
|
caused by a cycle in the waits-for graph, so there should be two locks held and
|
|
one waited for unless the deadlock information is truncated.
|
|
|
|
InnoDB puts deadlock information before some other information in the SHOW
|
|
INNODB STATUS output. If there are a lot of locks, the deadlock information can
|
|
grow very large, and there is a limit on the size of the SHOW INNODB
|
|
STATUS output. A large deadlock can fill the entire output, or even be
|
|
truncated, and prevent you from seeing other information at all. If you are
|
|
running innotop in another mode, for example T mode, and suddenly you don't see
|
|
anything, you might want to check and see if a deadlock has wiped out the data
|
|
you need.
|
|
|
|
If it has, you can create a small deadlock to replace the large one. Use the
|
|
'w' key to 'wipe' the large deadlock with a small one. This will not work
|
|
unless you have defined a deadlock table for the connection (see L<"SERVER
|
|
CONNECTIONS">).
|
|
|
|
You can also configure innotop to automatically detect when a large deadlock
|
|
needs to be replaced with a small one (see L<"auto_wipe_dl">).
|
|
|
|
This mode displays the L<"deadlock_transactions"> and L<"deadlock_locks"> tables
|
|
by default.
|
|
|
|
=item F: InnoDB Foreign Key Errors
|
|
|
|
This mode shows the last InnoDB foreign key error information, such as the
|
|
table where it happened, when and who and what query caused it, and so on.
|
|
|
|
InnoDB has a huge variety of foreign key error messages, and many of them are
|
|
just hard to parse. innotop doesn't always do the best job here, but there's
|
|
so much code devoted to parsing this messy, unparseable output that innotop is
|
|
likely never to be perfect in this regard. If innotop doesn't show you what
|
|
you need to see, just look at the status text directly.
|
|
|
|
This mode displays the L<"fk_error"> table by default.
|
|
|
|
=item I: InnoDB I/O Info
|
|
|
|
This mode shows InnoDB's I/O statistics, including the I/O threads, pending I/O,
|
|
file I/O miscellaneous, and log statistics. It displays the L<"io_threads">,
|
|
L<"pending_io">, L<"file_io_misc">, and L<"log_statistics"> tables by default.
|
|
|
|
=item L: Locks
|
|
|
|
This mode shows information about current locks. At the moment only InnoDB
|
|
locks are supported, and by default you'll only see locks for which transactions
|
|
are waiting. This information comes from the TRANSACTIONS section of the InnoDB
|
|
status text. If you have a very busy server, you may have frequent lock waits;
|
|
it helps to be able to see which tables and indexes are the "hot spot" for
|
|
locks. If your server is running pretty well, this mode should show nothing.
|
|
|
|
You can configure MySQL and innotop to monitor not only locks for which a
|
|
transaction is waiting, but those currently held, too. You can do this with the
|
|
InnoDB Lock Monitor (L<http://dev.mysql.com/doc/en/innodb-monitor.html>). It's
|
|
not documented in the MySQL manual, but creating the lock monitor with the
|
|
following statement also affects the output of SHOW INNODB STATUS, which innotop
|
|
uses:
|
|
|
|
CREATE TABLE innodb_lock_monitor(a int) ENGINE=INNODB;
|
|
|
|
This causes InnoDB to print its output to the MySQL file every 16 seconds or so,
|
|
as stated in the manual, but it also makes the normal SHOW INNODB STATUS output
|
|
include lock information, which innotop can parse and display (that's the
|
|
undocumented feature).
|
|
|
|
This means you can do what may have seemed impossible: to a limited extent
|
|
(InnoDB truncates some information in the output), you can see which transaction
|
|
holds the locks something else is waiting for. You can also enable and disable
|
|
the InnoDB Lock Monitor with the key mappings in this mode.
|
|
|
|
This mode displays the L<"innodb_locks"> table by default. Here's a sample of
|
|
the screen when one connection is waiting for locks another connection holds:
|
|
|
|
_________________________________ InnoDB Locks __________________________
|
|
CXN ID Type Waiting Wait Active Mode DB Table Index
|
|
localhost 12 RECORD 1 00:10 00:10 X test t1 PRIMARY
|
|
localhost 12 TABLE 0 00:10 00:10 IX test t1
|
|
localhost 12 RECORD 1 00:10 00:10 X test t1 PRIMARY
|
|
localhost 11 TABLE 0 00:00 00:25 IX test t1
|
|
localhost 11 RECORD 0 00:00 00:25 X test t1 PRIMARY
|
|
|
|
You can see the first connection, ID 12, is waiting for a lock on the PRIMARY
|
|
key on test.t1, and has been waiting for 10 seconds. The second connection
|
|
isn't waiting, because the Waiting column is 0, but it holds locks on the same
|
|
index. That tells you connection 11 is blocking connection 12.
|
|
|
|
=item M: Master/Slave Replication Status
|
|
|
|
This mode shows the output of SHOW SLAVE STATUS and SHOW MASTER STATUS in three
|
|
tables. The first two divide the slave's status into SQL and I/O thread status,
|
|
and the last shows master status. Filters are applied to eliminate non-slave
|
|
servers from the slave tables, and non-master servers from the master table.
|
|
|
|
This mode displays the L<"slave_sql_status">, L<"slave_io_status">, and
|
|
L<"master_status"> tables by default.
|
|
|
|
=item O: Open Tables
|
|
|
|
This section comes from MySQL's SHOW OPEN TABLES command. By default it is
|
|
filtered to show tables which are in use by one or more queries, so you can
|
|
get a quick look at which tables are 'hot'. You can use this to guess which
|
|
tables might be locked implicitly.
|
|
|
|
This mode displays the L<"open_tables"> mode by default.
|
|
|
|
=item Q: Query List
|
|
|
|
This mode displays the output from SHOW FULL PROCESSLIST, much like B<mytop>'s
|
|
query list mode. This mode does B<not> show InnoDB-related information. This
|
|
is probably one of the most useful modes for general usage.
|
|
|
|
There is an informative header that shows general status information about
|
|
your server. You can toggle it on and off with the 'h' key. By default,
|
|
innotop hides inactive processes and its own process. You can toggle these on
|
|
and off with the 'i' and 'a' keys.
|
|
|
|
You can EXPLAIN a query from this mode with the 'e' key. This displays the
|
|
query's full text, the results of EXPLAIN, and in newer MySQL versions, even
|
|
the optimized query resulting from EXPLAIN EXTENDED. innotop also tries to
|
|
rewrite certain queries to make them EXPLAIN-able. For example, INSERT/SELECT
|
|
statements are rewritable.
|
|
|
|
This mode displays the L<"q_header"> and L<"processlist"> tables by default.
|
|
|
|
=item R: InnoDB Row Operations and Semaphores
|
|
|
|
This mode shows InnoDB row operations, row operation miscellaneous, semaphores,
|
|
and information from the wait array. It displays the L<"row_operations">,
|
|
L<"row_operation_misc">, L<"semaphores">, and L<"wait_array"> tables by default.
|
|
|
|
=item S: Variables & Status
|
|
|
|
This mode calculates statistics, such as queries per second, and prints them out
|
|
in several different styles. You can show absolute values, or incremental values
|
|
between ticks.
|
|
|
|
You can switch between the views by pressing a key. The 's' key prints a
|
|
single line each time the screen updates, in the style of B<vmstat>. The 'g'
|
|
key changes the view to a graph of the same numbers, sort of like B<tload>.
|
|
The 'v' key changes the view to a pivoted table of variable names on the left,
|
|
with successive updates scrolling across the screen from left to right. You can
|
|
choose how many updates to put on the screen with the L<"num_status_sets">
|
|
configuration variable.
|
|
|
|
Headers may be abbreviated to fit on the screen in interactive operation. You
|
|
choose which variables to display with the 'c' key, which selects from
|
|
predefined sets, or lets you create your own sets. You can edit the current set
|
|
with the 'e' key.
|
|
|
|
This mode doesn't really display any tables like other modes. Instead, it uses
|
|
a table definition to extract and format the data, but it then transforms the
|
|
result in special ways before outputting it. It uses the L<"var_status"> table
|
|
definition for this.
|
|
|
|
=item T: InnoDB Transactions
|
|
|
|
This mode shows transactions from the InnoDB monitor's output, in B<top>-like
|
|
format. This mode is the reason I wrote innotop.
|
|
|
|
You can kill queries or processes with the 'k' and 'x' keys, and EXPLAIN a query
|
|
with the 'e' or 'f' keys. InnoDB doesn't print the full query in transactions,
|
|
so explaining may not work right if the query is truncated.
|
|
|
|
The informational header can be toggled on and off with the 'h' key. By
|
|
default, innotop hides inactive transactions and its own transaction. You can
|
|
toggle this on and off with the 'i' and 'a' keys.
|
|
|
|
This mode displays the L<"t_header"> and L<"innodb_transactions"> tables by
|
|
default.
|
|
|
|
=back
|
|
|
|
=head1 INNOTOP STATUS
|
|
|
|
The first line innotop displays is a "status bar" of sorts. What it contains
|
|
depends on the mode you're in, and what servers you're monitoring. The first
|
|
few words are always [RO] (if readonly is set to 1), the innotop mode, such as
|
|
"InnoDB Txns" for T mode, followed by a reminder to press '?' for help at any
|
|
time.
|
|
|
|
=head2 ONE SERVER
|
|
|
|
The simplest case is when you're monitoring a single server. In this case, the
|
|
name of the connection is next on the status line. This is the name you gave
|
|
when you created the connection -- most likely the MySQL server's hostname.
|
|
This is followed by the server's uptime.
|
|
|
|
If you're in an InnoDB mode, such as T or B, the next word is "InnoDB" followed
|
|
by some information about the SHOW INNODB STATUS output used to render the
|
|
screen. The first word is the number of seconds since the last SHOW INNODB
|
|
STATUS, which InnoDB uses to calculate some per-second statistics. The next is
|
|
a smiley face indicating whether the InnoDB output is truncated. If the smiley
|
|
face is a :-), all is well; there is no truncation. A :^| means the transaction
|
|
list is so long, InnoDB has only printed out some of the transactions. Finally,
|
|
a frown :-( means the output is incomplete, which is probably due to a deadlock
|
|
printing too much lock information (see L<"D: InnoDB Deadlocks">).
|
|
|
|
The next two words indicate the server's queries per second (QPS) and how many
|
|
threads (connections) exist. Finally, the server's version number is the last
|
|
thing on the line.
|
|
|
|
=head2 MULTIPLE SERVERS
|
|
|
|
If you are monitoring multiple servers (see L<"SERVER CONNECTIONS">), the status
|
|
line does not show any details about individual servers. Instead, it shows the
|
|
names of the connections that are active. Again, these are connection names you
|
|
specified, which are likely to be the server's hostname. A connection that has
|
|
an error is prefixed with an exclamation point.
|
|
|
|
If you are monitoring a group of servers (see L<"SERVER GROUPS">), the status
|
|
line shows the name of the group. If any connection in the group has an
|
|
error, the group's name is followed by the fraction of the connections that
|
|
don't have errors.
|
|
|
|
See L<"ERROR HANDLING"> for more details about innotop's error handling.
|
|
|
|
=head2 MONITORING A FILE
|
|
|
|
If you give a filename on the command line, innotop will not connect to ANY
|
|
servers at all. It will watch the specified file for InnoDB status output and
|
|
use that as its data source. It will always show a single connection called
|
|
'file'. And since it can't connect to a server, it can't determine how long the
|
|
server it's monitoring has been up; so it calculates the server's uptime as time
|
|
since innotop started running.
|
|
|
|
=head1 SERVER ADMINISTRATION
|
|
|
|
While innotop is primarily a monitor that lets you watch and analyze your
|
|
servers, it can also send commands to servers. The most frequently useful
|
|
commands are killing queries and stopping or starting slaves.
|
|
|
|
You can kill a connection, or in newer versions of MySQL kill a query but not a
|
|
connection, from L<"Q: Query List"> and L<"T: InnoDB Transactions"> modes.
|
|
Press 'k' to issue a KILL command, or 'x' to issue a KILL QUERY command.
|
|
innotop will prompt you for the server and/or connection ID to kill (innotop
|
|
does not prompt you if there is only one possible choice for any input).
|
|
innotop pre-selects the longest-running query, or the oldest connection.
|
|
Confirm the command with 'y'.
|
|
|
|
In L<"M: Master/Slave Replication Status"> mode, you can start and stop slaves
|
|
with the 'a' and 'o' keys, respectively. You can send these commands to many
|
|
slaves at once. innotop fills in a default command of START SLAVE or STOP SLAVE
|
|
for you, but you can actually edit the command and send anything you wish, such
|
|
as SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1 to make the slave skip one binlog event
|
|
when it starts.
|
|
|
|
You can also ask innotop to calculate the earliest binlog in use by any slave
|
|
and issue a PURGE MASTER LOGS on the master. Use the 'b' key for this. innotop
|
|
will prompt you for a master to run the command on, then prompt you for the
|
|
connection names of that master's slaves (there is no way for innotop to
|
|
determine this reliably itself). innotop will find the minimum binlog in use by
|
|
these slave connections and suggest it as the argument to PURGE MASTER LOGS.
|
|
|
|
=head1 SERVER CONNECTIONS
|
|
|
|
When you create a server connection using '@', innotop asks you for a series of
|
|
inputs, as follows:
|
|
|
|
=over
|
|
|
|
=item DSN
|
|
|
|
A DSN is a Data Source Name, which is the initial argument passed to the DBI
|
|
module for connecting to a server. It is usually of the form
|
|
|
|
DBI:mysql:;mysql_read_default_group=mysql;host=HOSTNAME
|
|
|
|
Since this DSN is passed to the DBD::mysql driver, you should read the driver's
|
|
documentation at L<"http://search.cpan.org/dist/DBD-mysql/lib/DBD/mysql.pm"> for
|
|
the exact details on all the options you can pass the driver in the DSN. You
|
|
can read more about DBI at L<http://dbi.perl.org/docs/>, and especially at
|
|
L<http://search.cpan.org/~timb/DBI/DBI.pm>.
|
|
|
|
The mysql_read_default_group=mysql option lets the DBD driver read your MySQL
|
|
options files, such as ~/.my.cnf on UNIX-ish systems. You can use this to avoid
|
|
specifying a username or password for the connection.
|
|
|
|
=item InnoDB Deadlock Table
|
|
|
|
This optional item tells innotop a table name it can use to deliberately create
|
|
a small deadlock (see L<"D: InnoDB Deadlocks">). If you specify this option,
|
|
you just need to be sure the table doesn't exist, and that innotop can create
|
|
and drop the table with the InnoDB storage engine. You can safely omit or just
|
|
accept the default if you don't intend to use this.
|
|
|
|
=item Username
|
|
|
|
innotop will ask you if you want to specify a username. If you say 'y', it will
|
|
then prompt you for a user name. If you have a MySQL option file that specifies
|
|
your username, you don't have to specify a username.
|
|
|
|
The username defaults to your login name on the system you're running innotop on.
|
|
|
|
=item Password
|
|
|
|
innotop will ask you if you want to specify a password. Like the username, the
|
|
password is optional, but there's an additional prompt that asks if you want to
|
|
save the password in the innotop configuration file. If you don't save it in
|
|
the configuration file, innotop will prompt you for a password each time it
|
|
starts. Passwords in the innotop configuration file are saved in plain text,
|
|
not encrypted in any way.
|
|
|
|
=back
|
|
|
|
Once you finish answering these questions, you should be connected to a server.
|
|
But innotop isn't limited to monitoring a single server; you can define many
|
|
server connections and switch between them by pressing the '@' key. See
|
|
L<"SWITCHING BETWEEN CONNECTIONS">.
|
|
|
|
=head1 SERVER GROUPS
|
|
|
|
If you have multiple MySQL instances, you can put them into named groups, such
|
|
as 'all', 'masters', and 'slaves', which innotop can monitor all together.
|
|
|
|
You can choose which group to monitor with the '#' key, and you can press the
|
|
TAB key to switch to the next group. If you're not currently monitoring a
|
|
group, pressing TAB selects the first group.
|
|
|
|
To create a group, press the '#' key and type the name of your new group, then
|
|
type the names of the connections you want the group to contain.
|
|
|
|
=head1 SWITCHING BETWEEN CONNECTIONS
|
|
|
|
innotop lets you quickly switch which servers you're monitoring. The most basic
|
|
way is by pressing the '@' key and typing the name(s) of the connection(s) you
|
|
want to use. This setting is per-mode, so you can monitor different connections
|
|
in each mode, and innotop remembers which connections you choose.
|
|
|
|
You can quickly switch to the 'next' connection in alphabetical order with the
|
|
'n' key. If you're monitoring a server group (see L<"SERVER GROUPS">) this will
|
|
switch to the first connection.
|
|
|
|
You can also type many connection names, and innotop will fetch and display data
|
|
from them all. Just separate the connection names with spaces, for example
|
|
"server1 server2." Again, if you type the name of a connection that doesn't
|
|
exist, innotop will prompt you for connection information and create the
|
|
connection.
|
|
|
|
Another way to monitor multiple connections at once is with server groups. You
|
|
can use the TAB key to switch to the 'next' group in alphabetical order, or if
|
|
you're not monitoring any groups, TAB will switch to the first group.
|
|
|
|
innotop does not fetch data in parallel from connections, so if you are
|
|
monitoring a large group or many connections, you may notice increased delay
|
|
between ticks.
|
|
|
|
When you monitor more than one connection, innotop's status bar changes. See
|
|
L<"INNOTOP STATUS">.
|
|
|
|
=head1 ERROR HANDLING
|
|
|
|
Error handling is not that important when monitoring a single connection, but is
|
|
crucial when you have many active connections. A crashed server or lost
|
|
connection should not crash innotop. As a result, innotop will continue to run
|
|
even when there is an error; it just won't display any information from the
|
|
connection that had an error. Because of this, innotop's behavior might confuse
|
|
you. It's a feature, not a bug!
|
|
|
|
innotop does not continue to query connections that have errors, because they
|
|
may slow innotop and make it hard to use, especially if the error is a problem
|
|
connecting and causes a long time-out. Instead, innotop retries the connection
|
|
occasionally to see if the error still exists. If so, it will wait until some
|
|
point in the future. The wait time increases in ticks as the Fibonacci series,
|
|
so it tries less frequently as time passes.
|
|
|
|
Since errors might only happen in certain modes because of the SQL commands
|
|
issued in those modes, innotop keeps track of which mode caused the error. If
|
|
you switch to a different mode, innotop will retry the connection instead of
|
|
waiting.
|
|
|
|
By default innotop will display the problem in red text at the bottom of the
|
|
first table on the screen. You can disable this behavior with the
|
|
L<"show_cxn_errors_in_tbl"> configuration option, which is enabled by default.
|
|
If the L<"debug"> option is enabled, innotop will display the error at the
|
|
bottom of every table, not just the first. And if L<"show_cxn_errors"> is
|
|
enabled, innotop will print the error text to STDOUT as well. Error messages
|
|
might only display in the mode that caused the error, depending on the mode and
|
|
whether innotop is avoiding querying that connection.
|
|
|
|
=head1 NON-INTERACTIVE OPERATION
|
|
|
|
You can run innotop in non-interactive mode, in which case it is entirely
|
|
controlled from the configuration file and command-line options. To start
|
|
innotop in non-interactive mode, give the L"<--nonint"> command-line option.
|
|
This changes innotop's behavior in the following ways:
|
|
|
|
=over
|
|
|
|
=item *
|
|
|
|
Certain Perl modules are not loaded. Term::Readline is not loaded, since
|
|
innotop doesn't prompt interactively. Term::ANSIColor and Win32::Console::ANSI
|
|
modules are not loaded. Term::ReadKey is still used, since innotop may have to
|
|
prompt for connection passwords when starting up.
|
|
|
|
=item *
|
|
|
|
innotop does not clear the screen after each tick.
|
|
|
|
=item *
|
|
|
|
innotop does not persist any changes to the configuration file.
|
|
|
|
=item *
|
|
|
|
If L<"--count"> is given and innotop is in incremental mode (see L<"status_inc">
|
|
and L<"--inc">), innotop actually refreshes one more time than specified so it
|
|
can print incremental statistics. This suppresses output during the first
|
|
tick, so innotop may appear to hang.
|
|
|
|
=item *
|
|
|
|
innotop only displays the first table in each mode. This is so the output can
|
|
be easily processed with other command-line utilities such as awk and sed. To
|
|
change which tables display in each mode, see L<"TABLES">. Since L<"Q: Query
|
|
List"> mode is so important, innotop automatically disables the L<"q_header">
|
|
table. This ensures you'll see the L<"processlist"> table, even if you have
|
|
innotop configured to show the q_header table during interactive operation.
|
|
Similarly, in L<"T: InnoDB Transactions"> mode, the L<"t_header"> table is
|
|
suppressed so you see only the L<"innodb_transactions"> table.
|
|
|
|
=item *
|
|
|
|
All output is tab-separated instead of being column-aligned with whitespace, and
|
|
innotop prints the full contents of each table instead of only printing one
|
|
screenful at a time.
|
|
|
|
=item *
|
|
|
|
innotop only prints column headers once instead of every tick (see
|
|
L<"hide_hdr">). innotop does not print table captions (see
|
|
L<"display_table_captions">). innotop ensures there are no empty lines in the
|
|
output.
|
|
|
|
=item *
|
|
|
|
innotop does not honor the L<"shorten"> transformation, which normally shortens
|
|
some numbers to human-readable formats.
|
|
|
|
=item *
|
|
|
|
innotop does not print a status line (see L<"INNOTOP STATUS">).
|
|
|
|
=back
|
|
|
|
=head1 CONFIGURING
|
|
|
|
Nearly everything about innotop is configurable. Most things are possible to
|
|
change with built-in commands, but you can also edit the configuration file.
|
|
|
|
While running innotop, press the '$' key to bring up the configuration editing
|
|
dialog. Press another key to select the type of data you want to edit:
|
|
|
|
=over
|
|
|
|
=item S: Statement Sleep Times
|
|
|
|
Edits SQL statement sleep delays, which make innotop pause for the specified
|
|
amount of time after executing a statement. See L<"SQL STATEMENTS"> for a
|
|
definition of each statement and what it does. By default innotop does not
|
|
delay after any statements.
|
|
|
|
This feature is included so you can customize the side-effects caused by
|
|
monitoring your server. You may not see any effects, but some innotop users
|
|
have noticed that certain MySQL versions under very high load with InnoDB
|
|
enabled take longer than usual to execute SHOW GLOBAL STATUS. If innotop calls
|
|
SHOW FULL PROCESSLIST immediately afterward, the processlist contains more
|
|
queries than the machine actually averages at any given moment. Configuring
|
|
innotop to pause briefly after calling SHOW GLOBAL STATUS alleviates this
|
|
effect.
|
|
|
|
Sleep times are stored in the L<"stmt_sleep_times"> section of the configuration
|
|
file. Fractional-second sleeps are supported, subject to your hardware's
|
|
limitations.
|
|
|
|
=item c: Edit Columns
|
|
|
|
Starts the table editor on one of the displayed tables. See L<"TABLE EDITOR">.
|
|
An alternative way to start the table editor without entering the configuration
|
|
dialog is with the '^' key.
|
|
|
|
=item g: General Configuration
|
|
|
|
Starts the configuration editor to edit global and mode-specific configuration
|
|
variables (see L<"MODES">). innotop prompts you to choose a variable from among
|
|
the global and mode-specific ones depending on the current mode.
|
|
|
|
=item k: Row-Coloring Rules
|
|
|
|
Starts the row-coloring rules editor on one of the displayed table(s). See
|
|
L<"COLORS"> for details.
|
|
|
|
=item p: Manage Plugins
|
|
|
|
Starts the plugin configuration editor. See L<"PLUGINS"> for details.
|
|
|
|
=item s: Server Groups
|
|
|
|
Lets you create and edit server groups. See L<"SERVER GROUPS">.
|
|
|
|
=item t: Choose Displayed Tables
|
|
|
|
Lets you choose which tables to display in this mode. See L<"MODES"> and
|
|
L<"TABLES">.
|
|
|
|
=back
|
|
|
|
=head1 CONFIGURATION FILE
|
|
|
|
innotop's default configuration file locations are $HOME/.innotop and
|
|
/etc/innotop/innotop.conf, and they are looked for in that order. If the first
|
|
configuration file exists, the second will not be processed. Those can be
|
|
overridden with the L<"--config"> command-line option. You can edit it by hand
|
|
safely, however innotop reads the configuration file when it starts, and, if
|
|
readonly is set to 0, writes it out again when it exits. Thus, if readonly is
|
|
set to 0, any changes you make by hand while innotop is running will be lost.
|
|
|
|
innotop doesn't store its entire configuration in the configuration file. It
|
|
has a huge set of default configuration values that it holds only in memory,
|
|
and the configuration file only overrides these defaults. When you customize a
|
|
default setting, innotop notices, and then stores the customizations into the
|
|
file. This keeps the file size down, makes it easier to edit, and makes
|
|
upgrades easier.
|
|
|
|
A configuration file is read-only be default. You can override that with
|
|
L<"--write">. See L<"readonly">.
|
|
|
|
The configuration file is arranged into sections like an INI file. Each
|
|
section begins with [section-name] and ends with [/section-name]. Each
|
|
section's entries have a different syntax depending on the data they need to
|
|
store. You can put comments in the file; any line that begins with a #
|
|
character is a comment. innotop will not read the comments, so it won't write
|
|
them back out to the file when it exits. Comments in read-only configuration
|
|
files are still useful, though.
|
|
|
|
The first line in the file is innotop's version number. This lets innotop
|
|
notice when the file format is not backwards-compatible, and upgrade smoothly
|
|
without destroying your customized configuration.
|
|
|
|
The following list describes each section of the configuration file and the data
|
|
it contains:
|
|
|
|
=over
|
|
|
|
=item general
|
|
|
|
The 'general' section contains global configuration variables and variables that
|
|
may be mode-specific, but don't belong in any other section. The syntax is a
|
|
simple key=value list. innotop writes a comment above each value to help you
|
|
edit the file by hand.
|
|
|
|
=over
|
|
|
|
=item S_func
|
|
|
|
Controls S mode presentation (see L<"S: Variables & Status">). If g, values are
|
|
graphed; if s, values are like vmstat; if p, values are in a pivoted table.
|
|
|
|
=item S_set
|
|
|
|
Specifies which set of variables to display in L<"S: Variables & Status"> mode.
|
|
See L<"VARIABLE SETS">.
|
|
|
|
=item auto_wipe_dl
|
|
|
|
Instructs innotop to automatically wipe large deadlocks when it notices them.
|
|
When this happens you may notice a slight delay. At the next tick, you will
|
|
usually see the information that was being truncated by the large deadlock.
|
|
|
|
=item charset
|
|
|
|
Specifies what kind of characters to allow through the L<"no_ctrl_char">
|
|
transformation. This keeps non-printable characters from confusing a
|
|
terminal when you monitor queries that contain binary data, such as images.
|
|
|
|
The default is 'ascii', which considers anything outside normal ASCII to be a
|
|
control character. The other allowable values are 'unicode' and 'none'. 'none'
|
|
considers every character a control character, which can be useful for
|
|
collapsing ALL text fields in queries.
|
|
|
|
=item cmd_filter
|
|
|
|
This is the prefix that filters variables in L<"C: Command Summary"> mode.
|
|
|
|
=item color
|
|
|
|
Whether terminal coloring is permitted.
|
|
|
|
=item cxn_timeout
|
|
|
|
On MySQL versions 4.0.3 and newer, this variable is used to set the connection's
|
|
timeout, so MySQL doesn't close the connection if it is not used for a while.
|
|
This might happen because a connection isn't monitored in a particular mode, for
|
|
example.
|
|
|
|
=item debug
|
|
|
|
This option enables more verbose errors and makes innotop more strict in some
|
|
places. It can help in debugging filters and other user-defined code. It also
|
|
makes innotop write a lot of information to L<"debugfile"> when there is a
|
|
crash.
|
|
|
|
=item debugfile
|
|
|
|
A file to which innotop will write information when there is a crash. See
|
|
L<"FILES">.
|
|
|
|
=item display_table_captions
|
|
|
|
innotop displays a table caption above most tables. This variable suppresses or
|
|
shows captions on all tables globally. Some tables are configured with the
|
|
hide_caption property, which overrides this.
|
|
|
|
=item global
|
|
|
|
Whether to show GLOBAL variables and status. innotop only tries to do this on
|
|
servers which support the GLOBAL option to SHOW VARIABLES and SHOW STATUS. In
|
|
some MySQL versions, you need certain privileges to do this; if you don't have
|
|
them, innotop will not be able to fetch any variable and status data. This
|
|
configuration variable lets you run innotop and fetch what data you can even
|
|
without the elevated privileges.
|
|
|
|
I can no longer find or reproduce the situation where GLOBAL wasn't allowed, but
|
|
I know there was one.
|
|
|
|
=item graph_char
|
|
|
|
Defines the character to use when drawing graphs in L<"S: Variables & Status">
|
|
mode.
|
|
|
|
=item header_highlight
|
|
|
|
Defines how to highlight column headers. This only works if Term::ANSIColor is
|
|
available. Valid values are 'bold' and 'underline'.
|
|
|
|
=item hide_hdr
|
|
|
|
Hides column headers globally.
|
|
|
|
=item interval
|
|
|
|
The interval at which innotop will refresh its data (ticks). The interval is
|
|
implemented as a sleep time between ticks, so the true interval will vary
|
|
depending on how long it takes innotop to fetch and render data.
|
|
|
|
This variable accepts fractions of a second.
|
|
|
|
=item mode
|
|
|
|
The mode in which innotop should start. Allowable arguments are the same as the
|
|
key presses that select a mode interactively. See L<"MODES">.
|
|
|
|
=item num_digits
|
|
|
|
How many digits to show in fractional numbers and percents. This variable's
|
|
range is between 0 and 9 and can be set directly from L<"S: Variables & Status">
|
|
mode with the '+' and '-' keys. It is used in the L<"set_precision">,
|
|
L<"shorten">, and L<"percent"> transformations.
|
|
|
|
=item num_status_sets
|
|
|
|
Controls how many sets of status variables to display in pivoted L<"S: Variables
|
|
& Status"> mode. It also controls the number of old sets of variables innotop
|
|
keeps in its memory, so the larger this variable is, the more memory innotop
|
|
uses.
|
|
|
|
=item plugin_dir
|
|
|
|
Specifies where plugins can be found. By default, innotop stores plugins in the
|
|
'plugins' subdirectory of your innotop configuration directory.
|
|
|
|
=item readonly
|
|
|
|
Whether the configuration file is readonly. This cannot be set interactively.
|
|
|
|
=item show_cxn_errors
|
|
|
|
Makes innotop print connection errors to STDOUT. See L<"ERROR HANDLING">.
|
|
|
|
=item show_cxn_errors_in_tbl
|
|
|
|
Makes innotop display connection errors as rows in the first table on screen.
|
|
See L<"ERROR HANDLING">.
|
|
|
|
=item show_percent
|
|
|
|
Adds a '%' character after the value returned by the L<"percent">
|
|
transformation.
|
|
|
|
=item show_statusbar
|
|
|
|
Controls whether to show the status bar in the display. See L<"INNOTOP
|
|
STATUS">.
|
|
|
|
=item skip_innodb
|
|
|
|
Disables fetching SHOW INNODB STATUS, in case your server(s) do not have InnoDB
|
|
enabled and you don't want innotop to try to fetch it. This can also be useful
|
|
when you don't have the SUPER privilege, required to run SHOW INNODB STATUS.
|
|
|
|
=item status_inc
|
|
|
|
Whether to show absolute or incremental values for status variables.
|
|
Incremental values are calculated as an offset from the last value innotop saw
|
|
for that variable. This is a global setting, but will probably become
|
|
mode-specific at some point. Right now it is honored a bit inconsistently; some
|
|
modes don't pay attention to it.
|
|
|
|
=back
|
|
|
|
=item plugins
|
|
|
|
This section holds a list of package names of active plugins. If the plugin
|
|
exists, innotop will activate it. See L<"PLUGINS"> for more information.
|
|
|
|
=item filters
|
|
|
|
This section holds user-defined filters (see L<"FILTERS">). Each line is in the
|
|
format filter_name=text='filter text' tbls='table list'.
|
|
|
|
The filter text is the text of the subroutine's code. The table list is a list
|
|
of tables to which the filter can apply. By default, user-defined filters apply
|
|
to the table for which they were created, but you can manually override that by
|
|
editing the definition in the configuration file.
|
|
|
|
=item active_filters
|
|
|
|
This section stores which filters are active on each table. Each line is in the
|
|
format table_name=filter_list.
|
|
|
|
=item tbl_meta
|
|
|
|
This section stores user-defined or user-customized columns (see L<"COLUMNS">).
|
|
Each line is in the format col_name=properties, where the properties are a
|
|
name=quoted-value list.
|
|
|
|
=item connections
|
|
|
|
This section holds the server connections you have defined. Each line is in
|
|
the format name=properties, where the properties are a name=value list. The
|
|
properties are self-explanatory, and the only one that is treated specially is
|
|
'pass' which is only present if 'savepass' is set. This section of the
|
|
configuration file will be skipped if any DSN, username, or password
|
|
command-line options are used. See L<"SERVER CONNECTIONS">.
|
|
|
|
=item active_connections
|
|
|
|
This section holds a list of which connections are active in each mode. Each
|
|
line is in the format mode_name=connection_list.
|
|
|
|
=item server_groups
|
|
|
|
This section holds server groups. Each line is in the format
|
|
name=connection_list. See L<"SERVER GROUPS">.
|
|
|
|
=item active_server_groups
|
|
|
|
This section holds a list of which server group is active in each mode. Each
|
|
line is in the format mode_name=server_group.
|
|
|
|
=item max_values_seen
|
|
|
|
This section holds the maximum values seen for variables. This is used to scale
|
|
the graphs in L<"S: Variables & Status"> mode. Each line is in the format
|
|
name=value.
|
|
|
|
=item active_columns
|
|
|
|
This section holds table column lists. Each line is in the format
|
|
tbl_name=column_list. See L<"COLUMNS">.
|
|
|
|
=item sort_cols
|
|
|
|
This section holds the sort definition. Each line is in the format
|
|
tbl_name=column_list. If a column is prefixed with '-', that column sorts
|
|
descending. See L<"SORTING">.
|
|
|
|
=item visible_tables
|
|
|
|
This section defines which tables are visible in each mode. Each line is in the
|
|
format mode_name=table_list. See L<"TABLES">.
|
|
|
|
=item varsets
|
|
|
|
This section defines variable sets for use in L<"S: Status & Variables"> mode.
|
|
Each line is in the format name=variable_list. See L<"VARIABLE SETS">.
|
|
|
|
=item colors
|
|
|
|
This section defines colorization rules. Each line is in the format
|
|
tbl_name=property_list. See L<"COLORS">.
|
|
|
|
=item stmt_sleep_times
|
|
|
|
This section contains statement sleep times. Each line is in the format
|
|
statement_name=sleep_time. See L<"S: Statement Sleep Times">.
|
|
|
|
=item group_by
|
|
|
|
This section contains column lists for table group_by expressions. Each line is
|
|
in the format tbl_name=column_list. See L<"GROUPING">.
|
|
|
|
=back
|
|
|
|
=head1 CUSTOMIZING
|
|
|
|
You can customize innotop a great deal. For example, you can:
|
|
|
|
=over
|
|
|
|
=item *
|
|
|
|
Choose which tables to display, and in what order.
|
|
|
|
=item *
|
|
|
|
Choose which columns are in those tables, and create new columns.
|
|
|
|
=item *
|
|
|
|
Filter which rows display with built-in filters, user-defined filters, and
|
|
quick-filters.
|
|
|
|
=item *
|
|
|
|
Sort the rows to put important data first or group together related rows.
|
|
|
|
=item *
|
|
|
|
Highlight rows with color.
|
|
|
|
=item *
|
|
|
|
Customize the alignment, width, and formatting of columns, and apply
|
|
transformations to columns to extract parts of their values or format the values
|
|
as you wish (for example, shortening large numbers to familiar units).
|
|
|
|
=item *
|
|
|
|
Design your own expressions to extract and combine data as you need. This gives
|
|
you unlimited flexibility.
|
|
|
|
=back
|
|
|
|
All these and more are explained in the following sections.
|
|
|
|
=head2 TABLES
|
|
|
|
A table is what you'd expect: a collection of columns. It also has some other
|
|
properties, such as a caption. Filters, sorting rules, and colorization rules
|
|
belong to tables and are covered in later sections.
|
|
|
|
Internally, table meta-data is defined in a data structure called %tbl_meta.
|
|
This hash holds all built-in table definitions, which contain a lot of default
|
|
instructions to innotop. The meta-data includes the caption, a list of columns
|
|
the user has customized, a list of columns, a list of visible columns, a list of
|
|
filters, color rules, a sort-column list, sort direction, and some information
|
|
about the table's data sources. Most of this is customizable via the table
|
|
editor (see L<"TABLE EDITOR">).
|
|
|
|
You can choose which tables to show by pressing the '$' key. See L<"MODES"> and
|
|
L<"TABLES">.
|
|
|
|
The table life-cycle is as follows:
|
|
|
|
=over
|
|
|
|
=item *
|
|
|
|
Each table begins with a data source, which is an array of hashes. See below
|
|
for details on data sources.
|
|
|
|
=item *
|
|
|
|
Each element of the data source becomes a row in the final table.
|
|
|
|
=item *
|
|
|
|
For each element in the data source, innotop extracts values from the source and
|
|
creates a row. This row is another hash, which later steps will refer to as
|
|
$set. The values innotop extracts are determined by the table's columns. Each
|
|
column has an extraction subroutine, compiled from an expression (see
|
|
L<"EXPRESSIONS">). The resulting row is a hash whose keys are named the same as
|
|
the column name.
|
|
|
|
=item *
|
|
|
|
innotop filters the rows, removing those that don't need to be displayed. See
|
|
L<"FILTERS">.
|
|
|
|
=item *
|
|
|
|
innotop sorts the rows. See L<"SORTING">.
|
|
|
|
=item *
|
|
|
|
innotop groups the rows together, if specified. See L<"GROUPING">.
|
|
|
|
=item *
|
|
|
|
innotop colorizes the rows. See L<"COLORS">.
|
|
|
|
=item *
|
|
|
|
innotop transforms the column values in each row. See L<"TRANSFORMATIONS">.
|
|
|
|
=item *
|
|
|
|
innotop optionally pivots the rows (see L<"PIVOTING">), then filters and sorts
|
|
them.
|
|
|
|
=item *
|
|
|
|
innotop formats and justifies the rows as a table. During this step, innotop
|
|
applies further formatting to the column values, including alignment, maximum
|
|
and minimum widths. innotop also does final error checking to ensure there are
|
|
no crashes due to undefined values. innotop then adds a caption if specified,
|
|
and the table is ready to print.
|
|
|
|
=back
|
|
|
|
The lifecycle is slightly different if the table is pivoted, as noted above. To
|
|
clarify, if the table is pivoted, the process is extract, group, transform,
|
|
pivot, filter, sort, create. If it's not pivoted, the process is extract,
|
|
filter, sort, group, color, transform, create. This slightly convoluted process
|
|
doesn't map all that well to SQL, but pivoting complicates things pretty
|
|
thoroughly. Roughly speaking, filtering and sorting happen as late as needed to
|
|
effect the final result as you might expect, but as early as possible for
|
|
efficiency.
|
|
|
|
Each built-in table is described below:
|
|
|
|
=over
|
|
|
|
=item adaptive_hash_index
|
|
|
|
Displays data about InnoDB's adaptive hash index. Data source:
|
|
L<"STATUS_VARIABLES">.
|
|
|
|
=item buffer_pool
|
|
|
|
Displays data about InnoDB's buffer pool. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item cmd_summary
|
|
|
|
Displays weighted status variables. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item deadlock_locks
|
|
|
|
Shows which locks were held and waited for by the last detected deadlock. Data
|
|
source: L<"DEADLOCK_LOCKS">.
|
|
|
|
=item deadlock_transactions
|
|
|
|
Shows transactions involved in the last detected deadlock. Data source:
|
|
L<"DEADLOCK_TRANSACTIONS">.
|
|
|
|
=item explain
|
|
|
|
Shows the output of EXPLAIN. Data source: L<"EXPLAIN">.
|
|
|
|
=item file_io_misc
|
|
|
|
Displays data about InnoDB's file and I/O operations. Data source:
|
|
L<"STATUS_VARIABLES">.
|
|
|
|
=item fk_error
|
|
|
|
Displays various data about InnoDB's last foreign key error. Data source:
|
|
L<"STATUS_VARIABLES">.
|
|
|
|
=item innodb_locks
|
|
|
|
Displays InnoDB locks. Data source: L<"INNODB_LOCKS">.
|
|
|
|
=item innodb_transactions
|
|
|
|
Displays data about InnoDB's current transactions. Data source:
|
|
L<"INNODB_TRANSACTIONS">.
|
|
|
|
=item insert_buffers
|
|
|
|
Displays data about InnoDB's insert buffer. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item io_threads
|
|
|
|
Displays data about InnoDB's I/O threads. Data source: L<"IO_THREADS">.
|
|
|
|
=item log_statistics
|
|
|
|
Displays data about InnoDB's logging system. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item master_status
|
|
|
|
Displays replication master status. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item open_tables
|
|
|
|
Displays open tables. Data source: L<"OPEN_TABLES">.
|
|
|
|
=item page_statistics
|
|
|
|
Displays InnoDB page statistics. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item pending_io
|
|
|
|
Displays InnoDB pending I/O operations. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item processlist
|
|
|
|
Displays current MySQL processes (threads/connections). Data source:
|
|
L<"PROCESSLIST">.
|
|
|
|
=item q_header
|
|
|
|
Displays various status values. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item row_operation_misc
|
|
|
|
Displays data about InnoDB's row operations. Data source:
|
|
L<"STATUS_VARIABLES">.
|
|
|
|
=item row_operations
|
|
|
|
Displays data about InnoDB's row operations. Data source:
|
|
L<"STATUS_VARIABLES">.
|
|
|
|
=item semaphores
|
|
|
|
Displays data about InnoDB's semaphores and mutexes. Data source:
|
|
L<"STATUS_VARIABLES">.
|
|
|
|
=item slave_io_status
|
|
|
|
Displays data about the slave I/O thread. Data source:
|
|
L<"STATUS_VARIABLES">.
|
|
|
|
=item slave_sql_status
|
|
|
|
Displays data about the slave SQL thread. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item t_header
|
|
|
|
Displays various InnoDB status values. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item var_status
|
|
|
|
Displays user-configurable data. Data source: L<"STATUS_VARIABLES">.
|
|
|
|
=item wait_array
|
|
|
|
Displays data about InnoDB's OS wait array. Data source: L<"OS_WAIT_ARRAY">.
|
|
|
|
=back
|
|
|
|
=head2 COLUMNS
|
|
|
|
Columns belong to tables. You can choose a table's columns by pressing the '^'
|
|
key, which starts the L<"TABLE EDITOR"> and lets you choose and edit columns.
|
|
Pressing 'e' from within the table editor lets you edit the column's properties:
|
|
|
|
=over
|
|
|
|
=item *
|
|
|
|
hdr: a column header. This appears in the first row of the table.
|
|
|
|
=item *
|
|
|
|
just: justification. '-' means left-justified and '' means right-justified,
|
|
just as with printf formatting codes (not a coincidence).
|
|
|
|
=item *
|
|
|
|
dec: whether to further align the column on the decimal point.
|
|
|
|
=item *
|
|
|
|
num: whether the column is numeric. This affects how values are sorted
|
|
(lexically or numerically).
|
|
|
|
=item *
|
|
|
|
label: a small note about the column, which appears in dialogs that help the
|
|
user choose columns.
|
|
|
|
=item *
|
|
|
|
src: an expression that innotop uses to extract the column's data from its
|
|
source (see L<"DATA SOURCES">). See L<"EXPRESSIONS"> for more on expressions.
|
|
|
|
=item *
|
|
|
|
minw: specifies a minimum display width. This helps stabilize the display,
|
|
which makes it easier to read if the data is changing frequently.
|
|
|
|
=item *
|
|
|
|
maxw: similar to minw.
|
|
|
|
=item *
|
|
|
|
trans: a list of column transformations. See L<"TRANSFORMATIONS">.
|
|
|
|
=item *
|
|
|
|
agg: an aggregate function. See L<"GROUPING">. The default is L<"first">.
|
|
|
|
=item *
|
|
|
|
aggonly: controls whether the column only shows when grouping is enabled on the
|
|
table (see L<"GROUPING">). By default, this is disabled. This means columns
|
|
will always be shown by default, whether grouping is enabled or not. If a
|
|
column's aggonly is set true, the column will appear when you toggle grouping on
|
|
the table. Several columns are set this way, such as the count column on
|
|
L<"processlist"> and L<"innodb_transactions">, so you don't see a count when the
|
|
grouping isn't enabled, but you do when it is.
|
|
|
|
=back
|
|
|
|
=head2 FILTERS
|
|
|
|
Filters remove rows from the display. They behave much like a WHERE clause in
|
|
SQL. innotop has several built-in filters, which remove irrelevant information
|
|
like inactive queries, but you can define your own as well. innotop also lets
|
|
you create quick-filters, which do not get saved to the configuration file, and
|
|
are just an easy way to quickly view only some rows.
|
|
|
|
You can enable or disable a filter on any table. Press the '%' key (mnemonic: %
|
|
looks kind of like a line being filtered between two circles) and choose which
|
|
table you want to filter, if asked. You'll then see a list of possible filters
|
|
and a list of filters currently enabled for that table. Type the names of
|
|
filters you want to apply and press Enter.
|
|
|
|
=head3 USER-DEFINED FILTERS
|
|
|
|
If you type a name that doesn't exist, innotop will prompt you to create the
|
|
filter. Filters are easy to create if you know Perl, and not hard if you don't.
|
|
What you're doing is creating a subroutine that returns true if the row should
|
|
be displayed. The row is a hash reference passed to your subroutine as $set.
|
|
|
|
For example, imagine you want to filter the processlist table so you only see
|
|
queries that have been running more than five minutes. Type a new name for your
|
|
filter, and when prompted for the subroutine body, press TAB to initiate your
|
|
terminal's auto-completion. You'll see the names of the columns in the
|
|
L<"processlist"> table (innotop generally tries to help you with auto-completion
|
|
lists). You want to filter on the 'time' column. Type the text "$set->{time} >
|
|
300" to return true when the query is more than five minutes old. That's all
|
|
you need to do.
|
|
|
|
In other words, the code you're typing is surrounded by an implicit context,
|
|
which looks like this:
|
|
|
|
sub filter {
|
|
my ( $set ) = @_;
|
|
# YOUR CODE HERE
|
|
}
|
|
|
|
If your filter doesn't work, or if something else suddenly behaves differently,
|
|
you might have made an error in your filter, and innotop is silently catching
|
|
the error. Try enabling L<"debug"> to make innotop throw an error instead.
|
|
|
|
=head3 QUICK-FILTERS
|
|
|
|
innotop's quick-filters are a shortcut to create a temporary filter that doesn't
|
|
persist when you restart innotop. To create a quick-filter, press the '/' key.
|
|
innotop will prompt you for the column name and filter text. Again, you can use
|
|
auto-completion on column names. The filter text can be just the text you want
|
|
to "search for." For example, to filter the L<"processlist"> table on queries
|
|
that refer to the products table, type '/' and then 'info product'.
|
|
|
|
The filter text can actually be any Perl regular expression, but of course a
|
|
literal string like 'product' works fine as a regular expression.
|
|
|
|
Behind the scenes innotop compiles the quick-filter into a specially tagged
|
|
filter that is otherwise like any other filter. It just isn't saved to the
|
|
configuration file.
|
|
|
|
To clear quick-filters, press the '\' key and innotop will clear them all at
|
|
once.
|
|
|
|
=head2 SORTING
|
|
|
|
innotop has sensible built-in defaults to sort the most important rows to the
|
|
top of the table. Like anything else in innotop, you can customize how any
|
|
table is sorted.
|
|
|
|
To start the sort dialog, start the L<"TABLE EDITOR"> with the '^' key, choose a
|
|
table if necessary, and press the 's' key. You'll see a list of columns you can
|
|
use in the sort expression and the current sort expression, if any. Enter a
|
|
list of columns by which you want to sort and press Enter. If you want to
|
|
reverse sort, prefix the column name with a minus sign. For example, if you
|
|
want to sort by column a ascending, then column b descending, type 'a -b'. You
|
|
can also explicitly add a + in front of columns you want to sort ascending, but
|
|
it's not required.
|
|
|
|
Some modes have keys mapped to open this dialog directly, and to quickly reverse
|
|
sort direction. Press '?' as usual to see which keys are mapped in any mode.
|
|
|
|
=head2 GROUPING
|
|
|
|
innotop can group, or aggregate, rows together (the terms are used
|
|
interchangeably). This is quite similar to an SQL GROUP BY clause. You can
|
|
specify to group on certain columns, or if you don't specify any, the entire set
|
|
of rows is treated as one group. This is quite like SQL so far, but unlike SQL,
|
|
you can also select un-grouped columns. innotop actually aggregates every
|
|
column. If you don't explicitly specify a grouping function, the default is
|
|
'first'. This is basically a convenience so you don't have to specify an
|
|
aggregate function for every column you want in the result.
|
|
|
|
You can quickly toggle grouping on a table with the '=' key, which toggles its
|
|
aggregate property. This property doesn't persist to the config file.
|
|
|
|
The columns by which the table is grouped are specified in its group_by
|
|
property. When you turn grouping on, innotop places the group_by columns at the
|
|
far left of the table, even if they're not supposed to be visible. The rest of
|
|
the visible columns appear in order after them.
|
|
|
|
Two tables have default group_by lists and a count column built in:
|
|
L<"processlist"> and L<"innodb_transactions">. The grouping is by connection
|
|
and status, so you can quickly see how many queries or transactions are in a
|
|
given status on each server you're monitoring. The time columns are aggregated
|
|
as a sum; other columns are left at the default 'first' aggregation.
|
|
|
|
By default, the table shown in L<"S: Variables & Status"> mode also uses
|
|
grouping so you can monitor variables and status across many servers. The
|
|
default aggregation function in this mode is 'avg'.
|
|
|
|
Valid grouping functions are defined in the %agg_funcs hash. They include
|
|
|
|
=over
|
|
|
|
=item first
|
|
|
|
Returns the first element in the group.
|
|
|
|
=item count
|
|
|
|
Returns the number of elements in the group, including undefined elements, much
|
|
like SQL's COUNT(*).
|
|
|
|
=item avg
|
|
|
|
Returns the average of defined elements in the group.
|
|
|
|
=item sum
|
|
|
|
Returns the sum of elements in the group.
|
|
|
|
=back
|
|
|
|
Here's an example of grouping at work. Suppose you have a very busy server with
|
|
hundreds of open connections, and you want to see how many connections are in
|
|
what status. Using the built-in grouping rules, you can press 'Q' to enter
|
|
L<"Q: Query List"> mode. Press '=' to toggle grouping (if necessary, select the
|
|
L<"processlist"> table when prompted).
|
|
|
|
Your display might now look like the following:
|
|
|
|
Query List (? for help) localhost, 32:33, 0.11 QPS, 1 thd, 5.0.38-log
|
|
|
|
CXN Cmd Cnt ID User Host Time Query
|
|
localhost Query 49 12933 webusr localhost 19:38 SELECT * FROM
|
|
localhost Sending Da 23 2383 webusr localhost 12:43 SELECT col1,
|
|
localhost Sleep 120 140 webusr localhost 5:18:12
|
|
localhost Statistics 12 19213 webusr localhost 01:19 SELECT * FROM
|
|
|
|
That's actually quite a worrisome picture. You've got a lot of idle connections
|
|
(Sleep), and some connections executing queries (Query and Sending Data).
|
|
That's okay, but you also have a lot in Statistics status, collectively spending
|
|
over a minute. That means the query optimizer is having a really hard time
|
|
optimizing your statements. Something is wrong; it should normally take
|
|
milliseconds to optimize queries. You might not have seen this pattern if you
|
|
didn't look at your connections in aggregate. (This is a made-up example, but
|
|
it can happen in real life).
|
|
|
|
=head2 PIVOTING
|
|
|
|
innotop can pivot a table for more compact display, similar to a Pivot Table in
|
|
a spreadsheet (also known as a crosstab). Pivoting a table makes columns into
|
|
rows. Assume you start with this table:
|
|
|
|
foo bar
|
|
=== ===
|
|
1 3
|
|
2 4
|
|
|
|
After pivoting, the table will look like this:
|
|
|
|
name set0 set1
|
|
==== ==== ====
|
|
foo 1 2
|
|
bar 3 4
|
|
|
|
To get reasonable results, you might need to group as well as pivoting.
|
|
innotop currently does this for L<"S: Variables & Status"> mode.
|
|
|
|
=head2 COLORS
|
|
|
|
By default, innotop highlights rows with color so you can see at a glance which
|
|
rows are more important. You can customize the colorization rules and add your
|
|
own to any table. Open the table editor with the '^' key, choose a table if
|
|
needed, and press 'o' to open the color editor dialog.
|
|
|
|
The color editor dialog displays the rules applied to the table, in the order
|
|
they are evaluated. Each row is evaluated against each rule to see if the rule
|
|
matches the row; if it does, the row gets the specified color, and no further
|
|
rules are evaluated. The rules look like the following:
|
|
|
|
state eq Locked black on_red
|
|
cmd eq Sleep white
|
|
user eq system user white
|
|
cmd eq Connect white
|
|
cmd eq Binlog Dump white
|
|
time > 600 red
|
|
time > 120 yellow
|
|
time > 60 green
|
|
time > 30 cyan
|
|
|
|
This is the default rule set for the L<"processlist"> table. In order of
|
|
priority, these rules make locked queries black on a red background, "gray out"
|
|
connections from replication and sleeping queries, and make queries turn from
|
|
cyan to red as they run longer.
|
|
|
|
(For some reason, the ANSI color code "white" is actually a light gray. Your
|
|
terminal's display may vary; experiment to find colors you like).
|
|
|
|
You can use keystrokes to move the rules up and down, which re-orders their
|
|
priority. You can also delete rules and add new ones. If you add a new rule,
|
|
innotop prompts you for the column, an operator for the comparison, a value
|
|
against which to compare the column, and a color to assign if the rule matches.
|
|
There is auto-completion and prompting at each step.
|
|
|
|
The value in the third step needs to be correctly quoted. innotop does not try
|
|
to quote the value because it doesn't know whether it should treat the value as
|
|
a string or a number. If you want to compare the column against a string, as
|
|
for example in the first rule above, you should enter 'Locked' surrounded by
|
|
quotes. If you get an error message about a bareword, you probably should have
|
|
quoted something.
|
|
|
|
=head2 EXPRESSIONS
|
|
|
|
Expressions are at the core of how innotop works, and are what enables you to
|
|
extend innotop as you wish. Recall the table lifecycle explained in
|
|
L<"TABLES">. Expressions are used in the earliest step, where it extracts
|
|
values from a data source to form rows.
|
|
|
|
It does this by calling a subroutine for each column, passing it the source data
|
|
set, a set of current values, and a set of previous values. These are all
|
|
needed so the subroutine can calculate things like the difference between this
|
|
tick and the previous tick.
|
|
|
|
The subroutines that extract the data from the set are compiled from
|
|
expressions. This gives significantly more power than just naming the values to
|
|
fill the columns, because it allows the column's value to be calculated from
|
|
whatever data is necessary, but avoids the need to write complicated and lengthy
|
|
Perl code.
|
|
|
|
innotop begins with a string of text that can look as simple as a value's name
|
|
or as complicated as a full-fledged Perl expression. It looks at each
|
|
'bareword' token in the string and decides whether it's supposed to be a key
|
|
into the $set hash. A bareword is an unquoted value that isn't already
|
|
surrounded by code-ish things like dollar signs or curly brackets. If innotop
|
|
decides that the bareword isn't a function or other valid Perl code, it converts
|
|
it into a hash access. After the whole string is processed, innotop compiles a
|
|
subroutine, like this:
|
|
|
|
sub compute_column_value {
|
|
my ( $set, $cur, $pre ) = @_;
|
|
my $val = # EXPANDED STRING GOES HERE
|
|
return $val;
|
|
}
|
|
|
|
Here's a concrete example, taken from the header table L<"q_header"> in L<"Q:
|
|
Query List"> mode. This expression calculates the qps, or Queries Per Second,
|
|
column's values, from the values returned by SHOW STATUS:
|
|
|
|
Questions/Uptime_hires
|
|
|
|
innotop decides both words are barewords, and transforms this expression into
|
|
the following Perl code:
|
|
|
|
$set->{Questions}/$set->{Uptime_hires}
|
|
|
|
When surrounded by the rest of the subroutine's code, this is executable Perl
|
|
that calculates a high-resolution queries-per-second value.
|
|
|
|
The arguments to the subroutine are named $set, $cur, and $pre. In most cases,
|
|
$set and $cur will be the same values. However, if L<"status_inc"> is set, $cur
|
|
will not be the same as $set, because $set will already contain values that are
|
|
the incremental difference between $cur and $pre.
|
|
|
|
Every column in innotop is computed by subroutines compiled in the same fashion.
|
|
There is no difference between innotop's built-in columns and user-defined
|
|
columns. This keeps things consistent and predictable.
|
|
|
|
=head2 TRANSFORMATIONS
|
|
|
|
Transformations change how a value is rendered. For example, they can take a
|
|
number of seconds and display it in H:M:S format. The following transformations
|
|
are defined:
|
|
|
|
=over
|
|
|
|
=item commify
|
|
|
|
Adds commas to large numbers every three decimal places.
|
|
|
|
=item dulint_to_int
|
|
|
|
Accepts two unsigned integers and converts them into a single longlong. This is
|
|
useful for certain operations with InnoDB, which uses two integers as
|
|
transaction identifiers, for example.
|
|
|
|
=item no_ctrl_char
|
|
|
|
Removes quoted control characters from the value. This is affected by the
|
|
L<"charset"> configuration variable.
|
|
|
|
This transformation only operates within quoted strings, for example, values to
|
|
a SET clause in an UPDATE statement. It will not alter the UPDATE statement,
|
|
but will collapse the quoted string to [BINARY] or [TEXT], depending on the
|
|
charset.
|
|
|
|
=item percent
|
|
|
|
Converts a number to a percentage by multiplying it by two, formatting it with
|
|
L<"num_digits"> digits after the decimal point, and optionally adding a percent
|
|
sign (see L<"show_percent">).
|
|
|
|
=item secs_to_time
|
|
|
|
Formats a number of seconds as time in days+hours:minutes:seconds format.
|
|
|
|
=item set_precision
|
|
|
|
Formats numbers with L<"num_digits"> number of digits after the decimal point.
|
|
|
|
=item shorten
|
|
|
|
Formats a number as a unit of 1024 (k/M/G/T) and with L<"num_digits"> number of
|
|
digits after the decimal point.
|
|
|
|
=back
|
|
|
|
=head2 TABLE EDITOR
|
|
|
|
The innotop table editor lets you customize tables with keystrokes. You start
|
|
the table editor with the '^' key. If there's more than one table on the
|
|
screen, it will prompt you to choose one of them. Once you do, innotop will
|
|
show you something like this:
|
|
|
|
Editing table definition for Buffer Pool. Press ? for help, q to quit.
|
|
|
|
name hdr label src
|
|
cxn CXN Connection from which cxn
|
|
buf_pool_size Size Buffer pool size IB_bp_buf_poo
|
|
buf_free Free Bufs Buffers free in the b IB_bp_buf_fre
|
|
pages_total Pages Pages total IB_bp_pages_t
|
|
pages_modified Dirty Pages Pages modified (dirty IB_bp_pages_m
|
|
buf_pool_hit_rate Hit Rate Buffer pool hit rate IB_bp_buf_poo
|
|
total_mem_alloc Memory Total memory allocate IB_bp_total_m
|
|
add_pool_alloc Add'l Pool Additonal pool alloca IB_bp_add_poo
|
|
|
|
The first line shows which table you're editing, and reminds you again to press
|
|
'?' for a list of key mappings. The rest is a tabular representation of the
|
|
table's columns, because that's likely what you're trying to edit. However, you
|
|
can edit more than just the table's columns; this screen can start the filter
|
|
editor, color rule editor, and more.
|
|
|
|
Each row in the display shows a single column in the table you're editing, along
|
|
with a couple of its properties such as its header and source expression (see
|
|
L<"EXPRESSIONS">).
|
|
|
|
The key mappings are Vim-style, as in many other places. Pressing 'j' and 'k'
|
|
moves the highlight up or down. You can then (d)elete or (e)dit the highlighted
|
|
column. You can also (a)dd a column to the table. This actually just activates
|
|
one of the columns already defined for the table; it prompts you to choose from
|
|
among the columns available but not currently displayed. Finally, you can
|
|
re-order the columns with the '+' and '-' keys.
|
|
|
|
You can do more than just edit the columns with the table editor, you can also
|
|
edit other properties, such as the table's sort expression and group-by
|
|
expression. Press '?' to see the full list, of course.
|
|
|
|
If you want to really customize and create your own column, as opposed to just
|
|
activating a built-in one that's not currently displayed, press the (n)ew key,
|
|
and innotop will prompt you for the information it needs:
|
|
|
|
=over
|
|
|
|
=item *
|
|
|
|
The column name: this needs to be a word without any funny characters, e.g. just
|
|
letters, numbers and underscores.
|
|
|
|
=item *
|
|
|
|
The column header: this is the label that appears at the top of the column, in
|
|
the table header. This can have spaces and funny characters, but be careful not
|
|
to make it too wide and waste space on-screen.
|
|
|
|
=item *
|
|
|
|
The column's data source: this is an expression that determines what data from
|
|
the source (see L<"TABLES">) innotop will put into the column. This can just be
|
|
the name of an item in the source, or it can be a more complex expression, as
|
|
described in L<"EXPRESSIONS">.
|
|
|
|
=back
|
|
|
|
Once you've entered the required data, your table has a new column. There is no
|
|
difference between this column and the built-in ones; it can have all the same
|
|
properties and behaviors. innotop will write the column's definition to the
|
|
configuration file, so it will persist across sessions.
|
|
|
|
Here's an example: suppose you want to track how many times your slaves have
|
|
retried transactions. According to the MySQL manual, the
|
|
Slave_retried_transactions status variable gives you that data: "The total
|
|
number of times since startup that the replication slave SQL thread has retried
|
|
transactions. This variable was added in version 5.0.4." This is appropriate to
|
|
add to the L<"slave_sql_status"> table.
|
|
|
|
To add the column, switch to the replication-monitoring mode with the 'M' key,
|
|
and press the '^' key to start the table editor. When prompted, choose
|
|
slave_sql_status as the table, then press 'n' to create the column. Type
|
|
'retries' as the column name, 'Retries' as the column header, and
|
|
'Slave_retried_transactions' as the source. Now the column is created, and you
|
|
see the table editor screen again. Press 'q' to exit the table editor, and
|
|
you'll see your column at the end of the table.
|
|
|
|
=head1 VARIABLE SETS
|
|
|
|
Variable sets are used in L<"S: Variables & Status"> mode to define more easily
|
|
what variables you want to monitor. Behind the scenes they are compiled to a
|
|
list of expressions, and then into a column list so they can be treated just
|
|
like columns in any other table, in terms of data extraction and
|
|
transformations. However, you're protected from the tedious details by a syntax
|
|
that ought to feel very natural to you: a SQL SELECT list.
|
|
|
|
The data source for variable sets, and indeed the entire S mode, is the
|
|
combination of SHOW STATUS, SHOW VARIABLES, and SHOW INNODB STATUS. Imagine
|
|
that you had a huge table with one column per variable returned from those
|
|
statements. That's the data source for variable sets. You can now query this
|
|
data source just like you'd expect. For example:
|
|
|
|
Questions, Uptime, Questions/Uptime as QPS
|
|
|
|
Behind the scenes innotop will split that variable set into three expressions,
|
|
compile them and turn them into a table definition, then extract as usual. This
|
|
becomes a "variable set," or a "list of variables you want to monitor."
|
|
|
|
innotop lets you name and save your variable sets, and writes them to the
|
|
configuration file. You can choose which variable set you want to see with the
|
|
'c' key, or activate the next and previous sets with the '>' and '<' keys.
|
|
There are many built-in variable sets as well, which should give you a good
|
|
start for creating your own. Press 'e' to edit the current variable set, or
|
|
just to see how it's defined. To create a new one, just press 'c' and type its
|
|
name.
|
|
|
|
You may want to use some of the functions listed in L<"TRANSFORMATIONS"> to help
|
|
format the results. In particular, L<"set_precision"> is often useful to limit
|
|
the number of digits you see. Extending the above example, here's how:
|
|
|
|
Questions, Uptime, set_precision(Questions/Uptime) as QPS
|
|
|
|
Actually, this still needs a little more work. If your L<"interval"> is less
|
|
than one second, you might be dividing by zero because Uptime is incremental in
|
|
this mode by default. Instead, use Uptime_hires:
|
|
|
|
Questions, Uptime, set_precision(Questions/Uptime_hires) as QPS
|
|
|
|
This example is simple, but it shows how easy it is to choose which variables
|
|
you want to monitor.
|
|
|
|
=head1 PLUGINS
|
|
|
|
innotop has a simple but powerful plugin mechanism by which you can extend
|
|
or modify its existing functionality, and add new functionality. innotop's
|
|
plugin functionality is event-based: plugins register themselves to be called
|
|
when events happen. They then have a chance to influence the event.
|
|
|
|
An innotop plugin is a Perl module placed in innotop's L<"plugin_dir">
|
|
directory. On UNIX systems, you can place a symbolic link to the module instead
|
|
of putting the actual file there. innotop automatically discovers the file. If
|
|
there is a corresponding entry in the L<"plugins"> configuration file section,
|
|
innotop loads and activates the plugin.
|
|
|
|
The module must conform to innotop's plugin interface. Additionally, the source
|
|
code of the module must be written in such a way that innotop can inspect the
|
|
file and determine the package name and description.
|
|
|
|
=head2 Package Source Convention
|
|
|
|
innotop inspects the plugin module's source to determine the Perl package name.
|
|
It looks for a line of the form "package Foo;" and if found, considers the
|
|
plugin's package name to be Foo. Of course the package name can be a valid Perl
|
|
package name, with double semicolons and so on.
|
|
|
|
It also looks for a description in the source code, to make the plugin editor
|
|
more human-friendly. The description is a comment line of the form "#
|
|
description: Foo", where "Foo" is the text innotop will consider to be the
|
|
plugin's description.
|
|
|
|
=head2 Plugin Interface
|
|
|
|
The innotop plugin interface is quite simple: innotop expects the plugin to be
|
|
an object-oriented module it can call certain methods on. The methods are
|
|
|
|
=over
|
|
|
|
=item new(%variables)
|
|
|
|
This is the plugin's constructor. It is passed a hash of innotop's variables,
|
|
which it can manipulate (see L<"Plugin Variables">). It must return a reference
|
|
to the newly created plugin object.
|
|
|
|
At construction time, innotop has only loaded the general configuration and
|
|
created the default built-in variables with their default contents (which is
|
|
quite a lot). Therefore, the state of the program is exactly as in the innotop
|
|
source code, plus the configuration variables from the L<"general"> section in
|
|
the config file.
|
|
|
|
If your plugin manipulates the variables, it is changing global data, which is
|
|
shared by innotop and all plugins. Plugins are loaded in the order they're
|
|
listed in the config file. Your plugin may load before or after another plugin,
|
|
so there is a potential for conflict or interaction between plugins if they
|
|
modify data other plugins use or modify.
|
|
|
|
=item register_for_events()
|
|
|
|
This method must return a list of events in which the plugin is interested, if
|
|
any. See L<"Plugin Events"> for the defined events. If the plugin returns an
|
|
event that's not defined, the event is ignored.
|
|
|
|
=item event handlers
|
|
|
|
The plugin must implement a method named the same as each event for which it has
|
|
registered. In other words, if the plugin returns qw(foo bar) from
|
|
register_for_events(), it must have foo() and bar() methods. These methods are
|
|
callbacks for the events. See L<"Plugin Events"> for more details about each
|
|
event.
|
|
|
|
=back
|
|
|
|
=head2 Plugin Variables
|
|
|
|
The plugin's constructor is passed a hash of innotop's variables, which it can
|
|
manipulate. It is probably a good idea if the plugin object saves a copy of it
|
|
for later use. The variables are defined in the innotop variable
|
|
%pluggable_vars, and are as follows:
|
|
|
|
=over
|
|
|
|
=item action_for
|
|
|
|
A hashref of key mappings. These are innotop's global hot-keys.
|
|
|
|
=item agg_funcs
|
|
|
|
A hashref of functions that can be used for grouping. See L<"GROUPING">.
|
|
|
|
=item config
|
|
|
|
The global configuration hash.
|
|
|
|
=item connections
|
|
|
|
A hashref of connection specifications. These are just specifications of how to
|
|
connect to a server.
|
|
|
|
=item dbhs
|
|
|
|
A hashref of innotop's database connections. These are actual DBI connection
|
|
objects.
|
|
|
|
=item filters
|
|
|
|
A hashref of filters applied to table rows. See L<"FILTERS"> for more.
|
|
|
|
=item modes
|
|
|
|
A hashref of modes. See L<"MODES"> for more.
|
|
|
|
=item server_groups
|
|
|
|
A hashref of server groups. See L<"SERVER GROUPS">.
|
|
|
|
=item tbl_meta
|
|
|
|
A hashref of innotop's table meta-data, with one entry per table (see
|
|
L<"TABLES"> for more information).
|
|
|
|
=item trans_funcs
|
|
|
|
A hashref of transformation functions. See L<"TRANSFORMATIONS">.
|
|
|
|
=item var_sets
|
|
|
|
A hashref of variable sets. See L<"VARIABLE SETS">.
|
|
|
|
=back
|
|
|
|
=head2 Plugin Events
|
|
|
|
Each event is defined somewhere in the innotop source code. When innotop runs
|
|
that code, it executes the callback function for each plugin that expressed its
|
|
interest in the event. innotop passes some data for each event. The events are
|
|
defined in the %event_listener_for variable, and are as follows:
|
|
|
|
=over
|
|
|
|
=item extract_values($set, $cur, $pre, $tbl)
|
|
|
|
This event occurs inside the function that extracts values from a data source.
|
|
The arguments are the set of values, the current values, the previous values,
|
|
and the table name.
|
|
|
|
=item set_to_tbl
|
|
|
|
Events are defined at many places in this subroutine, which is responsible for
|
|
turning an arrayref of hashrefs into an arrayref of lines that can be printed to
|
|
the screen. The events all pass the same data: an arrayref of rows and the name
|
|
of the table being created. The events are set_to_tbl_pre_filter,
|
|
set_to_tbl_pre_sort,set_to_tbl_pre_group, set_to_tbl_pre_colorize,
|
|
set_to_tbl_pre_transform, set_to_tbl_pre_pivot, set_to_tbl_pre_create,
|
|
set_to_tbl_post_create.
|
|
|
|
=item draw_screen($lines)
|
|
|
|
This event occurs inside the subroutine that prints the lines to the screen.
|
|
$lines is an arrayref of strings.
|
|
|
|
=back
|
|
|
|
=head2 Simple Plugin Example
|
|
|
|
The easiest way to explain the plugin functionality is probably with a simple
|
|
example. The following module adds a column to the beginning of every table and
|
|
sets its value to 1.
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
|
|
package Innotop::Plugin::Example;
|
|
# description: Adds an 'example' column to every table
|
|
|
|
sub new {
|
|
my ( $class, %vars ) = @_;
|
|
# Store reference to innotop's variables in $self
|
|
my $self = bless { %vars }, $class;
|
|
|
|
# Design the example column
|
|
my $col = {
|
|
hdr => 'Example',
|
|
just => '',
|
|
dec => 0,
|
|
num => 1,
|
|
label => 'Example',
|
|
src => 'example', # Get data from this column in the data source
|
|
tbl => '',
|
|
trans => [],
|
|
};
|
|
|
|
# Add the column to every table.
|
|
my $tbl_meta = $vars{tbl_meta};
|
|
foreach my $tbl ( values %$tbl_meta ) {
|
|
# Add the column to the list of defined columns
|
|
$tbl->{cols}->{example} = $col;
|
|
# Add the column to the list of visible columns
|
|
unshift @{$tbl->{visible}}, 'example';
|
|
}
|
|
|
|
# Be sure to return a reference to the object.
|
|
return $self;
|
|
}
|
|
|
|
# I'd like to be called when a data set is being rendered into a table, please.
|
|
sub register_for_events {
|
|
my ( $self ) = @_;
|
|
return qw(set_to_tbl_pre_filter);
|
|
}
|
|
|
|
# This method will be called when the event fires.
|
|
sub set_to_tbl_pre_filter {
|
|
my ( $self, $rows, $tbl ) = @_;
|
|
# Set the example column's data source to the value 1.
|
|
foreach my $row ( @$rows ) {
|
|
$row->{example} = 1;
|
|
}
|
|
}
|
|
|
|
1;
|
|
|
|
=head2 Plugin Editor
|
|
|
|
The plugin editor lets you view the plugins innotop discovered and activate or
|
|
deactivate them. Start the editor by pressing $ to start the configuration
|
|
editor from any mode. Press the 'p' key to start the plugin editor. You'll see
|
|
a list of plugins innotop discovered. You can use the 'j' and 'k' keys to move
|
|
the highlight to the desired one, then press the * key to toggle it active or
|
|
inactive. Exit the editor and restart innotop for the changes to take effect.
|
|
|
|
=head1 SQL STATEMENTS
|
|
|
|
innotop uses a limited set of SQL statements to retrieve data from MySQL for
|
|
display. The statements are customized depending on the server version against
|
|
which they are executed; for example, on MySQL 5 and newer, INNODB_STATUS
|
|
executes "SHOW ENGINE INNODB STATUS", while on earlier versions it executes
|
|
"SHOW INNODB STATUS". The statements are as follows:
|
|
|
|
Statement SQL executed
|
|
=================== ===============================
|
|
INNODB_STATUS SHOW [ENGINE] INNODB STATUS
|
|
KILL_CONNECTION KILL
|
|
KILL_QUERY KILL QUERY
|
|
OPEN_TABLES SHOW OPEN TABLES
|
|
PROCESSLIST SHOW FULL PROCESSLIST
|
|
SHOW_MASTER_LOGS SHOW MASTER LOGS
|
|
SHOW_MASTER_STATUS SHOW MASTER STATUS
|
|
SHOW_SLAVE_STATUS SHOW SLAVE STATUS
|
|
SHOW_STATUS SHOW [GLOBAL] STATUS
|
|
SHOW_VARIABLES SHOW [GLOBAL] VARIABLES
|
|
|
|
=head1 DATA SOURCES
|
|
|
|
Each time innotop extracts values to create a table (see L<"EXPRESSIONS"> and
|
|
L<"TABLES">), it does so from a particular data source. Largely because of the
|
|
complex data extracted from SHOW INNODB STATUS, this is slightly messy. SHOW
|
|
INNODB STATUS contains a mixture of single values and repeated values that form
|
|
nested data sets.
|
|
|
|
Whenever innotop fetches data from MySQL, it adds two extra bits to each set:
|
|
cxn and Uptime_hires. cxn is the name of the connection from which the data
|
|
came. Uptime_hires is a high-resolution version of the server's Uptime status
|
|
variable, which is important if your L<"interval"> setting is sub-second.
|
|
|
|
Here are the kinds of data sources from which data is extracted:
|
|
|
|
=over
|
|
|
|
=item STATUS_VARIABLES
|
|
|
|
This is the broadest category, into which the most kinds of data fall. It
|
|
begins with the combination of SHOW STATUS and SHOW VARIABLES, but other sources
|
|
may be included as needed, for example, SHOW MASTER STATUS and SHOW SLAVE
|
|
STATUS, as well as many of the non-repeated values from SHOW INNODB STATUS.
|
|
|
|
=item DEADLOCK_LOCKS
|
|
|
|
This data is extracted from the transaction list in the LATEST DETECTED DEADLOCK
|
|
section of SHOW INNODB STATUS. It is nested two levels deep: transactions, then
|
|
locks.
|
|
|
|
=item DEADLOCK_TRANSACTIONS
|
|
|
|
This data is from the transaction list in the LATEST DETECTED DEADLOCK
|
|
section of SHOW INNODB STATUS. It is nested one level deep.
|
|
|
|
=item EXPLAIN
|
|
|
|
This data is from the result set returned by EXPLAIN.
|
|
|
|
=item INNODB_TRANSACTIONS
|
|
|
|
This data is from the TRANSACTIONS section of SHOW INNODB STATUS.
|
|
|
|
=item IO_THREADS
|
|
|
|
This data is from the list of threads in the the FILE I/O section of SHOW INNODB
|
|
STATUS.
|
|
|
|
=item INNODB_LOCKS
|
|
|
|
This data is from the TRANSACTIONS section of SHOW INNODB STATUS and is nested
|
|
two levels deep.
|
|
|
|
=item OPEN_TABLES
|
|
|
|
This data is from SHOW OPEN TABLES.
|
|
|
|
=item PROCESSLIST
|
|
|
|
This data is from SHOW FULL PROCESSLIST.
|
|
|
|
=item OS_WAIT_ARRAY
|
|
|
|
This data is from the SEMAPHORES section of SHOW INNODB STATUS and is nested one
|
|
level deep. It comes from the lines that look like this:
|
|
|
|
--Thread 1568861104 has waited at btr0cur.c line 424 ....
|
|
|
|
=back
|
|
|
|
=head1 MYSQL PRIVILEGES
|
|
|
|
=over
|
|
|
|
=item *
|
|
|
|
You must connect to MySQL as a user who has the SUPER privilege for many of the
|
|
functions.
|
|
|
|
=item *
|
|
|
|
If you don't have the SUPER privilege, you can still run some functions, but you
|
|
won't necessarily see all the same data.
|
|
|
|
=item *
|
|
|
|
You need the PROCESS privilege to see the list of currently running queries in Q
|
|
mode.
|
|
|
|
=item *
|
|
|
|
You need special privileges to start and stop slave servers.
|
|
|
|
=item *
|
|
|
|
You need appropriate privileges to create and drop the deadlock tables if needed
|
|
(see L<"SERVER CONNECTIONS">).
|
|
|
|
=back
|
|
|
|
=head1 SYSTEM REQUIREMENTS
|
|
|
|
You need Perl to run innotop, of course. You also need a few Perl modules: DBI,
|
|
DBD::mysql, Term::ReadKey, and Time::HiRes. These should be included with most
|
|
Perl distributions, but in case they are not, I recommend using versions
|
|
distributed with your operating system or Perl distribution, not from CPAN.
|
|
Term::ReadKey in particular has been known to cause problems if installed from
|
|
CPAN.
|
|
|
|
If you have Term::ANSIColor, innotop will use it to format headers more readably
|
|
and compactly. (Under Microsoft Windows, you also need Win32::Console::ANSI for
|
|
terminal formatting codes to be honored). If you install Term::ReadLine,
|
|
preferably Term::ReadLine::Gnu, you'll get nice auto-completion support.
|
|
|
|
I run innotop on Gentoo GNU/Linux, Debian and Ubuntu, and I've had feedback from
|
|
people successfully running it on Red Hat, CentOS, Solaris, and Mac OSX. I
|
|
don't see any reason why it won't work on other UNIX-ish operating systems, but
|
|
I don't know for sure. It also runs on Windows under ActivePerl without
|
|
problem.
|
|
|
|
innotop has been used on MySQL versions 3.23.58, 4.0.27, 4.1.0, 4.1.22, 5.0.26,
|
|
5.1.15, and 5.2.3. If it doesn't run correctly for you, that is a bug that
|
|
should be reported.
|
|
|
|
=head1 FILES
|
|
|
|
$HOMEDIR/.innotop and/or /etc/innotop are used to store
|
|
configuration information. Files include the configuration file innotop.conf,
|
|
the core_dump file which contains verbose error messages if L<"debug"> is
|
|
enabled, and the plugins/ subdirectory.
|
|
|
|
=head1 GLOSSARY OF TERMS
|
|
|
|
=over
|
|
|
|
=item tick
|
|
|
|
A tick is a refresh event, when innotop re-fetches data from connections and
|
|
displays it.
|
|
|
|
=back
|
|
|
|
=head1 ACKNOWLEDGEMENTS
|
|
|
|
The following people and organizations are acknowledged for various reasons.
|
|
Hopefully no one has been forgotten.
|
|
|
|
Allen K. Smith,
|
|
Aurimas Mikalauskas,
|
|
Bartosz Fenski,
|
|
Brian Miezejewski,
|
|
Christian Hammers,
|
|
Cyril Scetbon,
|
|
Dane Miller,
|
|
David Multer,
|
|
Dr. Frank Ullrich,
|
|
Giuseppe Maxia,
|
|
Google.com Site Reliability Engineers,
|
|
Google Code,
|
|
Jan Pieter Kunst,
|
|
Jari Aalto,
|
|
Jay Pipes,
|
|
Jeremy Zawodny,
|
|
Johan Idren,
|
|
Kristian Kohntopp,
|
|
Lenz Grimmer,
|
|
Maciej Dobrzanski,
|
|
Michiel Betel,
|
|
MySQL AB,
|
|
Paul McCullagh,
|
|
Sebastien Estienne,
|
|
Sourceforge.net,
|
|
Steven Kreuzer,
|
|
The Gentoo MySQL Team,
|
|
Trevor Price,
|
|
Yaar Schnitman,
|
|
and probably more people that have not been included.
|
|
|
|
(If your name has been misspelled, it's probably out of fear of putting
|
|
international characters into this documentation; earlier versions of Perl might
|
|
not be able to compile it then).
|
|
|
|
=head1 COPYRIGHT, LICENSE AND WARRANTY
|
|
|
|
This program is copyright (c) 2006 Baron Schwartz.
|
|
Feedback and improvements are welcome.
|
|
|
|
THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
|
|
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
|
|
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
|
|
|
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, version 2; OR the Perl Artistic License. On UNIX and similar
|
|
systems, you can issue `man perlgpl' or `man perlartistic' to read these
|
|
licenses.
|
|
|
|
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.
|
|
|
|
Execute innotop and press '!' to see this information at any time.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Originally written by Baron Schwartz; currently maintained by Aaron Racine.
|
|
|
|
=head1 BUGS
|
|
|
|
You can report bugs, ask for improvements, and get other help and support at
|
|
L<http://code.google.com/p/innotop/>. There are mailing lists, a source code
|
|
browser, a bug tracker, etc. Please use these instead of contacting the
|
|
maintainer or author directly, as it makes our job easier and benefits others if the
|
|
discussions are permanent and public. Of course, if you need to contact us in
|
|
private, please do.
|
|
|
|
=cut
|