1
0
mirror of https://gitlab.com/depesz/explain.depesz.com.git synced 2024-11-24 08:42:27 +02:00

reorganization

This commit is contained in:
depesz 2011-03-10 15:12:57 +00:00
commit 1f10ef71ff
43 changed files with 1329 additions and 0 deletions

26
LICENSE Normal file
View File

@ -0,0 +1,26 @@
Copyright (c) 2009-2010, depesz <depesz@depesz.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the depesz Software nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

24
Makefile.PL Normal file
View File

@ -0,0 +1,24 @@
# IMPORTANT: if you delete this file your app will not work as
# expected. you have been warned
use inc::Module::Install;
name 'Explain_Depesz_Com';
all_from 'lib/Explain_Depesz_Com.pm';
requires 'Catalyst::Runtime' => '5.7015';
requires 'Catalyst::Plugin::ConfigLoader';
requires 'Catalyst::Plugin::Static::Simple';
requires 'Catalyst::Plugin::SmartURI';
requires 'Catalyst::Action::RenderView';
requires 'Catalyst::Model::DBI';
requires 'Catalyst::View::Email';
requires 'parent';
requires 'Pg::Explain';
requires 'Email::Valid';
requires 'Config::General'; # This should reflect the config file format you've chosen
# See Catalyst::Plugin::ConfigLoader for supported formats
catalyst;
install_script glob('script/*.pl');
auto_install;
WriteAll;

34
explain_depesz_com.conf Normal file
View File

@ -0,0 +1,34 @@
# rename this file to Explain_Depesz_Com.yml and put a : in front of "name" if
# you want to use yaml like in old versions of Catalyst
name Explain_Depesz_Com
root_dir __HOME__
default_view TT
using_frontend_proxy 1
<Plugin::SmartURI>
disposition hostless
</Plugin::SmartURI>
<View::TT>
INCLUDE_PATH __path_to(templates)__
POST_CHOMP 1
PRE_CHOMP 1
</View::TT>
<View::Email>
stash_key email
<default>
content_type text/plain
charset utf-8
</default>
<sender>
mailer SMTP
<mailer_args>
Host 127.0.0.1
</mailer_args>
</sender>
</View::Email>
<Model::DBI>
dsn dbi:Pg:dbname=depesz_explain;host=127.0.0.1;port=5840
user depesz_explain
<options>
auto_commit 1
</options>
</Model::DBI>

62
lib/Explain_Depesz_Com.pm Normal file
View File

@ -0,0 +1,62 @@
package Explain_Depesz_Com;
use strict;
use warnings;
use Catalyst::Runtime '5.70';
# Set flags and add plugins for the application
#
# -Debug: activates the debug mode for very useful log messages
# ConfigLoader: will load the configuration from a Config::General file in the
# application's home directory
# Static::Simple: will serve static files from the application's root
# directory
use parent qw/Catalyst/;
use Catalyst qw/ ConfigLoader Static::Simple SmartURI/;
our $VERSION = '0.01';
# Configure the application.
#
# Note that settings in explain_depesz_com.conf (or other external
# configuration file that you set up manually) take precedence
# over this when using ConfigLoader. Thus configuration
# details given here can function as a default configuration,
# with a external configuration file acting as an override for
# local deployment.
__PACKAGE__->config( name => 'Explain_Depesz_Com' );
# Start the application
__PACKAGE__->setup();
=head1 NAME
Explain_Depesz_Com - Catalyst based application
=head1 SYNOPSIS
script/explain_depesz_com_server.pl
=head1 DESCRIPTION
[enter your description here]
=head1 SEE ALSO
L<Explain_Depesz_Com::Controller::Root>, L<Catalyst>
=head1 AUTHOR
hubert depesz lubaczewski,,,
=head1 LICENSE
This library is free software, you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
1;

View File

@ -0,0 +1,162 @@
package Explain_Depesz_Com::Controller::Root;
use strict;
use warnings;
use parent 'Catalyst::Controller';
use English qw( -no_match_vars );
use Email::Valid;
use Pg::Explain;
use Data::Dumper;
#
# Sets the actions in this controller to be registered with no prefix
# so they function identically to actions created in MyApp.pm
#
__PACKAGE__->config->{namespace} = '';
=head1 NAME
Explain_Depesz_Com::Controller::Root - Root Controller for Explain_Depesz_Com
=head1 DESCRIPTION
[enter your description here]
=head1 METHODS
=cut
=head2 index
=cut
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
return;
}
sub help : Local {
my ( $self, $c ) = @_;
return;
}
sub contact : Local {
my ( $self, $c ) = @_;
return unless $c->req->param('message');
return unless $c->req->param('message') =~ m{\S};
unless (Email::Valid->address( $c->req->param('email') || '' )) {
$c->stash->{'errors'}->{'bad_email'} = 1;
return;
}
my $message_body = 'Message from: ' . $c->req->param('name') . ' <' . $c->req->param('email') . '>';
$message_body .= "\nPosted from: " . $c->req->address . " with " . $c->req->header('User-Agent');
$message_body .= "\n\n***************************\n\n";
$message_body .= $c->req->param('message');
$c->stash->{'email'} = {
'header' => [
'To' => 'depesz@depesz.com',
'From' => 'depesz@depesz.com',
'Subject' => 'Contact form on explain.depesz.com',
],
'body' => $message_body,
};
$c->forward($c->view('Email'));
return;
}
sub history : Local {
my ( $self, $c ) = @_;
my @plans = $c->model('DBI')->get_public_list();
$c->stash->{'plans'} = \@plans;
return;
}
sub show : Path('s') : Args(1) {
my ( $self, $c ) = @_;
my $explain_code = $c->req->args->[0];
return $c->detach('default') unless $explain_code =~ m{\A[a-zA-Z0-9]+\z};
my $explain = eval {
Pg::Explain->new(
'source' => $c->model('DBI')->get_plan( $explain_code ),
);
};
if ($EVAL_ERROR) {
print STDERR $EVAL_ERROR;
$c->detach('default');
}
eval {
my $tmp = $explain->top_node;
};
return $c->res->redirect('/') if $EVAL_ERROR;
$c->stash->{'explain_code'} = $explain_code;
$c->stash->{'explain'} = $explain;
return;
}
sub new_explain : Path('new') : Args(0) {
my ( $self, $c ) = @_;
my $explain = $c->req->param('explain');
my $public = $c->req->param('public') ? 1 : 0;
eval {
my $e = Pg::Explain->new( 'source' => $explain, );
my $t = $e->top_node();
};
return $c->res->redirect('/') if $EVAL_ERROR;
if (length($explain) > 10_000_000) {
$c->response->body( 'Too long explain' );
$c->response->status(413);
return;
}
my $code = $c->model('DBI')->save_with_random_name( $explain, $public, );
$c->res->redirect( $c->uri_for( 's', $code ));
return;
}
sub default :Path {
my ( $self, $c ) = @_;
$c->response->body( 'Page not found' );
$c->response->status(404);
return;
}
=head2 end
Attempt to render a view, if needed.
=cut
sub end : ActionClass('RenderView') { }
=head1 AUTHOR
hubert depesz lubaczewski,,,
=head1 LICENSE
This library is free software, you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
1;

View File

@ -0,0 +1,45 @@
package Explain_Depesz_Com::Model::DBI;
use strict;
use base 'Catalyst::Model::DBI';
sub save_with_random_name {
my $self = shift;
my $content = shift;
my $is_public = shift;
my $sth = $self->dbh->prepare( 'SELECT register_plan(?, ?)' );
$sth->execute( $content, $is_public );
my @row = $sth->fetchrow_array;
$sth->finish;
return $row[ 0 ];
}
sub get_plan {
my $self = shift;
my $code = shift;
my $sth = $self->dbh->prepare( 'SELECT plan FROM plans WHERE id = ?' );
$sth->execute( $code );
my @row = $sth->fetchrow_array;
$sth->finish;
return $row[ 0 ];
}
sub get_public_list {
my $self = shift;
my @reply;
my $sth = $self->dbh->prepare( 'SELECT id, to_char(entered_on, ?) as day FROM plans WHERE is_public ORDER BY entered_on DESC' );
$sth->execute( 'YYYY-MM-DD' );
while ( my $row = $sth->fetchrow_hashref ) {
push @reply, $row;
}
$sth->finish;
return @reply;
}
1;

View File

@ -0,0 +1,33 @@
package Explain_Depesz_Com::View::Email;
use strict;
use base 'Catalyst::View::Email';
__PACKAGE__->config(
stash_key => 'email'
);
=head1 NAME
Explain_Depesz_Com::View::Email - Email View for Explain_Depesz_Com
=head1 DESCRIPTION
View for sending email from Explain_Depesz_Com.
=head1 AUTHOR
hubert depesz lubaczewski,,,
=head1 SEE ALSO
L<Explain_Depesz_Com>
=head1 LICENSE
This library is free software, you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
1;

View File

@ -0,0 +1,31 @@
package Explain_Depesz_Com::View::TT;
use strict;
use base 'Catalyst::View::TT';
__PACKAGE__->config(TEMPLATE_EXTENSION => '.tt');
=head1 NAME
Explain_Depesz_Com::View::TT - TT View for Explain_Depesz_Com
=head1 DESCRIPTION
TT View for Explain_Depesz_Com.
=head1 AUTHOR
=head1 SEE ALSO
L<Explain_Depesz_Com>
hubert depesz lubaczewski,,,
=head1 LICENSE
This library is free software, you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
1;

BIN
root/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
root/static/images/arrow_out.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,34 @@
#!/usr/bin/perl
use strict;
use warnings;
use Carp;
use English qw( -no_match_vars );
use File::Spec;
use FindBin;
use Data::Dumper;
use autodie;
use POSIX qw( strftime );
my $source_dir = File::Spec->catfile( $FindBin::Bin, '..', 'storage', );
my $dir;
opendir $dir, $source_dir;
my @all_files = grep { ! /^\./ && -f File::Spec->catfile( $source_dir, $_ ) } readdir $dir;
closedir $dir;
for my $file (@all_files) {
my $full_filename = File::Spec->catfile( $source_dir, $file );
my $mtime = (stat($full_filename))[9];
open my $fh, '<', $full_filename;
undef $/;
my $content = <$fh>;
close $fh;
printf "INSERT INTO plans (id, plan, entered_on) VALUES ('%s', \$PLAN\$%s\$PLAN\$, '%s');\n",
$file,
$content,
strftime('%Y-%m-%d %H:%M:%S', localtime $mtime);
}
exit;

View File

@ -0,0 +1,30 @@
#!/usr/bin/env perl
use Catalyst::ScriptRunner;
Catalyst::ScriptRunner->run('Explain_Depesz_Com', 'CGI');
1;
=head1 NAME
explain_depesz_com_cgi.pl - Catalyst CGI
=head1 SYNOPSIS
See L<Catalyst::Manual>
=head1 DESCRIPTION
Run a Catalyst application as a cgi script.
=head1 AUTHORS
Catalyst Contributors, see Catalyst.pm
=head1 COPYRIGHT
This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.
=cut

View File

@ -0,0 +1,57 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Catalyst::ScriptRunner;
Catalyst::ScriptRunner->run('Explain_Depesz_Com', 'Create');
1;
=head1 NAME
explain_depesz_com_create.pl - Create a new Catalyst Component
=head1 SYNOPSIS
explain_depesz_com_create.pl [options] model|view|controller name [helper] [options]
Options:
--force don't create a .new file where a file to be created exists
--mechanize use Test::WWW::Mechanize::Catalyst for tests if available
--help display this help and exits
Examples:
explain_depesz_com_create.pl controller My::Controller
explain_depesz_com_create.pl -mechanize controller My::Controller
explain_depesz_com_create.pl view My::View
explain_depesz_com_create.pl view MyView TT
explain_depesz_com_create.pl view TT TT
explain_depesz_com_create.pl model My::Model
explain_depesz_com_create.pl model SomeDB DBIC::Schema MyApp::Schema create=dynamic\
dbi:SQLite:/tmp/my.db
explain_depesz_com_create.pl model AnotherDB DBIC::Schema MyApp::Schema create=static\
dbi:Pg:dbname=foo root 4321
See also:
perldoc Catalyst::Manual
perldoc Catalyst::Manual::Intro
=head1 DESCRIPTION
Create a new Catalyst Component.
Existing component files are not overwritten. If any of the component files
to be created already exist the file will be written with a '.new' suffix.
This behavior can be suppressed with the C<-force> option.
=head1 AUTHORS
Catalyst Contributors, see Catalyst.pm
=head1 COPYRIGHT
This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.
=cut

View File

@ -0,0 +1,47 @@
#!/usr/bin/env perl
use Catalyst::ScriptRunner;
Catalyst::ScriptRunner->run('Explain_Depesz_Com', 'FastCGI');
1;
=head1 NAME
explain_depesz_com_fastcgi.pl - Catalyst FastCGI
=head1 SYNOPSIS
explain_depesz_com_fastcgi.pl [options]
Options:
-? -help display this help and exits
-l --listen Socket path to listen on
(defaults to standard input)
can be HOST:PORT, :PORT or a
filesystem path
-n --nproc specify number of processes to keep
to serve requests (defaults to 1,
requires -listen)
-p --pidfile specify filename for pid file
(requires -listen)
-d --daemon daemonize (requires -listen)
-M --manager specify alternate process manager
(FCGI::ProcManager sub-class)
or empty string to disable
-e --keeperr send error messages to STDOUT, not
to the webserver
=head1 DESCRIPTION
Run a Catalyst application as fastcgi.
=head1 AUTHORS
Catalyst Contributors, see Catalyst.pm
=head1 COPYRIGHT
This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.
=cut

View File

@ -0,0 +1,60 @@
#!/usr/bin/env perl
BEGIN {
$ENV{CATALYST_SCRIPT_GEN} = 40;
}
use Catalyst::ScriptRunner;
Catalyst::ScriptRunner->run('Explain_Depesz_Com', 'Server');
1;
=head1 NAME
explain_depesz_com_server.pl - Catalyst Test Server
=head1 SYNOPSIS
explain_depesz_com_server.pl [options]
-d --debug force debug mode
-f --fork handle each request in a new process
(defaults to false)
-? --help display this help and exits
-h --host host (defaults to all)
-p --port port (defaults to 3000)
-k --keepalive enable keep-alive connections
-r --restart restart when files get modified
(defaults to false)
-rd --restart_delay delay between file checks
(ignored if you have Linux::Inotify2 installed)
-rr --restart_regex regex match files that trigger
a restart when modified
(defaults to '\.yml$|\.yaml$|\.conf|\.pm$')
--restart_directory the directory to search for
modified files, can be set mulitple times
(defaults to '[SCRIPT_DIR]/..')
--follow_symlinks follow symlinks in search directories
(defaults to false. this is a no-op on Win32)
--background run the process in the background
--pidfile specify filename for pid file
See also:
perldoc Catalyst::Manual
perldoc Catalyst::Manual::Intro
=head1 DESCRIPTION
Run a Catalyst Testserver for this application.
=head1 AUTHORS
Catalyst Contributors, see Catalyst.pm
=head1 COPYRIGHT
This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.
=cut

View File

@ -0,0 +1,40 @@
#!/usr/bin/env perl
use Catalyst::ScriptRunner;
Catalyst::ScriptRunner->run('Explain_Depesz_Com', 'Test');
1;
=head1 NAME
explain_depesz_com_test.pl - Catalyst Test
=head1 SYNOPSIS
explain_depesz_com_test.pl [options] uri
Options:
--help display this help and exits
Examples:
explain_depesz_com_test.pl http://localhost/some_action
explain_depesz_com_test.pl /some_action
See also:
perldoc Catalyst::Manual
perldoc Catalyst::Manual::Intro
=head1 DESCRIPTION
Run a Catalyst action from the command line.
=head1 AUTHORS
Catalyst Contributors, see Catalyst.pm
=head1 COPYRIGHT
This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.
=cut

44
sql/create.sql Normal file
View File

@ -0,0 +1,44 @@
CREATE TABLE plans (
id TEXT PRIMARY KEY,
plan TEXT NOT NULL,
entered_on TIMESTAMPTZ NOT NULL DEFAULT now(),
is_public BOOL NOT NULL DEFAULT 'true'
);
CREATE OR REPLACE FUNCTION get_random_string(string_length INT4) RETURNS TEXT
AS $BODY$
DECLARE
possible_chars TEXT = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
output TEXT = '';
i INT4;
pos INT4;
BEGIN
FOR i IN 1..string_length LOOP
pos := 1 + cast( random() * ( length(possible_chars) - 1) as INT4 );
output := output || substr(possible_chars, pos, 1);
END LOOP;
RETURN output;
END;
$BODY$ LANGUAGE 'plpgsql';
CREATE OR REPLACE FUNCTION register_plan( in_plan TEXT, in_is_public bool ) RETURNS TEXT as $$
DECLARE
use_hash_length int4 := 2;
use_hash text;
BEGIN
LOOP
use_hash := get_random_string(use_hash_length);
BEGIN
INSERT INTO plans (id, plan, is_public, entered_on) values (use_hash, in_plan, in_is_public, now());
RETURN use_hash;
EXCEPTION WHEN unique_violation THEN
-- do nothing
END;
use_hash_length := use_hash_length + 1;
IF use_hash_length >= 30 THEN
raise exception 'Random string of length == 30 requested. something''s wrong.';
END IF;
END LOOP;
END;
$$ language plpgsql;

2
start_fcgi.pl Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
./script/explain_depesz_com_fastcgi.pl -l 127.0.0.1:50001 -n 2 -d -p /home/depesz/sites/Explain_Depesz_Com.pid

7
t/01app.t Normal file
View File

@ -0,0 +1,7 @@
use strict;
use warnings;
use Test::More tests => 2;
BEGIN { use_ok 'Catalyst::Test', 'Explain_Depesz_Com' }
ok( request('/')->is_success, 'Request should succeed' );

9
t/02pod.t Normal file
View File

@ -0,0 +1,9 @@
use strict;
use warnings;
use Test::More;
eval "use Test::Pod 1.14";
plan skip_all => 'Test::Pod 1.14 required' if $@;
plan skip_all => 'set TEST_POD to enable this test' unless $ENV{TEST_POD};
all_pod_files_ok();

9
t/03podcoverage.t Normal file
View File

@ -0,0 +1,9 @@
use strict;
use warnings;
use Test::More;
eval "use Test::Pod::Coverage 1.04";
plan skip_all => 'Test::Pod::Coverage 1.04 required' if $@;
plan skip_all => 'set TEST_POD to enable this test' unless $ENV{TEST_POD};
all_pod_coverage_ok();

6
t/model_DBI.t Normal file
View File

@ -0,0 +1,6 @@
use strict;
use warnings;
use Test::More tests => 1;
BEGIN { use_ok 'Explain_Depesz_Com::Model::DBI' }

6
t/view_Email.t Normal file
View File

@ -0,0 +1,6 @@
use strict;
use warnings;
use Test::More tests => 1;
BEGIN { use_ok 'Explain_Depesz_Com::View::Email' }

6
t/view_TT.t Normal file
View File

@ -0,0 +1,6 @@
use strict;
use warnings;
use Test::More tests => 1;
BEGIN { use_ok 'Explain_Depesz_Com::View::TT' }

43
templates/contact.tt Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="pl,en" />
<title>explain.depesz.com</title>
[% PROCESS scripts.tt %]
[% PROCESS styles.tt %]
</head>
<body>
<h1>=#&nbsp;<a href="http://explain.depesz.com" title="http://explain.depesz.com">explain</a>&nbsp;<a href="http://www.depesz.com" title="http://www.depesz.com">depesz.com</a>;</h1>
<div class="explain">
[% WRAPPER tabs.tt %][% END %]
<div id="contact" class="container">
<form action="[% c.uri_for('contact') %]" method="post">
<table>
[% IF errors.bad_email %]
<tr><th colspan="2" class="error">Provided email address is not valid!</th></tr>
[% ELSIF email %]
<tr><th colspan="2" class="confirmation">Your email has been sent.</th></tr>
[% END %]
<tr>
<th>Your name:</th>
<td><input type="text" name="name" size="30"/></td>
</tr>
<tr>
<th>Your email:</th>
<td><input type="text" name="email" size="30"/></td>
</tr>
<tr>
<th>Your message:</th>
<td><textarea name="message" cols="60" rows="10"></textarea></td>
</tr>
<tr>
<th colspan="2"><input type="submit" name="submit" value="Send"/></th>
</tr>
</table>
</form>
<p>Or you can simply mail me at <a href="mailto:depesz@depesz.com">depesz@depesz.com</a></p>
</div>
</div>
</body>
</html>

76
templates/help.tt Normal file
View File

@ -0,0 +1,76 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="pl,en" />
<title>explain.depesz.com</title>
[% PROCESS scripts.tt %]
[% PROCESS styles.tt %]
</head>
<body>
<h1>=#&nbsp;<a href="http://explain.depesz.com" title="http://explain.depesz.com">explain</a>&nbsp;<a href="http://www.depesz.com" title="http://www.depesz.com">depesz.com</a>;</h1>
<div class="explain">
[% WRAPPER tabs.tt %][% END %]
<div id="help" class="container">
<p>explain.depesz.com is tool for finding real cause for slow queries.</p>
<p>Generally, one would use EXPLAIN ANALYZE query; and read the output. The problem is that not all
parts of the output are easily understandable by anybody, and it's not always obvious whether node
that executes in 17.3ms is faster or slower than the one that runs in 100ms - given the fact that
the first one is executed 7 times.</p>
<p>To use the site, simply go to its <a href="/">first page</a> and paste there explain analyze output from your psql.</p>
<p>This output could look like <a href="/s/ME#text">this</a>.</p>
<p>After uploading you will be directed to page which shows parsed, and nicely (well, at least nice for me :) colorized to put emphasis on important parts. This could look like <a href="/s/lB">this</a>.</p>
<p>Side note: the url for colorized output is persistent, so you can simply use it to show it to
others - for example - for those nice guys on irc channel #postgresql on freenode.</p>
<p>This graph uses 4 colours to mark important things:</p>
<ul>
<li class="example1">white background - everything is fine</li>
<li class="example2">yellow background - given node is worrying</li>
<li class="example3">brown background - given node is more worrying</li>
<li class="example4">red background - given node is very worrying</li>
</ul>
<p>Which color is used, is choosen based on which mode you will use: "Exclusive", "Inclusive" or "Rows X".</p>
<p>Their meaning:</p>
<h2>Exclusive</h2>
<p>This is total amount of time PostgreSQL spent evaluating this node, without time spent in its subnodes. If the node has been executed many times (for example because of Nested Loop plan), this time will be correctly multiplied.</p>
<p>Colors:</p>
<ul>
<li class="example1">white background - is choosen if exclusive time &lt;= 10% of total query time</li>
<li class="example2">yellow background - is choosen if exclusive time &isin; (10%, 50%&gt; of total query time</li>
<li class="example3">brown background - is choosen if exclusive time &isin; (50%, 90%&gt; of total query time</li>
<li class="example4">red background - is choosen if exclusive time &gt; 90% of total query time</li>
</ul>
<h2>Inclusive</h2>
<p>This is just like Exclusive, but it doesn't exclude time of subnodes. So, by definition - top node will have Inclusive time equal to total time of query.</p>
<p>Colors:</p>
<ul>
<li class="example1">white background - is choosen if inclusive time &lt;= 10% of total query time</li>
<li class="example2">yellow background - is choosen if inclusive time &isin; (10%, 50%&gt; of total query time</li>
<li class="example3">brown background - is choosen if inclusive time &isin; (50%, 90%&gt; of total query time</li>
<li class="example4">red background - is choosen if inclusive time &gt; 90% of total query time</li>
</ul>
<h2>Rows X</h2>
<p>This value stores information about how big was planner mistake when it estimated return row count.</p>
<p>For example - if planner estimated that given node will return 230 rows, but it returned 14118 rows, the error is 14118/230 == 61.4.</p>
<p>It has to be noted that if the numbers were the other way around (estimated 14118, but really only 230), the Rows X would be the same. To show whether planner underestimated or overestimated - there is an arrow showing either &darr; - if planner underestimated rowcount, or &uarr; if it overestimated.</p>
<p>Colors:</p>
<ul>
<li class="example1">white background - is choosen if rows-x &lt;= 10</li>
<li class="example2">yellow background - is choosen if rows-x &isin; (10, 100&gt;</li>
<li class="example3">brown background - is choosen if rows-x &isin; (100, 1000&gt;</li>
<li class="example4">red background - is choosen if rows-x &gt; 1000</li>
</ul>
</div>
</div>
</body>
</html>

41
templates/history.tt Normal file
View File

@ -0,0 +1,41 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="pl,en" />
<title>explain.depesz.com</title>
[% PROCESS scripts.tt %]
[% PROCESS styles.tt %]
</head>
[% previous_day = plans.0.day %]
[% jsarr = previous_day %]
[% FOREACH plan IN plans %]
[% day = plan.day %]
[% IF day != previous_day %]
[% jsarr = jsarr _ ':' _ day %]
[% previous_day = day %]
[% END %]
[% jsarr = jsarr _ ',' _ plan.id %]
[% END %]
<body>
<h1>=#&nbsp;<a href="http://explain.depesz.com" title="http://explain.depesz.com">explain</a>&nbsp;<a href="http://www.depesz.com" title="http://www.depesz.com">depesz.com</a>;</h1>
<div class="explain">
[% WRAPPER tabs.tt %][% END %]
<div id="history" class="container">
<script type="text/javascript">
var x = '[% jsarr %]';
var d = x.split(':');
var dl = d.length;
for (i=0; i<dl; i++) {
var j = d[i].split(',');
var jl = j.length;
document.write('<h2>' + j[0] + '</h2><p>');
for (k=1; k<jl; k++) {
document.write('<a href="/s/' + j[k] + '">' + j[k] + '</a>' + ( k == jl -1 ? '.</p>' : ', ' ) );
}
}
</script>
</div>
</div>
</body>
</html>

24
templates/index.tt Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="pl,en" />
<title>explain.depesz.com</title>
[% PROCESS scripts.tt %]
[% PROCESS styles.tt %]
</head>
<body>
<h1>=#&nbsp;<a href="http://explain.depesz.com" title="http://explain.depesz.com">explain</a>&nbsp;<a href="http://www.depesz.com" title="http://www.depesz.com">depesz.com</a>;</h1>
<div class="explain">
[% WRAPPER tabs.tt %][% END %]
<div id="new" class="container">
<h2>Paste your explain / explain analyze here:</h2>
<form action="/new" method="post">
<textarea cols="132" rows="40" name="explain"></textarea><br/>
I want this plan to be visible on <a href="/history">previous explains</a> page:<input type="checkbox" name="public" value="1" checked="1"/><br/>
<input type="submit" name="submit" value="submit"/>
</form>
</div>
</div>
</body>
</html>

117
templates/scripts.tt Normal file
View File

@ -0,0 +1,117 @@
<script type="text/javascript">
var Explain = {
_t : null,
_m : ['text','html'],
_c : ['ex','in','rx'],
init : function(i){
this._t = document.getElementById('explain');
var m = this._readMode();
this.mode(m);
if(m != 'html')
return;
var c = this._readColumn();
this.mark(c);
},
mode : function(m){
for(var i=0;i<this._m.length;i++){
try{
document.getElementById(this._m[i]).style.display = (this._m[i] == m ? 'block' : 'none');
}catch(e){}
}
},
mark : function(cn){
var bs = this._t.getElementsByTagName('tbody');
var vi = -1;
for(var i=0;i<this._c.length;i++){
if(this._c[i] == cn){
vi = i;
break;
}
}
if(vi < 0) return;
for(var i=0;i<bs.length;i++){
var rs = bs[i].getElementsByTagName('tr');
for(var j=0;j<rs.length;j++){
var rl = rs[j].getAttribute('rel');
if(!rl)
continue;
if(!(rl.match(/^\d+(;\d+)*$/g)))
continue;
var cs = rl.split(';');
if((cs.lenght - 1) < vi)
continue;
rs[j].className = cn + '-' + cs[vi];
}
}
},
highlight : function ( row, state ) {
var rs = row.parentNode.getElementsByTagName( 'tr' );
var pl = parseInt( new String( row.getAttribute( 'rel' ) ).replace( /^.*;/, '' ) );
var sm = false;
for( var i = 0 ; i < rs.length ; i++ ) {
if ( rs[ i ] == row ) {
sm = true;
continue;
}
if ( sm ) {
var cl = parseInt( new String( rs[ i ].getAttribute( 'rel' ) ).replace( /.*;/, '' ) );
if ( cl <= pl )
break;
if ( state && cl == pl + 1 ) rs[ i ].className += ' highlight';
if ( !state ) rs[ i ].className = new String( rs[ i ].className ).replace( /\s+highlight/, '' );
}
}
return;
},
collapse : function ( row ) {
var ci = new String( row.className ).indexOf( 'collapsed' );
var cs = '';
if ( ci > -1 ) {
row.className = new String( row.className ).replace( /\s+collapsed/i, '' );
cs = '';
} else {
row.className += ' collapsed';
cs = 'none';
}
var rs = row.parentNode.getElementsByTagName( 'tr' );
var pl = parseInt( new String( row.getAttribute( 'rel' ) ).replace( /^.*;/, '' ) );
var sm = false;
for( var i = 0 ; i < rs.length ; i++ ) {
if ( rs[ i ] == row ) {
sm = true;
continue;
}
if ( sm ) {
var cl = parseInt( new String( rs[ i ].getAttribute( 'rel' ) ).replace( /.*;/, '' ) );
if ( cl <= pl )
break;
rs[ i ].style.display = cs;
rs[ i ].className = new String( rs[ i ].className ).replace( /\s+collapsed/i, '' );
}
}
return;
},
_readMode : function(){
return (new String(window.location.href).match(/#text/) ? 'text' : 'html');
},
_readColumn : function(){
var c = new String(window.location.href).replace(/.*#/, '');
for(var i=0;i<this._c.length;i++){
if(c == this._c[i])
return c;
}
return this._c[0];
}
};
</script>

43
templates/show.tt Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
[% row_class = 'odd' %]
[% prefix = "&nbsp;&nbsp;&nbsp;&nbsp;" %]
[% PROCESS show_macro.tt %]
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>explain.depesz.com</title>
[% PROCESS scripts.tt %]
[% PROCESS styles.tt %]
</head>
<body onload="Explain.init();">
<h1>=#&nbsp;<a href="http://explain.depesz.com" title="http://explain.depesz.com">explain</a>&nbsp;<a href="http://www.depesz.com" title="http://www.depesz.com">depesz.com</a>;</h1>
<div class="explain">
[% WRAPPER tabs.tt %]
<li><a href="#" onclick="Explain.mode('text'); return false;">text</a></li>
<li><a href="#" onclick="Explain.mode('html'); return false;">html</a></li>
[% END %]
<div id="text" class="container" style="display:none">
<pre>[% explain.source | html %]</pre>
</div>
<div id="html" class="container">
<table id="explain" cellspacing="0" cellpadding="0">
<thead>
<tr>
<th><a href="#" onclick="Explain.mark('ex'); return false;">Exclusive</a></th>
<th><a href="#" onclick="Explain.mark('in'); return false;">Inclusive</a></th>
<th><a href="#" onclick="Explain.mark('rx'); return false;">Rows x</a></th>
<th>Node</th>
</tr>
<tr>
</tr>
</thead>
<tbody>
[% print_row( explain.top_node, 0 ) %]
</tbody>
</table>
</div>
</div>
</body>
</html>

108
templates/show_macro.tt Normal file
View File

@ -0,0 +1,108 @@
[% MACRO print_row(node, level) BLOCK %]
[% exclusive_point = 1 %]
[% inclusive_point = 1 %]
[% IF explain.top_node.total_inclusive_time != '' AND explain.top_node.total_inclusive_time > 0 AND node.total_exclusive_time != '' AND node.total_inclusive_time != '' %]
[% exclusive_point = node.total_exclusive_time / explain.top_node.total_inclusive_time %]
[% IF exclusive_point > 0.9 %]
[% exclusive_point = 4 %]
[% ELSIF exclusive_point > 0.5 %]
[% exclusive_point = 3 %]
[% ELSIF exclusive_point > 0.1 %]
[% exclusive_point = 2 %]
[% ELSE %]
[% exclusive_point = 1 %]
[% END %]
[% inclusive_point = node.total_inclusive_time / explain.top_node.total_inclusive_time %]
[% IF inclusive_point > 0.9 %]
[% inclusive_point = 4 %]
[% ELSIF inclusive_point > 0.5 %]
[% inclusive_point = 3 %]
[% ELSIF inclusive_point > 0.1 %]
[% inclusive_point = 2 %]
[% ELSE %]
[% inclusive_point = 1 %]
[% END %]
[% END %]
[% rows_x = 0 %]
[% rows_x_mark = '' %]
[% rows_point = 1 %]
[% IF node.estimated_rows != '' && node.actual_rows != '' && node.estimated_rows > 0 && node.actual_rows > 0 %]
[% IF node.actual_rows > node.estimated_rows %]
[% rows_x = node.actual_rows / node.estimated_rows %]
[% rows_x_mark = '&darr;' %]
[% ELSE %]
[% rows_x = node.estimated_rows / node.actual_rows %]
[% rows_x_mark = '&uarr;' %]
[% END %]
[% IF rows_x > 1000 %]
[% rows_point = 4 %]
[% ELSIF rows_x > 100 %]
[% rows_point = 3 %]
[% ELSIF rows_x > 10 %]
[% rows_point = 2 %]
[% ELSE %]
[% rows_point = 1 %]
[% END %]
[% END %]
<tr class="[% row_class %]" rel="[% exclusive_point %];[% inclusive_point %];[% rows_point %];[% level %]" onmouseover="Explain.highlight( this, 1 );" onmouseout="Explain.highlight( this, 0 );" onclick="Explain.collapse( this );">
[% row_class = ( row_class == 'odd' ? 'even' : 'odd' ) %]
<td>[% node.total_exclusive_time | format("%.3f") %]</td>
<td>[% node.total_inclusive_time | format("%.3f") %]</td>
<td>[% rows_x_mark %] [% rows_x | format("%.1f") %]</td>
<td class="content">
<div style="margin-left:[% IF (level > 0) %][% (level + 2) %][% ELSE %]2[% END %]em">
[% IF (level > 0) %]
<span>-&gt;</span>
[% END %]
[% node.type %]
[% IF node.type == "Bitmap Heap Scan" %] on [% node.scan_on.table_name %]&nbsp;[% node.scan_on.table_alias %][% END %]
[% IF node.type == "Bitmap Index Scan" %] on [% node.scan_on.index_name %][% END %]
[% IF node.type == "Index Scan" %] using [% node.scan_on.index_name %] on [% node.scan_on.table_name %]&nbsp;[% node.scan_on.table_alias %][% END %]
[% IF node.type == "Seq Scan" %] on [% node.scan_on.table_name %]&nbsp;[% node.scan_on.table_alias %][% END %]
(cost=[% node.estimated_startup_cost %]..[% node.estimated_total_cost %] rows=[% node.estimated_rows %] width=[% node.estimated_row_width %])
(actual time=[% node.actual_time_first %]..[% node.actual_time_last %] rows=[% node.actual_rows %] loops=[% node.actual_loops %])
[% IF node.extra_info %]
<ul>
[% FOREACH line IN node.extra_info %]<li>[% line | html %]</li>[% END %]
<ul>
[% END %]
</div>
</td>
</tr>
[% IF node.initplans %]
<tr class="[% row_class %]">
<td colspan="3">
</td>
<td class="content">
<div style="margin-left:[% IF (level > 0) %][% (level + 2) %][% ELSE %]2[% END %]em">
InitPlan ( for [% node.type %] )
</div>
</td>
</tr>
[% FOREACH subnode IN node.initplans %]
[% print_row(subnode, level + 1) %]
[% END %]
[% END %]
[% FOREACH subnode IN node.sub_nodes %]
[% print_row(subnode, level + 1) %]
[% END %]
[% IF node.subplans %]
<tr>
<td colspan="3">
</td>
<td class="content">
<div style="margin-left:[% IF (level > 0) %][% (level + 2) %][% ELSE %]2[% END %]em">
SubPlan ( for [% node.type %] )
</div>
</td>
</tr>
[% FOREACH subnode IN node.subplans %]
[% print_row(subnode, level + 1) %]
[% END %]
[% END %]
[% END %]

96
templates/styles.tt Normal file
View File

@ -0,0 +1,96 @@
<style type="text/css">
html {font-size:100%}
body,h1,h2,h3,h4,h5,h6,div,p,ul,ol,dl,li,dt,dd,form,input,select,label,legend,fieldset,img,hr {margin:0;padding:0;font-size:1em;text-align:left;font-style:normal}
body {font-size:62.5%;font-family:Arial,Tahoma,Verdana,sans-serif}
a {text-decoration:underline}
a:hover {text-decoration:none}
p {padding-bottom:0.7em}
hr {display:none}
img {border:0}
/* */
* html .clear {position:/**/relative}
.clear {display:inline-block}
/* */
.clear:after {content:'.';display:block;height:0;line-height:0;visibility:hidden;clear:both}
.clear {display:inline-block}
/*\*/
* html .clear {float:left;float:/**/none}
.clear {display:block}
/* */
body {padding:10px 0}
h1 {font-size:3em;margin:10px;font-family:'dejavu sans mono;courier;monospace'}
h2 {font-size:1.5em;margin:10px}
h1 a {color:#000; text-decoration: none}
.explain {margin:0 10px;padding-bottom:50px;border:1px solid #384959;background:#ddd}
ul.tabs {display:block;list-style:none;background:#73859d;border-top:1px solid #cbd8e9;border-bottom:1px solid #384959}
ul.tabs li {display:inline;line-height:3em}
ul.tabs li a {margin:0 2px;font-size:1.2em;padding:5px;color:#485969;text-decoration:none;border:1px solid #384959;background:#fff}
.container {position:relative;top:20px;padding:5px 0;background:#fff}
.container pre, .container table {margin:0 10px}
.container pre {font-size:1.6em}
.explain table {border-collapse:collapse;border:none}
.explain th, .explain td {padding:0.2em 0.1em;font-size:1.4em;white-space:nowrap}
.explain th, .explain td {border-right:1px dotted #ccc;padding-left:8px;padding-right:8px}
.explain td.content {border-right:none;padding-left:0px;padding-right:0px}
.explain td {border-top:1px dotted #ccc;text-align:right}
.explain td.content {text-align:left}
.explain td.content div {position:relative}
.explain td.content span {position:absolute;top:0;left:-1.2em}
.explain td.content ul {list-style:none}
.explain td li {display:block}
.explain tr {background:#fff}
.explain tr:hover {background:#ccc}
tr.ex-1 {background:#fff}
tr.ex-2 {background:#FFFF66}
tr.ex-3 {background:#FF8033}
tr.ex-4 {background:#FF0000}
tr.in-1 {background:#fff}
tr.in-2 {background:#FFFF66}
tr.in-3 {background:#FF8033}
tr.in-4 {background:#FF0000}
tr.rx-1 {background:#fff}
tr.rx-2 {background:#FFFF66}
tr.rx-3 {background:#FF8033}
tr.rx-4 {background:#FF0000}
li.example1 {margin-left: 2em; background:#fff}
li.example2 {margin-left: 2em; background:#FFFF66}
li.example3 {margin-left: 2em; background:#FF8033}
li.example4 {margin-left: 2em; background:#FF0000}
a.new {margin-left:10px;font-size:1.4em;line-height:2.2em;color:#8090a0;text-decoration:none}
a.new b {margin-left:6px;font-weight:normal;padding:0.1em 0;border-bottom:1px dotted #8090a0}
a.new:hover {color:#154565}
a.new:hover b {border-bottom:1px solid #154565}
input { font-size: 1.5em }
textarea { font-size: 1.5em }
form {padding-left: 1em }
#history h2 {font-size:1.4em;font-style:italic;font-weight:normal;color:#555}
#history p {margin-left:3em}
#history p a {font-size:1.4em;font-weight:bold;color:#73859d;text-decoration:none}
#help h2 {font-size:1.4em;font-style:italic;font-weight:normal;color:#555}
#help p {margin-left:1em; font-size: 1.4em}
#help p a {font-weight:bold;color:#73859d;text-decoration:none}
#help ul li {font-size:1.4em}
#help dl {padding-left: 1em; font-size: 1.4em}
#help dl dt {font-weight: bold; font-size: 1.2em}
#help dl dd p {0.7em}
#contact table th { text-align: right }
#contact table td { text-align: left }
#contact table th.error { text-align: center; font-size: 2em; background: #f88}
#contact table th.confirmation { text-align: center; font-size: 2em; background: #8f8}
#contact p {margin-left:1em; font-size: 1.4em}
.explain tr.highlight td.content { background:url( '/static/images/asterisk_orange.png' ) no-repeat 2px 2px }
.explain tr.collapsed td.content { background:url( '/static/images/arrow_out.png' ) no-repeat 2px 2px }
</style>

7
templates/tabs.tt Normal file
View File

@ -0,0 +1,7 @@
<ul class="tabs">
<li><a href="/">new explain</a></li>
[% content %]
<li><a href="/history">previous explains</a></li>
<li><a href="/contact">contact</a></li>
<li><a href="/help">help</a></li>
</ul>