Commit 26b1c3e1 authored by Hubert depesz Lubaczewski's avatar Hubert depesz Lubaczewski
Browse files

Release version 0.75

Change inclusive time calculation for parallel nodes to calculate wall
clock time, and not total time used.
Problem reported by Bricklen Anderson.

In process also fix anonymization of parallel nodes.
parent b388de33
Revision history for Pg-Explain
0.75 2017/11/29
- Change inclusive time calculation for parallel nodes to calculate
wall clock time, and not total time used.
Problem reported by Bricklen Anderson.
0.74 2017/06/20
- Fix extracting subquery name (problem reported by Jackson Popkin)
- Switch from using Digest::SHA1 to Digest::SHA
......
......@@ -117,6 +117,8 @@ t/26-explain-with-no-timing.t
t/27-anonymization-of-subquery-scans.t
t/28-anonymization-of-group-keys.t
t/29-extract-subquery-source.t
t/30-parallel-query.t
t/31-parallel-query-2.t
t/99-manifest.t
t/perlcriticrc
t/perltidyrc
......
......@@ -42,35 +42,35 @@
"provides" : {
"Pg::Explain" : {
"file" : "lib/Pg/Explain.pm",
"version" : "0.74"
"version" : "0.75"
},
"Pg::Explain::From" : {
"file" : "lib/Pg/Explain/From.pm",
"version" : "0.74"
"version" : "0.75"
},
"Pg::Explain::FromJSON" : {
"file" : "lib/Pg/Explain/FromJSON.pm",
"version" : "0.74"
"version" : "0.75"
},
"Pg::Explain::FromText" : {
"file" : "lib/Pg/Explain/FromText.pm",
"version" : "0.74"
"version" : "0.75"
},
"Pg::Explain::FromXML" : {
"file" : "lib/Pg/Explain/FromXML.pm",
"version" : "0.74"
"version" : "0.75"
},
"Pg::Explain::FromYAML" : {
"file" : "lib/Pg/Explain/FromYAML.pm",
"version" : "0.74"
"version" : "0.75"
},
"Pg::Explain::Node" : {
"file" : "lib/Pg/Explain/Node.pm",
"version" : "0.74"
"version" : "0.75"
},
"Pg::Explain::StringAnonymizer" : {
"file" : "lib/Pg/Explain/StringAnonymizer.pm",
"version" : "0.74"
"version" : "0.75"
}
},
"release_status" : "stable",
......@@ -79,6 +79,6 @@
"http://dev.perl.org/licenses/"
]
},
"version" : "0.74",
"version" : "0.75",
"x_serialization_backend" : "JSON::PP version 2.27300"
}
......@@ -19,28 +19,28 @@ name: Pg-Explain
provides:
Pg::Explain:
file: lib/Pg/Explain.pm
version: '0.74'
version: '0.75'
Pg::Explain::From:
file: lib/Pg/Explain/From.pm
version: '0.74'
version: '0.75'
Pg::Explain::FromJSON:
file: lib/Pg/Explain/FromJSON.pm
version: '0.74'
version: '0.75'
Pg::Explain::FromText:
file: lib/Pg/Explain/FromText.pm
version: '0.74'
version: '0.75'
Pg::Explain::FromXML:
file: lib/Pg/Explain/FromXML.pm
version: '0.74'
version: '0.75'
Pg::Explain::FromYAML:
file: lib/Pg/Explain/FromYAML.pm
version: '0.74'
version: '0.75'
Pg::Explain::Node:
file: lib/Pg/Explain/Node.pm
version: '0.74'
version: '0.75'
Pg::Explain::StringAnonymizer:
file: lib/Pg/Explain/StringAnonymizer.pm
version: '0.74'
version: '0.75'
requires:
Clone: '0'
Digest::SHA: '0'
......@@ -51,5 +51,5 @@ requires:
perl: '5.010'
resources:
license: http://dev.perl.org/licenses/
version: '0.74'
version: '0.75'
x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
......@@ -11,11 +11,11 @@ Pg::Explain - Object approach at reading explain analyze output
=head1 VERSION
Version 0.74
Version 0.75
=cut
our $VERSION = '0.74';
our $VERSION = '0.75';
=head1 SYNOPSIS
......@@ -168,6 +168,8 @@ sub parse_source {
$self->{ 'top_node' } = Pg::Explain::FromText->new()->parse_source( $source );
}
$self->{ 'top_node' }->check_for_parallelism();
return;
}
......
......@@ -9,11 +9,11 @@ Pg::Explain::From - Base class for parsers of non-text explain formats.
=head1 VERSION
Version 0.74
Version 0.75
=cut
our $VERSION = '0.74';
our $VERSION = '0.75';
=head1 SYNOPSIS
......
......@@ -10,11 +10,11 @@ Pg::Explain::FromJSON - Parser for explains in JSON format
=head1 VERSION
Version 0.74
Version 0.75
=cut
our $VERSION = '0.74';
our $VERSION = '0.75';
=head1 SYNOPSIS
......
......@@ -9,11 +9,11 @@ Pg::Explain::FromText - Parser for text based explains
=head1 VERSION
Version 0.74
Version 0.75
=cut
our $VERSION = '0.74';
our $VERSION = '0.75';
=head1 SYNOPSIS
......
......@@ -10,11 +10,11 @@ Pg::Explain::FromXML - Parser for explains in XML format
=head1 VERSION
Version 0.74
Version 0.75
=cut
our $VERSION = '0.74';
our $VERSION = '0.75';
=head1 SYNOPSIS
......
......@@ -10,11 +10,11 @@ Pg::Explain::FromYAML - Parser for explains in YAML format
=head1 VERSION
Version 0.74
Version 0.75
=cut
our $VERSION = '0.74';
our $VERSION = '0.75';
=head1 SYNOPSIS
......
......@@ -12,11 +12,11 @@ Pg::Explain::Node - Class representing single node from query plan
=head1 VERSION
Version 0.74
Version 0.75
=cut
our $VERSION = '0.74';
our $VERSION = '0.75';
=head1 SYNOPSIS
......@@ -76,6 +76,12 @@ Returns estimated full cost of given node.
This cost is measured in units of "single-page seq scan".
=head2 force_loops
Stores/returns number of "forced loops". In case of parallel plans, despite having loops=<some_number> in some "parallel node", we should use loops=X from nearest parent Gather node.
This is for calculation of total_inclusive_time and total_exclusive_time only.
=head2 type
Textual representation of type of current node. Some types for example:
......@@ -159,6 +165,7 @@ sub estimated_row_width { my $self = shift; $self->{ 'estimated_row_width' }
sub estimated_startup_cost { my $self = shift; $self->{ 'estimated_startup_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_startup_cost' }; }
sub estimated_total_cost { my $self = shift; $self->{ 'estimated_total_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_total_cost' }; }
sub extra_info { my $self = shift; $self->{ 'extra_info' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'extra_info' }; }
sub force_loops { my $self = shift; $self->{ 'force_loops' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'force_loops' }; }
sub initplans { my $self = shift; $self->{ 'initplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans' }; }
sub never_executed { my $self = shift; $self->{ 'never_executed' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'never_executed' }; }
sub scan_on { my $self = shift; $self->{ 'scan_on' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'scan_on' }; }
......@@ -205,7 +212,7 @@ sub new {
croak( 'estimated_total_cost has to be passed to constructor of explain node' ) unless defined $self->estimated_total_cost;
croak( 'type has to be passed to constructor of explain node' ) unless defined $self->type;
if ( $self->type =~ m{ \A ( Seq \s Scan | Bitmap \s+ Heap \s+ Scan | Foreign \s+ Scan | Update | Insert | Delete ) \s on \s (\S+) (?: \s+ (\S+) ) ? \z }xms ) {
if ( $self->type =~ m{ \A ( (?: Parallel \s+ )? (?: Seq \s Scan | Bitmap \s+ Heap \s+ Scan | Foreign \s+ Scan | Update | Insert | Delete ) ) \s on \s (\S+) (?: \s+ (\S+) ) ? \z }xms ) {
$self->type( $1 );
$self->scan_on( { 'table_name' => $2, } );
$self->scan_on->{ 'table_alias' } = $3 if defined $3;
......@@ -449,6 +456,34 @@ sub get_struct {
return $reply;
}
=head2 check_for_parallelism
Handles parallelism by setting "override_loops" if plan is analyzed and there are gather nodes.
=cut
sub check_for_parallelism {
my $self = shift;
my $force_loops = shift;
if ( 'Gather' eq $self->type ) {
$force_loops = $self->actual_loops;
}
else {
$self->{ 'force_loops' } = $force_loops;
}
for my $key ( qw( sub_nodes initplans subplans ) ) {
next unless $self->{ $key };
$_->check_for_parallelism( $force_loops ) for @{ $self->{ $key } };
}
if ( $self->{ 'ctes' } ) {
$_->check_for_parallelism( $force_loops ) for values %{ $self->{ 'ctes' } };
}
return;
}
=head2 total_inclusive_time
Method for getting total node time, summarized with times of all subnodes, subplans and initplans - which is basically ->actual_loops * ->actual_time_last.
......@@ -457,9 +492,14 @@ Method for getting total node time, summarized with times of all subnodes, subpl
sub total_inclusive_time {
my $self = shift;
return unless $self->actual_loops;
return unless defined $self->actual_time_last;
return $self->actual_loops * $self->actual_time_last;
if ( defined $self->{ 'force_loops' } ) {
return $self->actual_time_last * $self->{ 'force_loops' };
}
elsif ( $self->actual_loops ) {
return $self->actual_loops * $self->actual_time_last;
}
return;
}
=head2 total_exclusive_time
......
......@@ -11,11 +11,11 @@ Pg::Explain::StringAnonymizer - Class to anonymize sets of strings
=head1 VERSION
Version 0.74
Version 0.75
=cut
our $VERSION = '0.74';
our $VERSION = '0.75';
=head1 SYNOPSIS
......
#!perl
use Test::More;
use Test::Deep;
use Test::Exception;
use Data::Dumper;
use autodie;
use Pg::Explain;
plan 'tests' => 10;
my $explain = Pg::Explain->new(
'source' => q{
Finalize GroupAggregate (cost=158141.99..158145.24 rows=100 width=12) (actual time=770.753..770.766 rows=10 loops=1)
Group Key: c_100
-> Sort (cost=158141.99..158142.74 rows=300 width=12) (actual time=770.747..770.751 rows=40 loops=1)
Sort Key: c_100
Sort Method: quicksort Memory: 27kB
-> Gather (cost=158098.64..158129.64 rows=300 width=12) (actual time=769.859..770.724 rows=40 loops=1)
Workers Planned: 3
Workers Launched: 3
-> Partial HashAggregate (cost=157098.64..157099.64 rows=100 width=12) (actual time=765.184..765.188 rows=10 loops=4)
Group Key: c_100
-> Parallel Bitmap Heap Scan on p1 (cost=37639.11..153855.63 rows=648602 width=4) (actual time=242.999..600.416 rows=500000 loops=4)
Recheck Cond: (c_100 < 10)
Heap Blocks: exact=31663
-> Bitmap Index Scan on idx_p1 (cost=0.00..37136.44 rows=2010667 width=0) (actual time=213.409..213.409 rows=2000000 loops=1)
Index Cond: (c_100 < 10)
}
);
isa_ok( $explain, 'Pg::Explain' );
isa_ok( $explain->top_node, 'Pg::Explain::Node' );
is( $explain->top_node->type, 'Finalize GroupAggregate', 'Properly extracted top node type' );
is( $explain->top_node->sub_nodes->[0]->type, 'Sort', 'Properly extracted subnode-1' );
is( $explain->top_node->sub_nodes->[0]->sub_nodes->[0]->type, 'Gather', 'Properly extracted subnode-2' );
is( $explain->top_node->sub_nodes->[0]->sub_nodes->[0]->sub_nodes->[0]->type, 'Partial HashAggregate', 'Properly extracted subnode-3' );
my $pha = $explain->top_node->sub_nodes->[0]->sub_nodes->[0]->sub_nodes->[0];
is( $pha->total_inclusive_time, 765.188, "Inclusive time is calculated properly for parallel nodes" );
is( $pha->total_exclusive_time, 765.188 - 600.416, "Exclusive time is calculated properly for parallel nodes" );
lives_ok(
sub {
$explain->anonymize();
},
'Anonymization works',
);
ok( $explain->as_text !~ /p1/, 'anonymize() hides table names' );
exit;
#!perl
use Test::More;
use Test::Deep;
use Test::Exception;
use Data::Dumper;
use autodie;
use Pg::Explain;
plan 'tests' => 12;
my $explain = Pg::Explain->new(
'source' => q{
GroupAggregate (cost=522663.12..522663.20 rows=1 width=2301) (actual time=2128381.203..2128625.705 rows=217674 loops=1)
Output: m.id, m.mes, m.regiao, m.area_nielsen, m.ae, m.tipo_cliente, m.cnpj_cliente, m.qtd_itens_cobrados, m.target, m.qtd_itens_vendidos, m.faltantes, m.cliente_compliance, m.dta_inclusao, m.mesorregiao, m.microrregiao, m.commodity, (concat(m.cnpj_clien (...)
Group Key: m.id, m.mes, m.regiao, m.area_nielsen, m.ae, m.tipo_cliente, m.cnpj_cliente, m.qtd_itens_cobrados, m.target, m.qtd_itens_vendidos, m.faltantes, m.cliente_compliance, m.dta_inclusao, m.mesorregiao, m.microrregiao, m.commodity, (concat(m.cnpj_cl (...)
Buffers: shared hit=855575640
CTE vendedor_loja
-> Merge Full Join (cost=135165.41..147750.26 rows=436160 width=1128) (actual time=5821.524..6640.486 rows=572488 loops=1)
Output: COALESCE(v1.mes, v1_1.mes), COALESCE(v1.ae, v1_1.ae), COALESCE(v1.regiao, v1_1.regiao), COALESCE(v1.cidade, v1_1.cidade), COALESCE(v1.uf, v1_1.uf), COALESCE(v1.cnpj_cliente, v1_1.cnpj_cliente), v1.nome_rca, v1.nome_supervisor, v1_1.nome_r (...)
Merge Cond: (((v1.mes)::text = (v1_1.mes)::text) AND ((v1.ae)::text = (v1_1.ae)::text) AND ((v1.regiao)::text = (v1_1.regiao)::text) AND ((v1.cidade)::text = (v1_1.cidade)::text) AND ((v1.cnpj_cliente)::text = (v1_1.cnpj_cliente)::text))
Buffers: shared hit=35832
-> Sort (cost=65905.90..66912.97 rows=402825 width=94) (actual time=2697.364..2764.972 rows=405405 loops=1)
Output: v1.mes, v1.ae, v1.regiao, v1.cidade, v1.uf, v1.cnpj_cliente, v1.nome_rca, v1.nome_supervisor
Sort Key: v1.mes, v1.ae, v1.regiao, v1.cidade, v1.cnpj_cliente
Sort Method: quicksort Memory: 75581kB
Buffers: shared hit=17916
-> Seq Scan on public.vendedor v1 (cost=0.00..28403.31 rows=402825 width=94) (actual time=0.020..232.571 rows=405405 loops=1)
Output: v1.mes, v1.ae, v1.regiao, v1.cidade, v1.uf, v1.cnpj_cliente, v1.nome_rca, v1.nome_supervisor
Filter: ((v1.commodity)::text = 'HF'::text)
Rows Removed by Filter: 433580
Buffers: shared hit=17916
-> Sort (cost=69259.51..70349.91 rows=436160 width=94) (actual time=3124.137..3207.479 rows=433760 loops=1)
Output: v1_1.mes, v1_1.ae, v1_1.regiao, v1_1.cidade, v1_1.uf, v1_1.cnpj_cliente, v1_1.nome_rca, v1_1.nome_supervisor
Sort Key: v1_1.mes, v1_1.ae, v1_1.regiao, v1_1.cidade, v1_1.cnpj_cliente
Sort Method: quicksort Memory: 80040kB
Buffers: shared hit=17916
-> Seq Scan on public.vendedor v1_1 (cost=0.00..28403.31 rows=436160 width=94) (actual time=0.052..233.379 rows=433580 loops=1)
Output: v1_1.mes, v1_1.ae, v1_1.regiao, v1_1.cidade, v1_1.uf, v1_1.cnpj_cliente, v1_1.nome_rca, v1_1.nome_supervisor
Filter: ((v1_1.commodity)::text = 'PC'::text)
Rows Removed by Filter: 405405
Buffers: shared hit=17916
-> Sort (cost=374912.87..374912.87 rows=1 width=2277) (actual time=2128381.174..2128443.450 rows=218279 loops=1)
Output: m.id, m.mes, m.regiao, m.area_nielsen, m.ae, m.tipo_cliente, m.cnpj_cliente, m.qtd_itens_cobrados, m.target, m.qtd_itens_vendidos, m.faltantes, m.cliente_compliance, m.dta_inclusao, m.mesorregiao, m.microrregiao, m.commodity, (concat(m.cnpj (...)
Sort Key: m.id, m.regiao, m.area_nielsen, m.ae, m.cnpj_cliente, m.qtd_itens_cobrados, m.target, m.qtd_itens_vendidos, m.faltantes, m.cliente_compliance, m.dta_inclusao, m.mesorregiao, m.microrregiao, m.commodity, (concat(m.cnpj_cliente, ' - ', m.cl (...)
Sort Method: quicksort Memory: 117126kB
Buffers: shared hit=855575640
-> Nested Loop (cost=10054.26..374912.86 rows=1 width=2277) (actual time=5850.721..2121951.136 rows=218279 loops=1)
Output: m.id, m.mes, m.regiao, m.area_nielsen, m.ae, m.tipo_cliente, m.cnpj_cliente, m.qtd_itens_cobrados, m.target, m.qtd_itens_vendidos, m.faltantes, m.cliente_compliance, m.dta_inclusao, m.mesorregiao, m.microrregiao, m.commodity, concat(m (...)
Join Filter: (((m.area_nielsen)::text = (u.area_nielsen)::text) AND ((m.ae)::text = (u.ae)::text) AND ((m.cnpj_cliente)::text = (u.cnpj_cliente)::text))
Rows Removed by Join Filter: 690603259
Buffers: shared hit=855575640
-> Hash Join (cost=9054.26..19252.27 rows=1 width=2508) (actual time=5849.511..7248.584 rows=2974 loops=1)
Output: m.id, m.mes, m.regiao, m.area_nielsen, m.ae, m.tipo_cliente, m.cnpj_cliente, m.qtd_itens_cobrados, m.target, m.qtd_itens_vendidos, m.faltantes, m.cliente_compliance, m.dta_inclusao, m.mesorregiao, m.microrregiao, m.commodity, v. (...)
Hash Cond: (((v.ae)::text = (m.ae)::text) AND ((v.cnpj_cliente)::text = (m.cnpj_cliente)::text) AND ((v.regiao)::text = (m.regiao)::text))
Buffers: shared hit=42086
-> CTE Scan on vendedor_loja v (cost=0.00..9813.60 rows=2181 width=1866) (actual time=5821.552..7123.154 rows=119307 loops=1)
Output: v.mes, v.ae, v.regiao, v.cidade, v.uf, v.cnpj_cliente, v.rca_hf, v.supervisor_hf, v.rca_pc, v.supervisor_pc
Filter: ((v.mes)::text = '2017-05'::text)
Rows Removed by Filter: 453181
Buffers: shared hit=35832
-> Hash (cost=9000.01..9000.01 rows=3100 width=918) (actual time=27.710..27.710 rows=3235 loops=1)
Output: m.id, m.mes, m.regiao, m.area_nielsen, m.ae, m.tipo_cliente, m.cnpj_cliente, m.qtd_itens_cobrados, m.target, m.qtd_itens_vendidos, m.faltantes, m.cliente_compliance, m.dta_inclusao, m.mesorregiao, m.microrregiao, m.commodi (...)
Buckets: 4096 Batches: 1 Memory Usage: 435kB
Buffers: shared hit=6254
-> Gather (cost=1000.00..9000.01 rows=3100 width=918) (actual time=11.068..25.905 rows=3235 loops=1)
Output: m.id, m.mes, m.regiao, m.area_nielsen, m.ae, m.tipo_cliente, m.cnpj_cliente, m.qtd_itens_cobrados, m.target, m.qtd_itens_vendidos, m.faltantes, m.cliente_compliance, m.dta_inclusao, m.mesorregiao, m.microrregiao, m.c (...)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=6254
-> Parallel Seq Scan on public.mix_cliente_compliance m (cost=0.00..7690.01 rows=1292 width=918) (actual time=6.406..20.979 rows=1078 loops=3)
Output: m.id, m.mes, m.regiao, m.area_nielsen, m.ae, m.tipo_cliente, m.cnpj_cliente, m.qtd_itens_cobrados, m.target, m.qtd_itens_vendidos, m.faltantes, m.cliente_compliance, m.dta_inclusao, m.mesorregiao, m.microrregia (...)
Filter: (((m.mes)::text = '2017-05'::text) AND ((m.tipo_cliente)::text = 'G'::text))
Rows Removed by Filter: 88096
Buffers: shared hit=6042
Worker 0: actual time=2.038..16.904 rows=986 loops=1
Buffers: shared hit=1057
Worker 1: actual time=6.465..22.309 rows=1240 loops=1
Buffers: shared hit=1913
-> Gather (cost=1000.00..351861.31 rows=217101 width=81) (actual time=1.147..618.701 rows=232287 loops=2974)
Output: u.grupo, u.qtd_vendida, u.mes, u.area_nielsen, u.ae, u.cnpj_cliente, u.tipo_cliente
Workers Planned: 4
Workers Launched: 4
Buffers: shared hit=855533554
-> Parallel Seq Scan on public.mix_relatorio_up u (cost=0.00..329151.21 rows=54275 width=81) (actual time=0.120..609.395 rows=49265 loops=17698274)
Output: u.grupo, u.qtd_vendida, u.mes, u.area_nielsen, u.ae, u.cnpj_cliente, u.tipo_cliente
Filter: (((u.mes)::text = '2017-05'::text) AND ((u.tipo_cliente)::text = 'G'::text))
Rows Removed by Filter: 2304772
Buffers: shared hit=986029292643
Worker 0: actual time=0.107..610.367 rows=49900 loops=2974
Buffers: shared hit=166166226
Worker 1: actual time=0.143..608.446 rows=48508 loops=2974
Buffers: shared hit=164883263
Worker 2: actual time=0.126..608.012 rows=48364 loops=2974
Buffers: shared hit=164907256
Worker 3: actual time=0.107..610.440 rows=50049 loops=2974
Buffers: shared hit=166389259
}
);
isa_ok( $explain, 'Pg::Explain' );
isa_ok( $explain->top_node, 'Pg::Explain::Node' );
is( $explain->top_node->type, 'GroupAggregate', 'Properly extracted top node type' );
is( $explain->top_node->sub_nodes->[ 0 ]->type, 'Sort', 'Properly extracted subnode-1' );
is( $explain->top_node->sub_nodes->[ 0 ]->sub_nodes->[ 0 ]->type, 'Nested Loop', 'Properly extracted subnode-2' );
is( $explain->top_node->sub_nodes->[ 0 ]->sub_nodes->[ 0 ]->sub_nodes->[ 1 ]->type, 'Gather', 'Properly extracted subnode-3' );
my $gather = $explain->top_node->sub_nodes->[ 0 ]->sub_nodes->[ 0 ]->sub_nodes->[ 1 ];
is( $gather->sub_nodes->[ 0 ]->type, 'Parallel Seq Scan', 'Properly extracted subnode-4' );
my $pss = $gather->sub_nodes->[ 0 ];
is( $pss->total_inclusive_time, $pss->actual_time_last * $gather->actual_loops, "Inclusive time is calculated properly for parallel nodes" );
is( $pss->total_exclusive_time, $pss->total_inclusive_time, "Exclusive time is calculated properly for parallel nodes" );
lives_ok(
sub {
$explain->anonymize();
},
'Anonymization works',
);
ok( $explain->as_text !~ /mix_cliente_compliance|mix_relatorio_up|vendedor_loja|vendedor|public/, 'anonymize() hides table names' );
ok( $explain->as_text !~ /cnpj_cliente|regiao/, 'anonymize() hides column names' );
exit;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment