pgbench: INSERT workload, FK indexes, filler fix
Attached is a combined diff for a set of related patches to the built-in
pgbench workloads. One commit adds an INSERT workload. One fixes the long
standing 0 length filler issue. A new --extra-indexes option adds the
indexes needed for lookups added by the --foreign-keys option.
The commits are independent but overlap in goals. I'm grouping them here
mainly to consolidate this message, covering the feedback leading to this
particular combination plus a first review from me. More graphs etc.
coming as my pgbench toolchain settles down again.
Code all by David Christensen based on vague specs from me, errors probably
mine, changes are also at
https://github.com/pgguru/postgres/commits/pgbench-improvements David ran
through the pgbench TAP regression tests and we're thinking about how to
add more for changes like this. Long term that collides with performance
testing for things like CREATE INDEX, which I've done some work on myself
recently in pgbench-tools.
After bouncing the possibilities around a little, David and I thought this
specific set of changes might be the right amount of change for one PG
version. Core development could bite on all these pgbench changes or even
more [foreshadowing] as part of a themed rework of pgbench's workload
that's known to adjust results a bit, so beware direct comparisons to old
versions. That's what I'd prefer to do, a break it all at once strategy
for these items and whatever else we can dig up this cycle. I'll do my
usual thing to help with that, starting with more benchmark graphs of this
patch and such once my pgbench toolchain settles again.
To me pgbench should continue to demonstrate good PostgreSQL client
behavior, and all this is just modernizing polish. Row size and indexing
matter of course, but none of these changes really alter the fundamentals
of pgbench results. With modern hardware acceleration, the performance
drag due to the increased size of the filler is so much further down in the
benchmark noise from where I started at with PG. The $750 USD AMD retail
chip in my basement lab pushes 1M TPS of prepared SELECT statements over
sockets. Plus or minus 84 bytes per row in a benchmark database doesn't
worry me so much anymore. Seems down there with JSON overhead as a lost
micro optimization fight nowadays.
# Background: pgbench vs. sysbench
This whole rework idea came from a performance review pass where I compared
pgbench and sysbench again, as both have evolved a good bit since my last
comparison. All of the software defined storage testing brewing right now
is shining a brighter light on both tools lately than I've seen in a while.
The goal I worked on a bit (with Joe Conway and RedHat, thank you to our
sponsors) was how to make both tools closer to equal when performing
similar tasks. pgbench can duplicate the basics of the sysbench OLTP
workload easily enough, running custom pgbench scripts against the
generated pgbench_accounts and/or the initially empty pgbench_history. Joe
and I did some work on sysbench to improve its error handling to where it
reconnected automatically as part of that. How to add a reconnection
feature to pgbench is a struggle because of where it fits between PG's
typical connection and connection pooler abstractions; different story than
this one. sysbench had the basics and just needed some error handling bug
fixes, which might even have made their way upstream. These three patches
are the changes I thought core PG could use in parallel, as a mix of
correctness, new features, and fair play in benchmarking.
# INSERT workload
The easiest way to measure the basic COMMIT overhead of network storage is
by doing an INSERT into an empty database and seeing the latency. I've
been doing that regularly since 9.1 added sync rep and that was the easiest
way to test client scaling. From my perspective as an old CRUD app writer,
creating a row is the main interesting operation that's not already
available in pgbench. (No one has a DELETE heavy workload for very long)
Some chunk of pgbench users are trying to do that job now using the
built-ins, and none of the options fit well. Anything that touches the
accounts table becomes heavily wrapped into the checkpoint cycle, and
extracting signal from checkpoint noise is so hard dudes charge for books
about it. In this context I trust INSERT results more than I do the output
from pg_test_fsync, which is too low level for me to recommend as a
general-purpose tool.
For better or worse pgbench is a primary tool in that role to PG customers,
and the INSERT scaling looks great all over. I've attached an early sample
comparing 5 models of SSD to show it; what looks like a PG14 regression
there is a testing artifact I'm working on.
The INSERT workload is useful with or without the history indexes, which
again as written here only are created if you ask for the FKs. When I do
these performance studies of INSERT scaling as a new history table builds,
for really no good reason other than my curiosity, the slowdowns from
whether the pgbench_history has keys on it seem like basic primary key
overhead to me.
# FK indexes
The new extra index set always appears if you turn on FKs after this
change. Then there's also the original path to turn on the indexes but not
the FKs.
As I don't consider the use case of FKs without indexes to exist in the
wild, I was surprised at the current state of things, that you could even
have FKs but not the associated indexes. I have not RTFA for it but I'd
wager it's been brought up before. In that case, +1 from me and David for
this patch's view of database correctness I guess.
On a fresh pgbench database, the history table is empty and only the
accounts table has serious size to it. Adding indexes to the other tables,
like this patch does, has light overhead during the creation cycle.
My take on INSERT/UPDATE workloads that once you're hitting disk and have
WAL changes, whether one or three index blocks are touched each time on the
small tables is so much more of a checkpoint problem than anything else.
The overhead these new indexes add should be in the noise of the standard
pgbench "TPC-B (sort of)" workload.
The index overhead only gets substantial once you've run pgbench long
enough that history has some size to it. The tiny slice of people using
pgbench for long-term simulation--which might only be me--are surely
sophisticated enough to deal with index overhead increasing from zero to
normal primary key index overhead.
I personally would prefer to see pgbench lead by example here, that tables
related this way should be indexed with FKs by default, as the Right Way to
do such things. There's a slow deprecation plan leading that way possible
from here. This patch set adds options to add those indexes, and slowly
those options could become the defaults. Or there's the break it all at
once and the FK+Index path is the new default path forward, and users would
have to turn it off if they want to reduce overhead.
# filler
Every few years a customer I deal with discovers pgbench's generated tables
don't really fill its filler column. I think on modern hardware it's time
to pay for that fully, as not as scary of a performance regression.
memcpy() is AVX accelerated for me on Linux now; it's not the old C
standard library doing the block work. When I field detailed questions
about the filler, why it's length is 0, how the problem was introduced, and
why it was never fixed before, it's not the best look.
From port 5432 you can identify if a patched pgbench client created the
database like this:
pgbench# SELECT length(filler) FROM pgbench_accounts LIMIT 1;
length | 84
That is 0 in HEAD. I'd really prefer not to have to pause and explain this
filler thing again. It looks a little too much like benchmark mischief for
my comfort, which the whole sysbench comparison really highlighted again.
Attachments:
pgbench-insert-workload.patchapplication/octet-stream; name=pgbench-insert-workload.patchDownload
diff --git a/doc/src/sgml/ref/pgbench.sgml b/doc/src/sgml/ref/pgbench.sgml
index 0c60077e1f..2c0827a440 100644
--- a/doc/src/sgml/ref/pgbench.sgml
+++ b/doc/src/sgml/ref/pgbench.sgml
@@ -326,13 +326,25 @@ pgbench <optional> <replaceable>options</replaceable> </optional> <replaceable>d
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>--extra-indexes</option></term>
+ <listitem>
+ <para>
+ Create extra indexes on the standard tables for referenced FK columns.
+ (This option adds the <literal>i</literal> step to the initialization
+ step sequence, if it is not already present.)
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>--foreign-keys</option></term>
<listitem>
<para>
Create foreign key constraints between the standard tables.
(This option adds the <literal>f</literal> step to the initialization
- step sequence, if it is not already present.)
+ step sequence, if it is not already present.) Also implies
+ <option>--extra-indexes</option>.
</para>
</listitem>
</varlistentry>
@@ -410,7 +422,7 @@ pgbench <optional> <replaceable>options</replaceable> </optional> <replaceable>d
<listitem>
<para>
Add the specified built-in script to the list of scripts to be executed.
- Available built-in scripts are: <literal>tpcb-like</literal>,
+ Available built-in scripts are: <literal>tpcb-like</literal>, <literal>insert-only</literal>,
<literal>simple-update</literal> and <literal>select-only</literal>.
Unambiguous prefixes of built-in names are accepted.
With the special name <literal>list</literal>, show the list of built-in scripts
@@ -489,6 +501,16 @@ pgbench <optional> <replaceable>options</replaceable> </optional> <replaceable>d
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>--insert-only</option></term>
+ <listitem>
+ <para>
+ Run built-in insert-only script.
+ Shorthand for <option>-b insert-only</option>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>-j</option> <replaceable>threads</replaceable></term>
<term><option>--jobs=</option><replaceable>threads</replaceable></term>
@@ -986,6 +1008,11 @@ pgbench <optional> <replaceable>options</replaceable> </optional> <replaceable>d
If you select the <literal>select-only</literal> built-in (also <option>-S</option>),
only the <command>SELECT</command> is issued.
</para>
+
+ <para>
+ If you select the <literal>insert-only</literal> built-in,
+ only the <command>INSERT</command> is issued.
+ </para>
</refsect2>
<refsect2>
diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 4aeccd93af..cb30082c58 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -85,6 +85,12 @@
#define MM2_MUL_TIMES_8 UINT64CONST(0x35253c9ade8f4ca8)
#define MM2_ROT 47
+/* Filler strings of indicated lenghts */
+#define FILLER_ACCOUNTS_PADDING "123456789012345678901234567890123456789012345678901234567890123456789012345678901234"
+#define FILLER_BRANCHES_PADDING "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"
+#define FILLER_HISTORY_PADDING "1234567890123456789012"
+#define FILLER_TELLERS_PADDING "123456789012345678901234567890123456789012345678901234567890123456789012345678901234"
+
/*
* Multi-platform socket set implementations
*/
@@ -616,7 +622,7 @@ static const BuiltinScript builtin_script[] =
"SELECT abalance FROM pgbench_accounts WHERE aid = :aid;\n"
"UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid;\n"
"UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid;\n"
- "INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);\n"
+ "INSERT INTO pgbench_history (tid, bid, aid, delta, mtime, filler) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP, " FILLER_HISTORY_PADDING ");\n"
"END;\n"
},
{
@@ -629,7 +635,7 @@ static const BuiltinScript builtin_script[] =
"BEGIN;\n"
"UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;\n"
"SELECT abalance FROM pgbench_accounts WHERE aid = :aid;\n"
- "INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);\n"
+ "INSERT INTO pgbench_history (tid, bid, aid, delta, mtime, filler) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP, " FILLER_HISTORY_PADDING ");\n"
"END;\n"
},
{
@@ -637,6 +643,15 @@ static const BuiltinScript builtin_script[] =
"<builtin: select only>",
"\\set aid random(1, " CppAsString2(naccounts) " * :scale)\n"
"SELECT abalance FROM pgbench_accounts WHERE aid = :aid;\n"
+ },
+ {
+ "insert-only",
+ "<builtin: insert only>",
+ "\\set aid random(1, " CppAsString2(naccounts) " * :scale)\n"
+ "\\set bid random(1, " CppAsString2(nbranches) " * :scale)\n"
+ "\\set tid random(1, " CppAsString2(ntellers) " * :scale)\n"
+ "\\set delta random(-5000, 5000)\n"
+ "INSERT INTO pgbench_history (tid, bid, aid, delta, mtime, filler) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP, " FILLER_HISTORY_PADDING ");\n"
}
};
@@ -703,6 +718,7 @@ usage(void)
" -n, --no-vacuum do not run VACUUM during initialization\n"
" -q, --quiet quiet logging (one message each 5 seconds)\n"
" -s, --scale=NUM scaling factor\n"
+ " --extra-indexes create additional indexes on the tables\n"
" --foreign-keys create foreign key constraints between tables\n"
" --index-tablespace=TABLESPACE\n"
" create indexes in the specified tablespace\n"
@@ -719,6 +735,8 @@ usage(void)
" (same as \"-b simple-update\")\n"
" -S, --select-only perform SELECT-only transactions\n"
" (same as \"-b select-only\")\n"
+ " --insert-only perform INSERT-only transactions\n"
+ " (same as \"-b insert-only\")\n"
"\nBenchmarking options:\n"
" -c, --client=NUM number of concurrent database clients (default: 1)\n"
" -C, --connect establish new connection for each transaction\n"
@@ -4009,14 +4027,12 @@ initCreateTables(PGconn *con)
{
/*
* Note: TPC-B requires at least 100 bytes per row, and the "filler"
- * fields in these table declarations were intended to comply with that.
- * The pgbench_accounts table complies with that because the "filler"
- * column is set to blank-padded empty string. But for all other tables
- * the columns default to NULL and so don't actually take any space. We
- * could fix that by giving them non-null default values. However, that
- * would completely break comparability of pgbench results with prior
- * versions. Since pgbench has never pretended to be fully TPC-B compliant
- * anyway, we stick with the historical behavior.
+ * fields in these table declarations comply with that. This does change
+ * how "pgbench" has traditionally handled this, which in versions prior
+ * to 15 used NULL values for this field for every table but the accounts
+ * table. It was determined that having this timing data be comparable to
+ * other benchmarking tools was more important than compatibility with
+ * previous runs of pgbench.
*/
struct ddlinfo
{
@@ -4146,18 +4162,16 @@ initGenerateDataClientSide(PGconn *con)
*/
for (i = 0; i < nbranches * scale; i++)
{
- /* "filler" column defaults to NULL */
printfPQExpBuffer(&sql,
- "insert into pgbench_branches(bid,bbalance) values(%d,0)",
+ "insert into pgbench_branches(bid,bbalance,filler) values(%d,0,'" FILLER_BRANCHES_PADDING "')",
i + 1);
executeStatement(con, sql.data);
}
for (i = 0; i < ntellers * scale; i++)
{
- /* "filler" column defaults to NULL */
printfPQExpBuffer(&sql,
- "insert into pgbench_tellers(tid,bid,tbalance) values (%d,%d,0)",
+ "insert into pgbench_tellers(tid,bid,tbalance,filler) values (%d,%d,0,'" FILLER_TELLERS_PADDING "')",
i + 1, i / ntellers + 1);
executeStatement(con, sql.data);
}
@@ -4181,7 +4195,7 @@ initGenerateDataClientSide(PGconn *con)
/* "filler" column defaults to blank padded empty string */
printfPQExpBuffer(&sql,
- INT64_FORMAT "\t" INT64_FORMAT "\t%d\t\n",
+ INT64_FORMAT "\t" INT64_FORMAT "\t%d\t" FILLER_ACCOUNTS_PADDING "\n",
j, k / naccounts + 1, 0);
if (PQputline(con, sql.data))
{
@@ -4248,8 +4262,8 @@ initGenerateDataClientSide(PGconn *con)
* Fill the standard tables with some data generated on the server
*
* As already the case with the client-side data generation, the filler
- * column defaults to NULL in pgbench_branches and pgbench_tellers,
- * and is a blank-padded string in pgbench_accounts.
+ * columns default to blank-padded strings, so bring the record size up to
+ * 100.
*/
static void
initGenerateDataServerSide(PGconn *con)
@@ -4270,20 +4284,20 @@ initGenerateDataServerSide(PGconn *con)
initPQExpBuffer(&sql);
printfPQExpBuffer(&sql,
- "insert into pgbench_branches(bid,bbalance) "
- "select bid, 0 "
+ "insert into pgbench_branches(bid,bbalance,filler) "
+ "select bid, 0, '" FILLER_BRANCHES_PADDING "' "
"from generate_series(1, %d) as bid", nbranches * scale);
executeStatement(con, sql.data);
printfPQExpBuffer(&sql,
- "insert into pgbench_tellers(tid,bid,tbalance) "
- "select tid, (tid - 1) / %d + 1, 0 "
+ "insert into pgbench_tellers(tid,bid,tbalance,filler) "
+ "select tid, (tid - 1) / %d + 1, 0, '" FILLER_TELLERS_PADDING "' "
"from generate_series(1, %d) as tid", ntellers, ntellers * scale);
executeStatement(con, sql.data);
printfPQExpBuffer(&sql,
"insert into pgbench_accounts(aid,bid,abalance,filler) "
- "select aid, (aid - 1) / %d + 1, 0, '' "
+ "select aid, (aid - 1) / %d + 1, 0, '" FILLER_ACCOUNTS_PADDING "' "
"from generate_series(1, " INT64_FORMAT ") as aid",
naccounts, (int64) naccounts * scale);
executeStatement(con, sql.data);
@@ -4366,6 +4380,28 @@ initCreateFKeys(PGconn *con)
}
}
+/*
+ * Create extra indexes on the standard tables
+ */
+static void
+initCreateExtraIndexes(PGconn *con)
+{
+ static const char *const DDLKEYs[] = {
+ "create index pgbench_tellers_bid_idx on pgbench_tellers (bid)",
+ "create index pgbench_accounts_bid_idx on pgbench_accounts (bid)",
+ "create index pgbench_history_aid_idx on pgbench_history (aid)",
+ "create index pgbench_history_bid_idx on pgbench_history (bid)",
+ "create index pgbench_history_tid_idx on pgbench_history (tid)"
+ };
+ int i;
+
+ fprintf(stderr, "creating extra indexes...\n");
+ for (i = 0; i < lengthof(DDLKEYs); i++)
+ {
+ executeStatement(con, DDLKEYs[i]);
+ }
+}
+
/*
* Validate an initialization-steps string
*
@@ -4448,6 +4484,10 @@ runInitSteps(const char *initialize_steps)
op = "foreign keys";
initCreateFKeys(con);
break;
+ case 'i':
+ op = "extra indexes";
+ initCreateExtraIndexes(con);
+ break;
case ' ':
break; /* ignore */
default:
@@ -5769,6 +5809,8 @@ main(int argc, char **argv)
{"show-script", required_argument, NULL, 10},
{"partitions", required_argument, NULL, 11},
{"partition-method", required_argument, NULL, 12},
+ {"insert-only", no_argument, NULL, 13},
+ {"extra-indexes", no_argument, NULL, 14},
{NULL, 0, NULL, 0}
};
@@ -5776,6 +5818,7 @@ main(int argc, char **argv)
bool is_init_mode = false; /* initialize mode? */
char *initialize_steps = NULL;
bool foreign_keys = false;
+ bool extra_indexes = false;
bool is_no_vacuum = false;
bool do_vacuum_accounts = false; /* vacuum accounts table? */
int optindex;
@@ -6137,6 +6180,15 @@ main(int argc, char **argv)
exit(1);
}
break;
+ case 13: /* insert-only */
+ process_builtin(findBuiltin("insert-only"), 1);
+ benchmarking_option_set = true;
+ internal_script_used = true;
+ break;
+ case 14: /* indexes */
+ initialization_option_set = true;
+ extra_indexes = true;
+ break;
default:
fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname);
exit(1);
@@ -6252,6 +6304,19 @@ main(int argc, char **argv)
}
}
+ /* foreign_keys always implies extra_indexes */
+ if (extra_indexes || foreign_keys)
+ {
+ /* Add 'i' to end of initialize_steps, if not already there */
+ if (strchr(initialize_steps, 'i') == NULL)
+ {
+ initialize_steps = (char *)
+ pg_realloc(initialize_steps,
+ strlen(initialize_steps) + 2);
+ strcat(initialize_steps, "i");
+ }
+ }
+
runInitSteps(initialize_steps);
exit(0);
}
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c2a35a488a..e8cd07e9c9 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -102,15 +102,13 @@ all: all-lib check-libpq-refs
include $(top_srcdir)/src/Makefile.shlib
backend_src = $(top_srcdir)/src/backend
-# Check for functions that libpq must not call, currently just exit().
-# (Ideally we'd reject abort() too, but there are various scenarios where
-# build toolchains silently insert abort() calls, e.g. when profiling.)
-# If nm doesn't exist or doesn't work on shlibs, this test will do nothing,
-# which is fine. The exclusion of __cxa_atexit is necessary on OpenBSD,
-# which seems to insert references to that even in pure C code.
+# Check for functions that libpq must not call, currently abort() and exit().
+# If nm doesn't exist or doesn't work on shlibs, this test will silently
+# do nothing, which is fine. The exclusion of _eprintf.o is to prevent
+# complaining about <assert.h> infrastructure on ancient macOS releases.
.PHONY: check-libpq-refs
check-libpq-refs: $(shlib)
- ! nm -A -g -u $< 2>/dev/null | grep -v __cxa_atexit | grep exit
+ ! nm -A -g -u $< 2>/dev/null | grep -v '_eprintf\.o:' | grep -e abort -e exit
# Make dependencies on pg_config_paths.h visible in all builds.
fe-connect.o: fe-connect.c $(top_builddir)/src/port/pg_config_paths.h
insert-pg14b1-1.pngimage/png; name=insert-pg14b1-1.pngDownload
�PNG
IHDR � � 5��� 9tEXtSoftware Matplotlib version3.3.4, https://matplotlib.org/T�� pHYs N Nw�# �vIDATx���yX�U������������,n(����j���{��K�Tj�O�7��6++KM3s�Wq7����r��E7@��e���12���3���.���<�=�n��<��EQB!����!�B��% �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# �B!D# ��������M3vBa�T��(�B!�Sll,���������k����5�(�B�G13v BQ��r���,--�rm!�(
B�;��T*V�XA��]���������`���N��}�����R�jU:v�H||��}�����cccC�V���o��m����������U����C�������Q�T������B���PQ���=��c�r��j����a�m���h��������C���k�������7��h�"��?����N��= 7�s��]~��'6l��������oh��5�������
*��*��"C�B�ro������� |���m����d����~�:�z���� ooo�qs��e�������c��m�6~��gC�1##�~�!W����;;;j��QJ�P!�F@!D���iS��9IYtt4vvv�3�g�}������[7���������1p�@T*�����\\\��V�*��B�2G@!D�gnnn�:'���t |��'���kl�����W3c��=J���������*P�R%��666%�B;�PQ�5i��)S�p��Qj�������^�:5j� ""OO�\����z>sss�Zm)E/�E'@!D�����������?�������DDD��AT*|���O����:����i��5�����n��=z��7obccC��UK�] !��I(���4
������+��s���k3c��#c������y��1j�(i��
/���C�;q�D^�u����Y�f������J B!���(�BQ�H(�BQ�H(�BQ�H(�BQ�H(�BQ�H(�BQ��<�O����j����222���,�s�7�7��)��M��o
&}S���7w��!##��a�$�O�Z�j��q�X�H�����\���M��o
&}S0���I��<������C0*B!��`$B!��`d���t:����V�-�h�6���W��T*�j��P!�;I KHff&dee��j��R�Q�]�7+��177����b;�B�" ` ����R�J8::�R�
uLbb"�+W.���&���g�(�BLLxzz�9�B�I K�N�#++GGG��
��j��FS���]�7+��qtt$66�N'��BQN�O��s�_a+B����mQ�]BQ�H(�BQ�HXA���q��y ����%W�^5�O�8�Y�f�����q����������3�| ���133�Y�f�m����������/;v�����}]]]���7��������a���}mAAA�k��5k���{
mC�����p��I�m:���c�������'�-2���7�����:)) �\���o�>Z�lY�>W�T$''j��*��AAA�m���������#�>���f��Qz�{!�r`�U�V-�N���5k��-X����H��=�������������9s�L����m���4���8��+V�}�v6l����6l�@�&M�P��_~���W��sg.^�H�n� ����)S�0f��<������p�!!!$$$��E���i��!'Nd��-,[��7�|� &��gO����)�Y�&_�5�O���?������KPP����t:�����%K7n���BalR���z�-<��S���EDDP�F
�,VVVx{{��;w&))����'� &&���X:w�@��
������~{���=�F���`�����!�+V0m�4/^��������y���p����1c~~~xzz�s��|��4i�Z��Y�ft�����/��6m�a���i��9s�����[��w�N�����Diii�p�BTpR,o��ZL�#���th������
���*rL666��1���'���9�=z�g���mK�.]�����I���x�5kf�����U�V����������Z�jE����^C����SO��'�`aa������l����}�r��1BBB77�|�.]���+s��5�A��u
����q�� �����3f��~�m:���M�����������g�k�.�}�]z���g���'����]�{������z���hF�����i�����+j7�����K/q����yF����BQ�I�{��7�~�z�������P���[������3�����=g�7����_Nr��������������8{�,�N�����!C���n���e�h���-�}�����G}��+W��7������u���Oe?���V�e��m���p���B�jkk��/�@�6mr
������M�64i����g�B/�G��E�4h� ���6777��9Cdd$l�����%��E���J�T KAa+t T�R�����F����?f��)t��)W������������oR�f�B
��$���0g�
������*t\����~E����S�~}C���O�!�F���qc j��m����/3e�.]�����������������k�����O�^�:k���M�6���ww�G�z���h4�.���q�8~�8��������s��4,vvv4�_~��A��������'��v�������P�QQxR��������E�����}������'qpp������U�TL�>'''/^\��RRR���7�^�f
��77����4|��?`kkkH�n��ah;z�(111��,�����%K�j�����������c�����W��3g2|��bK�����F�(��w�}W����i����
��-[����jX�033�M�6����X�B�������ugxc� �$gz�xgSzR��7�:^GDD0~�x�����������[�x�@�O5� �J��_~���5jT�������o��h�ZE���=���%K���_P�F��y�f�����C���B��`mm����
���d�&N�4�F������!C���/�Y�&