Make NUM_XLOGINSERT_LOCKS configurable
Dear all,
I recently used benchmarksql to evaluate the performance of postgresql. I achieved nearly 20% improvement
with NUM_XLOGINSERT_LOCKS changed from 8 to 16 under some cases of high concurrency. I wonder whether
it is feasible to make NUM_XLOGINSERT_LOCKS a configuration parameter, so that users can get easier to optimize
their postgresql performance through this setting.
Thanks,
Qingsong
<1111hqshj@sina.com> writes:
I recently used benchmarksql to evaluate the performance of postgresql. I achieved nearly 20% improvement
with NUM_XLOGINSERT_LOCKS changed from 8 to 16 under some cases of high concurrency. I wonder whether
it is feasible to make NUM_XLOGINSERT_LOCKS a configuration parameter, so that users can get easier to optimize
their postgresql performance through this setting.
Making it an actual GUC would carry nontrivial costs, not least that
there are hot code paths that do "foo % NUM_XLOGINSERT_LOCKS" which
would go from a mask operation to a full integer divide. We are
unlikely to consider that on the basis of an unsupported assertion
that there's a performance gain under unspecified conditions.
Even with data to justify a change, I think it'd make a lot more sense
to just raise the constant value.
regards, tom lane
On Tue, Jan 09, 2024 at 09:38:17PM -0500, Tom Lane wrote:
Making it an actual GUC would carry nontrivial costs, not least that
there are hot code paths that do "foo % NUM_XLOGINSERT_LOCKS" which
would go from a mask operation to a full integer divide. We are
unlikely to consider that on the basis of an unsupported assertion
that there's a performance gain under unspecified conditions.Even with data to justify a change, I think it'd make a lot more sense
to just raise the constant value.
This suggestion has showed up more than once in the past, and WAL
insertion is a path that can become so hot under some workloads that
changing it to a GUC would not be wise from the point of view of
performance. Redesigning all that to not require a set of LWLocks
into something more scalable would lead to better result, whatever
this design may be.
--
Michael
Michael Paquier <michael@paquier.xyz> writes:
This suggestion has showed up more than once in the past, and WAL
insertion is a path that can become so hot under some workloads that
changing it to a GUC would not be wise from the point of view of
performance. Redesigning all that to not require a set of LWLocks
into something more scalable would lead to better result, whatever
this design may be.
Maybe. I bet just bumping up the constant by 2X or 4X or so would get
most of the win for far less work; it's not like adding a few more
LWLocks is expensive. But we need some evidence about what to set it to.
regards, tom lane
On Wed, Jan 10, 2024 at 10:00 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Michael Paquier <michael@paquier.xyz> writes:
This suggestion has showed up more than once in the past, and WAL
insertion is a path that can become so hot under some workloads that
changing it to a GUC would not be wise from the point of view of
performance. Redesigning all that to not require a set of LWLocks
into something more scalable would lead to better result, whatever
this design may be.Maybe. I bet just bumping up the constant by 2X or 4X or so would get
most of the win for far less work; it's not like adding a few more
LWLocks is expensive. But we need some evidence about what to set it to.
I previously made an attempt to improve WAL insertion performance with
varying NUM_XLOGINSERT_LOCKS. IIRC, we will lose what we get by
increasing insertion locks (reduction in WAL insertion lock
acquisition time) to the CPU overhead of flushing the WAL in
WaitXLogInsertionsToFinish as referred to by the following comment.
Unfortunately, I've lost the test results, I'll run them up again and
come back.
/*
* Number of WAL insertion locks to use. A higher value allows more insertions
* to happen concurrently, but adds some CPU overhead to flushing the WAL,
* which needs to iterate all the locks.
*/
#define NUM_XLOGINSERT_LOCKS 8
--
Bharath Rupireddy
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com> writes:
On Wed, Jan 10, 2024 at 10:00 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Maybe. I bet just bumping up the constant by 2X or 4X or so would get
most of the win for far less work; it's not like adding a few more
LWLocks is expensive. But we need some evidence about what to set it to.
I previously made an attempt to improve WAL insertion performance with
varying NUM_XLOGINSERT_LOCKS. IIRC, we will lose what we get by
increasing insertion locks (reduction in WAL insertion lock
acquisition time) to the CPU overhead of flushing the WAL in
WaitXLogInsertionsToFinish as referred to by the following comment.
Very interesting --- this is at variance with what the OP said, so
we definitely need details about the test conditions in both cases.
Unfortunately, I've lost the test results, I'll run them up again and
come back.
Please.
regards, tom lane
On Wed, Jan 10, 2024 at 11:43 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com> writes:
On Wed, Jan 10, 2024 at 10:00 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Maybe. I bet just bumping up the constant by 2X or 4X or so would get
most of the win for far less work; it's not like adding a few more
LWLocks is expensive. But we need some evidence about what to set it to.I previously made an attempt to improve WAL insertion performance with
varying NUM_XLOGINSERT_LOCKS. IIRC, we will lose what we get by
increasing insertion locks (reduction in WAL insertion lock
acquisition time) to the CPU overhead of flushing the WAL in
WaitXLogInsertionsToFinish as referred to by the following comment.Very interesting --- this is at variance with what the OP said, so
we definitely need details about the test conditions in both cases.Unfortunately, I've lost the test results, I'll run them up again and
come back.Please.
Okay, I'm back with some testing.
Test case:
./pgbench --initialize --scale=100 --username=ubuntu postgres
./pgbench --progress=10 --client=64 --time=300 --builtin=tpcb-like
--username=ubuntu postgres
Setup:
./configure --prefix=$PWD/inst/ CFLAGS="-ggdb3 -O3" > install.log &&
make -j 8 install > install.log 2>&1 &
shared_buffers = '8GB'
max_wal_size = '32GB'
track_wal_io_timing = on
Stats measured:
I've used the attached patch to measure WAL Insert Lock Acquire Time
(wal_insert_lock_acquire_time) and WAL Wait for In-progress Inserts
to Finish Time (wal_wait_for_insert_to_finish_time).
Results with varying NUM_XLOGINSERT_LOCKS (note that we can't allow it
be more than MAX_SIMUL_LWLOCKS):
Locks TPS WAL Insert Lock Acquire Time in Milliseconds WAL
Wait for In-progress Inserts to Finish Time in Milliseconds
8 18669 12532 8775
16 18076 10641 13491
32 18034 6635 13997
64 17582 3937 14718
128 17782 4563 20145
Also, check the attached graph. Clearly there's an increase in the
time spent in waiting for in-progress insertions to finish in
WaitXLogInsertionsToFinish from 8.7 seconds to 20 seconds. Whereas,
the time spent to acquire WAL insertion locks decreased from 12.5
seconds to 4.5 seconds. Overall, this hasn't resulted any improvement
in TPS, in fact observed slight reduction.
--
Bharath Rupireddy
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
v1-0001-WAL-insertion-stats.patchapplication/octet-stream; name=v1-0001-WAL-insertion-stats.patchDownload
From 8765c04adb73d2c7057a5310e3670ec1fefdc0a5 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Fri, 12 Jan 2024 06:20:47 +0000
Subject: [PATCH v1] WAL insertion stats
---
doc/src/sgml/monitoring.sgml | 33 +++++++++++++++++++++
src/backend/access/transam/xlog.c | 39 +++++++++++++++++++++++++
src/backend/catalog/system_views.sql | 5 +++-
src/backend/utils/activity/pgstat_wal.c | 7 +++--
src/backend/utils/adt/pgstatfuncs.c | 15 ++++++++--
src/include/catalog/pg_proc.dat | 6 ++--
src/include/pgstat.h | 6 ++++
src/test/regress/expected/rules.out | 7 +++--
8 files changed, 107 insertions(+), 11 deletions(-)
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b804eb8b5e..61f077b246 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3181,6 +3181,39 @@ description | Waiting for a newly initialized WAL file to reach durable storage
Time at which these statistics were last reset
</para></entry>
</row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_insert_lock_acquire</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times WAL insertion lock was acquired
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_insert_lock_acquire_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total amount of time spent acquiring WAL insertion lock via
+ <function>LWLockAcquire</function> request, in milliseconds
+ (if <varname>track_wal_io_timing</varname> is enabled, otherwise
+ zero).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_wait_for_insert_to_finish_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total amount of time spent waiting for WAL insertion to finish,
+ in milliseconds (if <varname>track_wal_io_timing</varname> is enabled,
+ otherwise zero).
+ </para></entry>
+ </row>
+
</tbody>
</tgroup>
</table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..b808025fe0 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -1364,6 +1364,7 @@ static void
WALInsertLockAcquire(void)
{
bool immed;
+ instr_time start;
/*
* It doesn't matter which of the WAL insertion locks we acquire, so try
@@ -1382,6 +1383,12 @@ WALInsertLockAcquire(void)
lockToTry = MyProc->pgprocno % NUM_XLOGINSERT_LOCKS;
MyLockNo = lockToTry;
+ /* Measure WAL insertion lock acquire time. */
+ if (track_wal_io_timing)
+ INSTR_TIME_SET_CURRENT(start);
+ else
+ INSTR_TIME_SET_ZERO(start);
+
/*
* The insertingAt value is initially set to 0, as we don't know our
* insert location yet.
@@ -1399,6 +1406,20 @@ WALInsertLockAcquire(void)
*/
lockToTry = (lockToTry + 1) % NUM_XLOGINSERT_LOCKS;
}
+
+ /*
+ * Increment acquire time and number of times WAL insertion lock is
+ * acquired.
+ */
+ if (track_wal_io_timing)
+ {
+ instr_time end;
+
+ INSTR_TIME_SET_CURRENT(end);
+ INSTR_TIME_ACCUM_DIFF(PendingWalStats.wal_insert_lock_acquire_time, end, start);
+ }
+
+ PendingWalStats.wal_insert_lock_acquire++;
}
/*
@@ -1501,6 +1522,7 @@ WaitXLogInsertionsToFinish(XLogRecPtr upto)
XLogRecPtr finishedUpto;
XLogCtlInsert *Insert = &XLogCtl->Insert;
int i;
+ instr_time start;
if (MyProc == NULL)
elog(PANIC, "cannot wait without a PGPROC structure");
@@ -1537,6 +1559,13 @@ WaitXLogInsertionsToFinish(XLogRecPtr upto)
* out for any insertion that's still in progress.
*/
finishedUpto = reservedUpto;
+
+ /* Measure wait for WAL insert to finish time. */
+ if (track_wal_io_timing)
+ INSTR_TIME_SET_CURRENT(start);
+ else
+ INSTR_TIME_SET_ZERO(start);
+
for (i = 0; i < NUM_XLOGINSERT_LOCKS; i++)
{
XLogRecPtr insertingat = InvalidXLogRecPtr;
@@ -1584,6 +1613,16 @@ WaitXLogInsertionsToFinish(XLogRecPtr upto)
if (insertingat != InvalidXLogRecPtr && insertingat < finishedUpto)
finishedUpto = insertingat;
}
+
+ /* Increment wait for WAL insert to finish time. */
+ if (track_wal_io_timing)
+ {
+ instr_time end;
+
+ INSTR_TIME_SET_CURRENT(end);
+ INSTR_TIME_ACCUM_DIFF(PendingWalStats.wal_wait_for_insert_to_finish_time, end, start);
+ }
+
return finishedUpto;
}
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43e36f5ac..1acc21a9cd 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1181,7 +1181,10 @@ CREATE VIEW pg_stat_wal AS
w.wal_sync,
w.wal_write_time,
w.wal_sync_time,
- w.stats_reset
+ w.stats_reset,
+ w.wal_insert_lock_acquire,
+ w.wal_insert_lock_acquire_time,
+ w.wal_wait_for_insert_to_finish_time
FROM pg_stat_get_wal() w;
CREATE VIEW pg_stat_progress_analyze AS
diff --git a/src/backend/utils/activity/pgstat_wal.c b/src/backend/utils/activity/pgstat_wal.c
index 1a3c0e1a66..c6b596fcb1 100644
--- a/src/backend/utils/activity/pgstat_wal.c
+++ b/src/backend/utils/activity/pgstat_wal.c
@@ -119,6 +119,9 @@ pgstat_flush_wal(bool nowait)
WALSTAT_ACC(wal_sync, PendingWalStats);
WALSTAT_ACC_INSTR_TIME(wal_write_time);
WALSTAT_ACC_INSTR_TIME(wal_sync_time);
+ WALSTAT_ACC(wal_insert_lock_acquire, PendingWalStats);
+ WALSTAT_ACC_INSTR_TIME(wal_insert_lock_acquire_time);
+ WALSTAT_ACC_INSTR_TIME(wal_wait_for_insert_to_finish_time);
#undef WALSTAT_ACC_INSTR_TIME
#undef WALSTAT_ACC
@@ -158,9 +161,7 @@ pgstat_init_wal(void)
bool
pgstat_have_pending_wal(void)
{
- return pgWalUsage.wal_records != prevWalUsage.wal_records ||
- PendingWalStats.wal_write != 0 ||
- PendingWalStats.wal_sync != 0;
+ return true;
}
void
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 30a2063505..b320e71c2a 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1470,7 +1470,7 @@ pg_stat_get_io(PG_FUNCTION_ARGS)
Datum
pg_stat_get_wal(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_WAL_COLS 9
+#define PG_STAT_GET_WAL_COLS 12
TupleDesc tupdesc;
Datum values[PG_STAT_GET_WAL_COLS] = {0};
bool nulls[PG_STAT_GET_WAL_COLS] = {0};
@@ -1497,7 +1497,12 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
FLOAT8OID, -1, 0);
TupleDescInitEntry(tupdesc, (AttrNumber) 9, "stats_reset",
TIMESTAMPTZOID, -1, 0);
-
+ TupleDescInitEntry(tupdesc, (AttrNumber) 10, "wal_insert_lock_acquire",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 11, "wal_insert_lock_acquire_time",
+ FLOAT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 12, "wal_wait_for_insert_to_finish_time",
+ FLOAT8OID, -1, 0);
BlessTupleDesc(tupdesc);
/* Get statistics about WAL activity */
@@ -1524,6 +1529,12 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
values[8] = TimestampTzGetDatum(wal_stats->stat_reset_timestamp);
+ values[9] = Int64GetDatum(wal_stats->wal_insert_lock_acquire);
+
+ /* Convert counters from microsec to millisec for display */
+ values[10] = Float8GetDatum(((double) wal_stats->wal_insert_lock_acquire_time) / 1000.0);
+ values[11] = Float8GetDatum(((double) wal_stats->wal_wait_for_insert_to_finish_time) / 1000.0);
+
/* Returns the record as Datum */
PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 58811a6530..a8d562114a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5785,9 +5785,9 @@
{ oid => '1136', descr => 'statistics: information about WAL activity',
proname => 'pg_stat_get_wal', proisstrict => 'f', provolatile => 's',
proparallel => 'r', prorettype => 'record', proargtypes => '',
- proallargtypes => '{int8,int8,numeric,int8,int8,int8,float8,float8,timestamptz}',
- proargmodes => '{o,o,o,o,o,o,o,o,o}',
- proargnames => '{wal_records,wal_fpi,wal_bytes,wal_buffers_full,wal_write,wal_sync,wal_write_time,wal_sync_time,stats_reset}',
+ proallargtypes => '{int8,int8,numeric,int8,int8,int8,float8,float8,timestamptz,int8,float8,float8}',
+ proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{wal_records,wal_fpi,wal_bytes,wal_buffers_full,wal_write,wal_sync,wal_write_time,wal_sync_time,stats_reset,wal_insert_lock_acquire,wal_insert_lock_acquire_time,wal_wait_for_insert_to_finish_time}',
prosrc => 'pg_stat_get_wal' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 2136239710..ef3179376c 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -439,6 +439,9 @@ typedef struct PgStat_WalStats
PgStat_Counter wal_write_time;
PgStat_Counter wal_sync_time;
TimestampTz stat_reset_timestamp;
+ PgStat_Counter wal_insert_lock_acquire;
+ PgStat_Counter wal_insert_lock_acquire_time;
+ PgStat_Counter wal_wait_for_insert_to_finish_time;
} PgStat_WalStats;
/*
@@ -454,6 +457,9 @@ typedef struct PgStat_PendingWalStats
PgStat_Counter wal_sync;
instr_time wal_write_time;
instr_time wal_sync_time;
+ PgStat_Counter wal_insert_lock_acquire;
+ instr_time wal_insert_lock_acquire_time;
+ instr_time wal_wait_for_insert_to_finish_time;
} PgStat_PendingWalStats;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index d878a971df..46274f8da9 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2233,8 +2233,11 @@ pg_stat_wal| SELECT wal_records,
wal_sync,
wal_write_time,
wal_sync_time,
- stats_reset
- FROM pg_stat_get_wal() w(wal_records, wal_fpi, wal_bytes, wal_buffers_full, wal_write, wal_sync, wal_write_time, wal_sync_time, stats_reset);
+ stats_reset,
+ wal_insert_lock_acquire,
+ wal_insert_lock_acquire_time,
+ wal_wait_for_insert_to_finish_time
+ FROM pg_stat_get_wal() w(wal_records, wal_fpi, wal_bytes, wal_buffers_full, wal_write, wal_sync, wal_write_time, wal_sync_time, stats_reset, wal_insert_lock_acquire, wal_insert_lock_acquire_time, wal_wait_for_insert_to_finish_time);
pg_stat_wal_receiver| SELECT pid,
status,
receive_start_lsn,
--
2.34.1
image.pngimage/png; name=image.pngDownload
�PNG
IHDR � ! �`s� IDATx^�}p�y�$ �$J%!� JeW��H�����bGciF'MO�4(��$E�i���H3:
u��u&i$
����DnjV2�b)�%�^��K"%J| ����w���w��=���={��"p�9�����w�=�����k�m,// � @ ������^���:-� � @�^|�E0� �@�@��F� � �� #
� @ d� 8��$ @ 0r �@@�3 M � � #� @ d� 8��$ @ %����)���Q�n�~����1*|�_��H7�7'�ih��S�k�&�;�_U?M@ �
���p��g;��E��}xh�z&���xx��D���L�qj�n����9+9 � u�@t.�{�Am`����B/�������U>wz�8� @ ���{����^o��;4
��DA4@ ���@'����i�g�9��^�`
� e��X���X�P��3�&��B���O��;����Xhccq� ��N�,�>�����&���;D�R`�����p�ugM��8���g)L�>}��9"�IF��ka�H�iR�%�|&��$�39�xv���g�
��|//�*>jd}�58A���(��%�������?�&8*R�(�<<E�|FE*��gr��p���-@��!N�����-���8��gr�!��1L�?u�E����n|��h��|B�E��1.�2���8��fj-�i*3����p��� '�0uH��!m|����q��:��
J� �N��dB�x�*������������|��
=���ej��)����!�� ��~��K���v��
J&����[@����CI�����>�cV���- �S�\�A�)
o���g���6(�'X�:�� !�q<��3n����2�/�7o-prS���Or���(����C.��$�`Q�d�K&�����"�x��Z|��L<��g<��N�[�������q���L
��L����$��k�K��dB�{���<��y`)���3:Va%!��1L�?u�E����n|��h��|B�E��1.�2���8��fj-�i*3����p��� '�0uH��!m|����q��:��
J� �N��dB�x�*������������|��
=���ej��)����!�� ��~��K���v��
J&����[@����CI�����>�cV���- �S�\�A�)
o���g���6(�'X�:�� !�q<��3n����2�/�7o-prS���Or���(����C.��$�`Q�d�K&�����"�x��Z|��L<��g<��N�[�������q���L
��L����$��k�K��dB�{���<��y`)���3:Va%!��1L�?u�E����n|��h��|* ��x�M9�v���Hw�����a�<�|�5H��Z?���������#Q����dB�
�4��������dA����%���!�w��-t����sg�aK��]�N��l'���Z��Q�D?uz�Y�;N��#�M�2I>w4>��Sn
I&�I N��H����!���?LW�^-hii����?S��3� �aZ���a:Fe�a�{��T��n/;
��((���d����4������x��{aa��2��|��xF5���3� s����H7�5��Ch�\�A� �!_��@���4��4�mN������t��)Z]]����
������jHg���c�������3m� ��)�Y�2��@ �b^\\�.^�h�����MMM��}(Q�I+���?����������/�BX��P��wd�FR�5�i�}��14�B>Y\��?Oo��6���[���9^7���V��u+����e�`^�u��A����2=J�q�S`���������bV\�c��L��N TD��fh|f�@�~>�b���b����]�v��7�h�����-'��������|y��}���nR2o���8��� |wE
{<I��Q��"��H���d��� �4����\|��>��3�����-�`Y`���S!���N��$��8�X�N�q�%��`�Il$����\[[�s�����g�����WlYt�g��m����$�������dB��G���B6��3�U[}��7���,�.\���f����m���
�J2o!�5����Wi�O^�s�iG�f���;�}�3_��!�Y��v�#��@���y��g�"��������|���{�n����m��gs�g-tk����?�����E~3��l�u�7���7SF��N�<� �yb������=YY[]��,��")�w�dCC��~���=�����<�vQ|������#�j� ����l������9Z��]���\��=Y�g���O�|FE*�z�U0��_������������?&p�V��������jY`y52���A����(�g��mccc�( C���+U� W���C/�^�����rC������o�N?���b
I��_N���g�L �X�[ |�������d�7hE��Y[\^��� ���� W������_���ti��njl���Z^^/�y��-�����;w��?���jG��e?���J�����q�"����YO'�QIq/����/�H���q�����d���������=��y��x'��G��=���\���r����w�TXbAf1n������]-t�
�v�$��� �DHr���[&�8U|MRq&A���q��ER��m�ER�y�����g������ X����;��=���w����kn�q-�]����^���O}���W<��w�Y�|���[��/��d��f����e�������HrH��I����8����n>��#9�")������CZ7�*>qY�qz�� ���P�y��%�;�D//\���� ���L�6l4�F�F,������sQ<2�gW�g,���������N�a�XP)� ����4Q���!
��8��IJ�")�Xu��dTP2V�qB�%��h���/��?�=ZZ.=���>t�.�}���V����o��+>65n�{���?��U�{��E���\��z{kSYy�]������c���i�=w��Q�7�g��Y�8U��Z��'��_����"�MRi�#�G��8_9MC�H��S�H�����tu-xT�^�:A8nj�6�\�t����i,���'��������{�J��]<J�"�=^YX����`oQ�O��{����}�`��fka^����6???_����������58��!���g�f��'���z����#XAGk�mk\���W��=�����tym+-����Z+]Yw���G�������u����g�~P�t$���[p$
�*$�Q#�{��>��'��_]-�k� ?��Ke�p�K2���7�VG����q�U�a�
��m�n���<{���Y�Bo/m&������&��:|��m]����������w�g�WJ�s�{1���r��>8.r��L���h:�8��_���+?i��p���������m�8w�l����;�_�O��g\�������_���K��v���.X�^����i������u'�n��Z���m;o�8=}����������j�q�>@�U�0��dB^���@�d�|�F��������[����6�wI�s��(P������1�S I>!�y����� a�����=�\1�OwEr�7I%�H��8U��Sq�A��89��[@���h��S�({�z�$���q�6������YX���� ��L���i��8����1i>�]���xX9���my�����yH����$�g������n��:��
�Oux��Hf����wo[�g�[l����|�\C�O�����&�&��8Mb#�/q�d���Z�������m�<����q��!
[�39����n��:��
������7G�[�;�YxyX��|���?$�� ��(U+� �j 5C�&���?�Q��Yl�9[�'�����4������.�ZZ�L-j2�85i�^��|n�����������;v�(�q���!����5�u�$�� ���y
���<8���$6������n�6#`k�����MF�mp)�m���+�'�<�kz$�5O� �L�My��uJ]��i����utt�����a*WA�*CftI>x�N�$���H7�����x�M�v��[89LC�g�o�i����U�����>}��9�xn�I&�I� Ns��^,���l���^��}�#��\�|���O�gr���8����D^^����N-����9����N[����#�M��;*�� G�)7�p�gC����^�x�\����e����J�@j��=�����v��l��j|&GVA�=�z�� s/�
�H�������*�����
��R~��������{�=z���m�u{�A����nD�� ��S-������`�tG��-�����'�"���_HC,G���p����=��8p�jo���K���~�w��^n��!���2���QQ�4��Dn �D�WY�O
���4��9�M��Ma��8���D`ee��y���a`~���������b�Voyy�:���)�����7�-�; ���h`kI�x�����0a���#y�iJ��������
������[n�{�a��r%�����s��;������X+N[&�E�&����$�z�^x5M��,�
[������zr�\C2�M�;(N���^,?���v���������l�B�����I�����I�=nd��5��t��>+<md���
�'�s�����Q�� GE*���w�>�����P�;W�,��5������{�i���4k��O/6�3i��U_�O6� o ��q�dQPT/%?�������C����-����=��o���y����m���+��n/��j<H��v������#�q�B�qP3��$�`sy�L2!����������������#����N�q�W ���]��\�R|��Wt���e�9X.���x��hYp�B�����-�MMM�*�}��Em��rY��&