Commit 7bafffea authored by Alvaro Herrera's avatar Alvaro Herrera

pgbench: Allow changing weights for scripts

Previously, all scripts had the same probability of being chosen when
multiple of them were specified via -b, -f, -N, -S.  With this commit,
-b and -f now search for an "@" in the script name and use the integer
found after it as the drawing probability for that script.

(One disadvantage is that if you have script whose names contain @, you
are now forced to specify "@1" at the end; otherwise the name's @ is
confused with a weight separator.  We don't expect many pgbench script
with @ in their names in the wild, so this shouldn't be too serious a
problem.)

While at it, rework the interface between addScript, process_file,
process_builtin, and findBuiltin.  It had gotten a bit out of hand with
recent commits.

Author: Fabien Coelho
Reviewed-By: Andres Freund, Robert Haas, Álvaro Herrera, Michaël Paquier
Discussion: http://www.postgresql.org/message-id/alpine.DEB.2.10.1603160721240.1666@sto
parent b46d9beb
...@@ -262,11 +262,13 @@ pgbench <optional> <replaceable>options</> </optional> <replaceable>dbname</> ...@@ -262,11 +262,13 @@ pgbench <optional> <replaceable>options</> </optional> <replaceable>dbname</>
<variablelist> <variablelist>
<varlistentry> <varlistentry>
<term><option>-b</> <replaceable>scriptname</></term> <term><option>-b</> <replaceable>scriptname[@weight]</></term>
<term><option>--builtin</> <replaceable>scriptname</></term> <term><option>--builtin</>=<replaceable>scriptname[@weight]</></term>
<listitem> <listitem>
<para> <para>
Add the specified builtin script to the list of executed scripts. Add the specified builtin script to the list of executed scripts.
An optional integer weight after <literal>@</> allows to adjust the
probability of drawing the script. If not specified, it is set to 1.
Available builtin scripts are: <literal>tpcb-like</>, Available builtin scripts are: <literal>tpcb-like</>,
<literal>simple-update</> and <literal>select-only</>. <literal>simple-update</> and <literal>select-only</>.
Unambiguous prefixes of builtin names are accepted. Unambiguous prefixes of builtin names are accepted.
...@@ -322,12 +324,14 @@ pgbench <optional> <replaceable>options</> </optional> <replaceable>dbname</> ...@@ -322,12 +324,14 @@ pgbench <optional> <replaceable>options</> </optional> <replaceable>dbname</>
</varlistentry> </varlistentry>
<varlistentry> <varlistentry>
<term><option>-f</> <replaceable>filename</></term> <term><option>-f</> <replaceable>filename[@weight]</></term>
<term><option>--file=</><replaceable>filename</></term> <term><option>--file=</><replaceable>filename[@weight]</></term>
<listitem> <listitem>
<para> <para>
Add a transaction script read from <replaceable>filename</> to Add a transaction script read from <replaceable>filename</> to
the list of executed scripts. the list of executed scripts.
An optional integer weight after <literal>@</> allows to adjust the
probability of drawing the test.
See below for details. See below for details.
</para> </para>
</listitem> </listitem>
...@@ -687,9 +691,13 @@ pgbench <optional> <replaceable>options</> </optional> <replaceable>dbname</> ...@@ -687,9 +691,13 @@ pgbench <optional> <replaceable>options</> </optional> <replaceable>dbname</>
<title>What is the <quote>Transaction</> Actually Performed in <application>pgbench</application>?</title> <title>What is the <quote>Transaction</> Actually Performed in <application>pgbench</application>?</title>
<para> <para>
Pgbench executes test scripts chosen randomly from a specified list. <application>pgbench</> executes test scripts chosen randomly
from a specified list.
They include built-in scripts with <option>-b</> and They include built-in scripts with <option>-b</> and
user-provided custom scripts with <option>-f</>. user-provided custom scripts with <option>-f</>.
Each script may be given a relative weight specified after a
<literal>@</> so as to change its drawing probability.
The default weight is <literal>1</>.
</para> </para>
<para> <para>
...@@ -1194,12 +1202,11 @@ number of clients: 10 ...@@ -1194,12 +1202,11 @@ number of clients: 10
number of threads: 1 number of threads: 1
number of transactions per client: 1000 number of transactions per client: 1000
number of transactions actually processed: 10000/10000 number of transactions actually processed: 10000/10000
latency average = 15.844 ms
latency stddev = 2.715 ms
tps = 618.764555 (including connections establishing) tps = 618.764555 (including connections establishing)
tps = 622.977698 (excluding connections establishing) tps = 622.977698 (excluding connections establishing)
SQL script 1: &lt;builtin: TPC-B (sort of)&gt; script statistics:
- 10000 transactions (100.0% of total, tps = 618.764555)
- latency average = 15.844 ms
- latency stddev = 2.715 ms
- statement latencies in milliseconds: - statement latencies in milliseconds:
0.004386 \set nbranches 1 * :scale 0.004386 \set nbranches 1 * :scale
0.001343 \set ntellers 10 * :scale 0.001343 \set ntellers 10 * :scale
......
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
#include "portability/instr_time.h" #include "portability/instr_time.h"
#include <ctype.h> #include <ctype.h>
#include <limits.h>
#include <math.h> #include <math.h>
#include <signal.h> #include <signal.h>
#include <sys/time.h> #include <sys/time.h>
...@@ -180,6 +181,8 @@ char *login = NULL; ...@@ -180,6 +181,8 @@ char *login = NULL;
char *dbName; char *dbName;
const char *progname; const char *progname;
#define WSEP '@' /* weight separator */
volatile bool timer_exceeded = false; /* flag from signal handler */ volatile bool timer_exceeded = false; /* flag from signal handler */
/* variable definitions */ /* variable definitions */
...@@ -298,26 +301,30 @@ typedef struct ...@@ -298,26 +301,30 @@ typedef struct
SimpleStats stats; /* time spent in this command */ SimpleStats stats; /* time spent in this command */
} Command; } Command;
static struct typedef struct ParsedScript
{ {
const char *name; const char *desc;
int weight;
Command **commands; Command **commands;
StatsData stats; StatsData stats;
} sql_script[MAX_SCRIPTS]; /* SQL script files */ } ParsedScript;
static ParsedScript sql_script[MAX_SCRIPTS]; /* SQL script files */
static int num_scripts; /* number of scripts in sql_script[] */ static int num_scripts; /* number of scripts in sql_script[] */
static int num_commands = 0; /* total number of Command structs */ static int num_commands = 0; /* total number of Command structs */
static int64 total_weight = 0;
static int debug = 0; /* debug flag */ static int debug = 0; /* debug flag */
/* Define builtin test scripts */ /* Builtin test scripts */
#define N_BUILTIN 3 typedef struct BuiltinScript
static struct
{ {
char *name; /* very short name for -b ... */ char *name; /* very short name for -b ... */
char *desc; /* short description */ char *desc; /* short description */
char *commands; /* actual pgbench script */ char *script; /* actual pgbench script */
} } BuiltinScript;
builtin_script[] = static BuiltinScript builtin_script[] =
{ {
{ {
"tpcb-like", "tpcb-like",
...@@ -393,9 +400,9 @@ usage(void) ...@@ -393,9 +400,9 @@ usage(void)
" --tablespace=TABLESPACE create tables in the specified tablespace\n" " --tablespace=TABLESPACE create tables in the specified tablespace\n"
" --unlogged-tables create tables as unlogged tables\n" " --unlogged-tables create tables as unlogged tables\n"
"\nOptions to select what to run:\n" "\nOptions to select what to run:\n"
" -b, --builtin=NAME add buitin script (use \"-b list\" to display\n" " -b, --builtin=NAME[@W] add builtin script NAME weighted at W (default: 1)\n"
" available scripts)\n" " (use \"-b list\" to list available scripts)\n"
" -f, --file=FILENAME add transaction script from FILENAME\n" " -f, --file=FILENAME[@W] add script FILENAME weighted at W (default: 1)\n"
" -N, --skip-some-updates skip updates of pgbench_tellers and pgbench_branches\n" " -N, --skip-some-updates skip updates of pgbench_tellers and pgbench_branches\n"
" (same as \"-b simple-update\")\n" " (same as \"-b simple-update\")\n"
" -S, --select-only perform SELECT-only transactions\n" " -S, --select-only perform SELECT-only transactions\n"
...@@ -1313,13 +1320,23 @@ clientDone(CState *st, bool ok) ...@@ -1313,13 +1320,23 @@ clientDone(CState *st, bool ok)
return false; /* always false */ return false; /* always false */
} }
/* return a script number with a weighted choice. */
static int static int
chooseScript(TState *thread) chooseScript(TState *thread)
{ {
int i = 0;
int64 w;
if (num_scripts == 1) if (num_scripts == 1)
return 0; return 0;
return getrand(thread, 0, num_scripts - 1); w = getrand(thread, 0, total_weight - 1);
do
{
w -= sql_script[i++].weight;
} while (w >= 0);
return i - 1;
} }
/* return false iff client should be disconnected */ /* return false iff client should be disconnected */
...@@ -1493,7 +1510,7 @@ top: ...@@ -1493,7 +1510,7 @@ top:
commands = sql_script[st->use_file].commands; commands = sql_script[st->use_file].commands;
if (debug) if (debug)
fprintf(stderr, "client %d executing script \"%s\"\n", st->id, fprintf(stderr, "client %d executing script \"%s\"\n", st->id,
sql_script[st->use_file].name); sql_script[st->use_file].desc);
st->is_throttled = false; st->is_throttled = false;
/* /*
...@@ -2632,34 +2649,33 @@ read_line_from_file(FILE *fd) ...@@ -2632,34 +2649,33 @@ read_line_from_file(FILE *fd)
} }
/* /*
* Given a file name, read it and return the array of Commands contained * Given a file name, read it and return its ParsedScript representation. "-"
* therein. "-" means to read stdin. * means to read stdin.
*/ */
static Command ** static ParsedScript
process_file(char *filename) process_file(char *filename)
{ {
#define COMMANDS_ALLOC_NUM 128 #define COMMANDS_ALLOC_NUM 128
ParsedScript ps;
Command **my_commands;
FILE *fd; FILE *fd;
int lineno, int lineno,
index; index;
char *buf; char *buf;
int alloc_num; int alloc_num;
alloc_num = COMMANDS_ALLOC_NUM;
my_commands = (Command **) pg_malloc(sizeof(Command *) * alloc_num);
if (strcmp(filename, "-") == 0) if (strcmp(filename, "-") == 0)
fd = stdin; fd = stdin;
else if ((fd = fopen(filename, "r")) == NULL) else if ((fd = fopen(filename, "r")) == NULL)
{ {
fprintf(stderr, "could not open file \"%s\": %s\n", fprintf(stderr, "could not open file \"%s\": %s\n",
filename, strerror(errno)); filename, strerror(errno));
pg_free(my_commands); exit(1);
return NULL;
} }
alloc_num = COMMANDS_ALLOC_NUM;
ps.commands = (Command **) pg_malloc(sizeof(Command *) * alloc_num);
ps.desc = filename;
lineno = 0; lineno = 0;
index = 0; index = 0;
...@@ -2676,35 +2692,36 @@ process_file(char *filename) ...@@ -2676,35 +2692,36 @@ process_file(char *filename)
if (command == NULL) if (command == NULL)
continue; continue;
my_commands[index] = command; ps.commands[index] = command;
index++; index++;
if (index >= alloc_num) if (index >= alloc_num)
{ {
alloc_num += COMMANDS_ALLOC_NUM; alloc_num += COMMANDS_ALLOC_NUM;
my_commands = pg_realloc(my_commands, sizeof(Command *) * alloc_num); ps.commands = pg_realloc(ps.commands, sizeof(Command *) * alloc_num);
} }
} }
fclose(fd); fclose(fd);
my_commands[index] = NULL; ps.commands[index] = NULL;
return my_commands; return ps;
} }
static Command ** /* Parse the given builtin script and return the parsed representation */
process_builtin(const char *tb, const char *source) static ParsedScript
process_builtin(BuiltinScript *bi)
{ {
#define COMMANDS_ALLOC_NUM 128
Command **my_commands;
int lineno, int lineno,
index; index;
char buf[BUFSIZ]; char buf[BUFSIZ];
int alloc_num; int alloc_num;
char *tb = bi->script;
ParsedScript ps;
alloc_num = COMMANDS_ALLOC_NUM; alloc_num = COMMANDS_ALLOC_NUM;
my_commands = (Command **) pg_malloc(sizeof(Command *) * alloc_num); ps.desc = bi->desc;
ps.commands = (Command **) pg_malloc(sizeof(Command *) * alloc_num);
lineno = 0; lineno = 0;
index = 0; index = 0;
...@@ -2714,6 +2731,7 @@ process_builtin(const char *tb, const char *source) ...@@ -2714,6 +2731,7 @@ process_builtin(const char *tb, const char *source)
char *p; char *p;
Command *command; Command *command;
/* buffer overflow check? */
p = buf; p = buf;
while (*tb && *tb != '\n') while (*tb && *tb != '\n')
*p++ = *tb++; *p++ = *tb++;
...@@ -2728,58 +2746,58 @@ process_builtin(const char *tb, const char *source) ...@@ -2728,58 +2746,58 @@ process_builtin(const char *tb, const char *source)
lineno += 1; lineno += 1;
command = process_commands(buf, source, lineno); command = process_commands(buf, bi->desc, lineno);
if (command == NULL) if (command == NULL)
continue; continue;
my_commands[index] = command; ps.commands[index] = command;
index++; index++;
if (index >= alloc_num) if (index >= alloc_num)
{ {
alloc_num += COMMANDS_ALLOC_NUM; alloc_num += COMMANDS_ALLOC_NUM;
my_commands = pg_realloc(my_commands, sizeof(Command *) * alloc_num); ps.commands = pg_realloc(ps.commands, sizeof(Command *) * alloc_num);
} }
} }
my_commands[index] = NULL; ps.commands[index] = NULL;
return my_commands; return ps;
} }
/* show available builtin scripts */
static void static void
listAvailableScripts(void) listAvailableScripts(void)
{ {
int i; int i;
fprintf(stderr, "Available builtin scripts:\n"); fprintf(stderr, "Available builtin scripts:\n");
for (i = 0; i < N_BUILTIN; i++) for (i = 0; i < lengthof(builtin_script); i++)
fprintf(stderr, "\t%s\n", builtin_script[i].name); fprintf(stderr, "\t%s\n", builtin_script[i].name);
fprintf(stderr, "\n"); fprintf(stderr, "\n");
} }
/* return builtin script "name" if unambiguous */ /* return builtin script "name" if unambiguous, of fails if not found */
static char * static BuiltinScript *
findBuiltin(const char *name, char **desc) findBuiltin(const char *name)
{ {
int i, int i,
found = 0, found = 0,
len = strlen(name); len = strlen(name);
char *commands = NULL; BuiltinScript *result = NULL;
for (i = 0; i < N_BUILTIN; i++) for (i = 0; i < lengthof(builtin_script); i++)
{ {
if (strncmp(builtin_script[i].name, name, len) == 0) if (strncmp(builtin_script[i].name, name, len) == 0)
{ {
*desc = builtin_script[i].desc; result = &builtin_script[i];
commands = builtin_script[i].commands;
found++; found++;
} }
} }
/* ok, unambiguous result */ /* ok, unambiguous result */
if (found == 1) if (found == 1)
return commands; return result;
/* error cases */ /* error cases */
if (found == 0) if (found == 0)
...@@ -2792,13 +2810,61 @@ findBuiltin(const char *name, char **desc) ...@@ -2792,13 +2810,61 @@ findBuiltin(const char *name, char **desc)
exit(1); exit(1);
} }
/*
* Determine the weight specification from a script option (-b, -f), if any,
* and return it as an integer (1 is returned if there's no weight). The
* script name is returned in *script as a malloc'd string.
*/
static int
parseScriptWeight(const char *option, char **script)
{
char *sep;
int weight;
if ((sep = strrchr(option, WSEP)))
{
int namelen = sep - option;
long wtmp;
char *badp;
/* generate the script name */
*script = pg_malloc(namelen + 1);
strncpy(*script, option, namelen);
(*script)[namelen] = '\0';
/* process digits of the weight spec */
errno = 0;
wtmp = strtol(sep + 1, &badp, 10);
if (errno != 0 || badp == sep + 1 || *badp != '\0')
{
fprintf(stderr, "invalid weight specification: %s\n", sep);
exit(1);
}
if (wtmp > INT_MAX || wtmp <= 0)
{
fprintf(stderr,
"weight specification out of range (1 .. %u): " INT64_FORMAT "\n",
INT_MAX, (int64) wtmp);
exit(1);
}
weight = wtmp;
}
else
{
*script = pg_strdup(option);
weight = 1;
}
return weight;
}
/* append a script to the list of scripts to process */
static void static void
addScript(const char *name, Command **commands) addScript(ParsedScript script, int weight)
{ {
if (commands == NULL || if (script.commands == NULL || script.commands[0] == NULL)
commands[0] == NULL)
{ {
fprintf(stderr, "empty command list for script \"%s\"\n", name); fprintf(stderr, "empty command list for script \"%s\"\n", script.desc);
exit(1); exit(1);
} }
...@@ -2808,8 +2874,8 @@ addScript(const char *name, Command **commands) ...@@ -2808,8 +2874,8 @@ addScript(const char *name, Command **commands)
exit(1); exit(1);
} }
sql_script[num_scripts].name = name; sql_script[num_scripts] = script;
sql_script[num_scripts].commands = commands; sql_script[num_scripts].weight = weight;
initStats(&sql_script[num_scripts].stats, 0.0); initStats(&sql_script[num_scripts].stats, 0.0);
num_scripts++; num_scripts++;
} }
...@@ -2840,7 +2906,7 @@ printResults(TState *threads, StatsData *total, instr_time total_time, ...@@ -2840,7 +2906,7 @@ printResults(TState *threads, StatsData *total, instr_time total_time,
(INSTR_TIME_GET_DOUBLE(conn_total_time) / nclients)); (INSTR_TIME_GET_DOUBLE(conn_total_time) / nclients));
printf("transaction type: %s\n", printf("transaction type: %s\n",
num_scripts == 1 ? sql_script[0].name : "multiple scripts"); num_scripts == 1 ? sql_script[0].desc : "multiple scripts");
printf("scaling factor: %d\n", scale); printf("scaling factor: %d\n", scale);
printf("query mode: %s\n", QUERYMODE[querymode]); printf("query mode: %s\n", QUERYMODE[querymode]);
printf("number of clients: %d\n", nclients); printf("number of clients: %d\n", nclients);
...@@ -2894,19 +2960,24 @@ printResults(TState *threads, StatsData *total, instr_time total_time, ...@@ -2894,19 +2960,24 @@ printResults(TState *threads, StatsData *total, instr_time total_time,
printf("tps = %f (including connections establishing)\n", tps_include); printf("tps = %f (including connections establishing)\n", tps_include);
printf("tps = %f (excluding connections establishing)\n", tps_exclude); printf("tps = %f (excluding connections establishing)\n", tps_exclude);
/* Report per-command statistics */ /* Report per-script/command statistics */
if (per_script_stats) if (per_script_stats || latency_limit || is_latencies)
{ {
int i; int i;
for (i = 0; i < num_scripts; i++) for (i = 0; i < num_scripts; i++)
{ {
if (num_scripts > 1)
printf("SQL script %d: %s\n" printf("SQL script %d: %s\n"
" - weight = %d\n"
" - " INT64_FORMAT " transactions (%.1f%% of total, tps = %f)\n", " - " INT64_FORMAT " transactions (%.1f%% of total, tps = %f)\n",
i + 1, sql_script[i].name, i + 1, sql_script[i].desc,
sql_script[i].weight,
sql_script[i].stats.cnt, sql_script[i].stats.cnt,
100.0 * sql_script[i].stats.cnt / total->cnt, 100.0 * sql_script[i].stats.cnt / total->cnt,
sql_script[i].stats.cnt / time_include); sql_script[i].stats.cnt / time_include);
else
printf("script statistics:\n");
if (latency_limit) if (latency_limit)
printf(" - number of transactions skipped: " INT64_FORMAT " (%.3f%%)\n", printf(" - number of transactions skipped: " INT64_FORMAT " (%.3f%%)\n",
...@@ -2914,6 +2985,7 @@ printResults(TState *threads, StatsData *total, instr_time total_time, ...@@ -2914,6 +2985,7 @@ printResults(TState *threads, StatsData *total, instr_time total_time,
100.0 * sql_script[i].stats.skipped / 100.0 * sql_script[i].stats.skipped /
(sql_script[i].stats.skipped + sql_script[i].stats.cnt)); (sql_script[i].stats.skipped + sql_script[i].stats.cnt));
if (num_scripts > 1)
printSimpleStats(" - latency", &sql_script[i].stats.latency); printSimpleStats(" - latency", &sql_script[i].stats.latency);
/* Report per-command latencies */ /* Report per-command latencies */
...@@ -2997,7 +3069,7 @@ main(int argc, char **argv) ...@@ -2997,7 +3069,7 @@ main(int argc, char **argv)
instr_time conn_total_time; instr_time conn_total_time;
int64 latency_late = 0; int64 latency_late = 0;
StatsData stats; StatsData stats;
char *desc; int weight;
int i; int i;
int nclients_dealt; int nclients_dealt;
...@@ -3045,6 +3117,8 @@ main(int argc, char **argv) ...@@ -3045,6 +3117,8 @@ main(int argc, char **argv)
while ((c = getopt_long(argc, argv, "ih:nvp:dqb:SNc:j:Crs:t:T:U:lf:D:F:M:P:R:L:", long_options, &optindex)) != -1) while ((c = getopt_long(argc, argv, "ih:nvp:dqb:SNc:j:Crs:t:T:U:lf:D:F:M:P:R:L:", long_options, &optindex)) != -1)
{ {
char *script;
switch (c) switch (c)
{ {
case 'i': case 'i':
...@@ -3176,27 +3250,25 @@ main(int argc, char **argv) ...@@ -3176,27 +3250,25 @@ main(int argc, char **argv)
exit(0); exit(0);
} }
addScript(desc, weight = parseScriptWeight(optarg, &script);
process_builtin(findBuiltin(optarg, &desc), desc)); addScript(process_builtin(findBuiltin(script)), weight);
benchmarking_option_set = true; benchmarking_option_set = true;
internal_script_used = true; internal_script_used = true;
break; break;
case 'S': case 'S':
addScript(desc, addScript(process_builtin(findBuiltin("select-only")), 1);
process_builtin(findBuiltin("select-only", &desc),
desc));
benchmarking_option_set = true; benchmarking_option_set = true;
internal_script_used = true; internal_script_used = true;
break; break;
case 'N': case 'N':
addScript(desc, addScript(process_builtin(findBuiltin("simple-update")), 1);
process_builtin(findBuiltin("simple-update", &desc),
desc));
benchmarking_option_set = true; benchmarking_option_set = true;
internal_script_used = true; internal_script_used = true;
break; break;
case 'f': case 'f':
addScript(optarg, process_file(optarg)); weight = parseScriptWeight(optarg, &script);
addScript(process_file(script), weight);
benchmarking_option_set = true; benchmarking_option_set = true;
break; break;
case 'D': case 'D':
...@@ -3334,12 +3406,16 @@ main(int argc, char **argv) ...@@ -3334,12 +3406,16 @@ main(int argc, char **argv)
/* set default script if none */ /* set default script if none */
if (num_scripts == 0 && !is_init_mode) if (num_scripts == 0 && !is_init_mode)
{ {
addScript(desc, addScript(process_builtin(findBuiltin("tpcb-like")), 1);
process_builtin(findBuiltin("tpcb-like", &desc), desc));
benchmarking_option_set = true; benchmarking_option_set = true;
internal_script_used = true; internal_script_used = true;
} }
/* compute total_weight */
for (i = 0; i < num_scripts; i++)
/* cannot overflow: weight is 32b, total_weight 64b */
total_weight += sql_script[i].weight;
/* show per script stats if several scripts are used */ /* show per script stats if several scripts are used */
if (num_scripts > 1) if (num_scripts > 1)
per_script_stats = true; per_script_stats = true;
...@@ -3745,7 +3821,7 @@ threadRun(void *arg) ...@@ -3745,7 +3821,7 @@ threadRun(void *arg)
commands = sql_script[st->use_file].commands; commands = sql_script[st->use_file].commands;
if (debug) if (debug)
fprintf(stderr, "client %d executing script \"%s\"\n", st->id, fprintf(stderr, "client %d executing script \"%s\"\n", st->id,
sql_script[st->use_file].name); sql_script[st->use_file].desc);
if (!doCustom(thread, st, &aggs)) if (!doCustom(thread, st, &aggs))
remains--; /* I've aborted */ remains--; /* I've aborted */
......
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