pax_global_header00006660000000000000000000000064146573545540014534gustar00rootroot0000000000000052 comment=92c4d4af1823b4e56740dd2c07d68f53c4188de4 plprofiler-REL4_2_5/000077500000000000000000000000001465735455400144275ustar00rootroot00000000000000plprofiler-REL4_2_5/.gitignore000066400000000000000000000000331465735455400164130ustar00rootroot00000000000000*.o *.so .*.swp *.pyc *.bc plprofiler-REL4_2_5/LICENSE000066400000000000000000000112601465735455400154340ustar00rootroot00000000000000The Artistic License Preamble The intent of this document is to state the conditions under which a Package may be copied, such that the Copyright Holder maintains some semblance of artistic control over the development of the package, while giving the users of the package the right to use and distribute the Package in a more-or-less customary fashion, plus the right to make reasonable modifications. Definitions: "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder. "Copyright Holder" is whoever is named in the copyright or copyrights for the package. "You" is you, if you're thinking about copying or distributing this Package. "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify it to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) "Freely Available" means that no fee is charged for the item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. 1. You may make and give away verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. 3. You may otherwise modify your copy of this Package in any way, provided that you insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as ftp.uu.net, or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. b) use the modified Package only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided, and provide a separate manual page for each non-standard executable that clearly documents how it differs from the Standard Version. d) make other distribution arrangements with the Copyright Holder. 4. You may distribute the programs of this Package in object code or executable form, provided that you do at least ONE of the following: a) distribute a Standard Version of the executables and library files, together with instructions (in the manual page or equivalent) on where to get the Standard Version. b) accompany the distribution with the machine-readable source of the Package with your modifications. c) accompany any non-standard executables with their corresponding Standard Version executables, giving the non-standard executables non-standard names, and clearly documenting the differences in manual pages (or equivalent), together with instructions on where to get the Standard Version. d) make other distribution arrangements with the Copyright Holder. 5. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you do not advertise this Package as a product of your own. 6. The scripts and library files supplied as input to or produced as output from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this Package. 7. C or perl subroutines supplied by you and linked into this Package shall not be considered part of this Package. 8. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. The End plprofiler-REL4_2_5/LICENSE-flamegraph000066400000000000000000000443511465735455400175470ustar00rootroot00000000000000COMMON DEVELOPMENT AND DISTRIBUTION LICENSE Version 1.0 1. Definitions. 1.1. "Contributor" means each individual or entity that creates or contributes to the creation of Modifications. 1.2. "Contributor Version" means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor. 1.3. "Covered Software" means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof. 1.4. "Executable" means the Covered Software in any form other than Source Code. 1.5. "Initial Developer" means the individual or entity that first makes Original Software available under this License. 1.6. "Larger Work" means a work which combines Covered Software or portions thereof with code not governed by the terms of this License. 1.7. "License" means this document. 1.8. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein. 1.9. "Modifications" means the Source Code and Executable form of any of the following: A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications; B. Any new file that contains any part of the Original Software or previous Modifications; or C. Any new file that is contributed or otherwise made available under the terms of this License. 1.10. "Original Software" means the Source Code and Executable form of computer software code that is originally released under this License. 1.11. "Patent Claims" means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor. 1.12. "Source Code" means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code. 1.13. "You" (or "Your") means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants. 2.1. The Initial Developer Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof). (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License. (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices. 2.2. Contributor Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination). (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party. (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor. 3. Distribution Obligations. 3.1. Availability of Source Code. Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange. 3.2. Modifications. The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License. 3.3. Required Notices. You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer. 3.4. Application of Additional Terms. You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients' rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer. 3.5. Distribution of Executable Versions. You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient's rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer. 3.6. Larger Works. You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software. 4. Versions of the License. 4.1. New Versions. Sun Microsystems, Inc. is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License. 4.2. Effect of New Versions. You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward. 4.3. Modified Versions. When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License. 5. DISCLAIMER OF WARRANTY. COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 6. TERMINATION. 6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive. 6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as "Participant") alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant. 6.3. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination. 7. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 8. U.S. GOVERNMENT END USERS. The Covered Software is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer software" (as that term is defined at 48 C.F.R. 252.227-7014(a)(1)) and "commercial computer software documentation" as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License. 9. MISCELLANEOUS. This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction's conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software. 10. RESPONSIBILITY FOR CLAIMS. As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability. -------------------------------------------------------------------- NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) For Covered Software in this distribution, this License shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California. plprofiler-REL4_2_5/Makefile000066400000000000000000000007271465735455400160750ustar00rootroot00000000000000 MODULE_big = plprofiler OBJS = plprofiler.o EXTENSION = plprofiler DATA = plprofiler--4.1--4.2.sql \ plprofiler--4.2.sql ifdef USE_PGXS PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) else subdir = contrib/plprofiler top_builddir = ../.. include $(top_builddir)/src/Makefile.global include $(top_srcdir)/contrib/contrib-global.mk plprofiler.o: CFLAGS += -I$(top_builddir)/src/pl/plpgsql/src endif plprofiler.o: plprofiler.c plprofiler.h plprofiler-REL4_2_5/README.md000066400000000000000000000105161465735455400157110ustar00rootroot00000000000000# plProfiler The **plprofiler** is an extension for the PostgreSQL database system to create performance profiles of PL/pgSQL functions and stored procedures. The included external Python class and command line utility can be used to easily control the extension, run arbitrary SQL commands (invoking PL/pgSQL functions), save and manage the resulting performance datasets and create HTML reports from them. 1. [Overview](#markdown-header-overview) 2. [Installation](doc/installation.md) 3. [Examples](doc/examples.md) * [The example test case](doc/examples.md#markdown-header-the-example-test-case) * [General command syntax](doc/examples.md#markdown-header-general-command-syntax) * [Executing SQL using the plprofiler utility](doc/examples.md#markdown-header-executing-sql-using-the-plprofiler-utility) * [Analyzing the first profile](doc/examples.md#markdown-header-analyzing-the-first-profile) * [Capturing profiling data by instrumenting the application](doc/examples.md#markdown-header-capturing-profiling-data-by-instrumenting-the-application) * [Collecting statistics at a timed interval](doc/examples.md#markdown-header-collecting-statistics-at-a-timed-interval) * [Collecting statistics via ALTER USER](doc/examples.md#markdown-header-collecting-statistics-via-alter-user) * [Profiling a live production system](doc/examples.md#markdown-header-profiling-a-live-production-system) * [Fixing the performance problem](doc/examples.md#markdown-header-fixing-the-performance-problem) 4. [plprofiler command reference](doc/plprofiler_cmd_ref.md) * [Command run](doc/plprofiler_cmd_ref.md#markdown-header-command-run) * [Command monitor](doc/plprofiler_cmd_ref.md#markdown-header-command-monitor) * [Command reset-data](doc/plprofiler_cmd_ref.md#markdown-header-command-reset-data) * [Command save](doc/plprofiler_cmd_ref.md#markdown-header-command-save) * [Command list](doc/plprofiler_cmd_ref.md#markdown-header-command-list) * [Command edit](doc/plprofiler_cmd_ref.md#markdown-header-command-edit) * [Command report](doc/plprofiler_cmd_ref.md#markdown-header-command-report) * [Command delete](doc/plprofiler_cmd_ref.md#markdown-header-command-delete) * [Command export](doc/plprofiler_cmd_ref.md#markdown-header-command-export) * [Command import](doc/plprofiler_cmd_ref.md#markdown-header-command-import) Overview -------- Finding performance problems within PL/pgSQL functions and stored procedures can be difficult, especially when the code is nested. This is because PL/pgSQL creates a cloak over whatever is happening inside. The only thing visible in system or extension views, such as `pg_stat_activity` or `pg_stat_statements` is the query, sent from the client. In the case of invoking a stored procedure, that is just the outermost stored procedure call. The **plprofiler** extension can be used to quickly identify the most time consuming functions and then drill down to find the individual statements within them. The output, generated by the **plprofiler**, is a self-contained HTML document. The document starts with a [FlameGraph](http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html) at the top, followed by details about functions in the profile. Unlike usual CPU FlameGraphs, the **plprofiler** FlameGraph is based on the actual Wall-Clock time, spent in the PL/pgSQL functions. By default, the top ten functions, based on their self_time (total_time - children_time), are detailed. This can be overridden by the user. Click on the screenshot below to see the actual, interactive report in your browser. [ ![ Example report ](doc/images/pgbench_pl-1.png) ](http://wi3ck.info/plprofiler/doc/pgbench_pl-1.html) [`doc/pgbench_pl-1.html`](http://wi3ck.info/plprofiler/doc/pgbench_pl-1.html) Please see the [Examples](doc/examples.md) for more details about this interactive report. Credits for the FlameGraph go to [Brendan Gregg](http://www.brendangregg.com/). His `flamegraph.pl` script is used by the **plprofiler** utility to generate these incredibly powerful, interactive SVGs. #####Major Change History * 2012 - Removed from PostgreSQL plDebugger Extension * 2015 - Resurrected as standalone plProfiler by OpenSCG * 2016 - Rewritten as v2 to use shared hash tables, have lower overhead * 2016 - v3 Major performance improvements, flame graph UI * 2019 - v3.5 Placed all extension objects under role plprofiler for easier grant plprofiler-REL4_2_5/doc/000077500000000000000000000000001465735455400151745ustar00rootroot00000000000000plprofiler-REL4_2_5/doc/examples.md000066400000000000000000000435141465735455400173430ustar00rootroot00000000000000PL Profiler Examples ==================== In this tutorial style set of examples, I mostly want to demonstrate the different ways, **plprofiler** allows to capture profiling data. The examples are built on top of each other so it is best to at least once read over this document top to bottom. It is assumed that anyone, interested in profiling complex PL/pgSQL code, is familiar with performance testing in general and performance testing of a PostgreSQL database in particular. Therefore it is also also assumed that the reader has a basic understanding of the pgbench utility. The example test case --------------------- All examples in this documentation are based on a modified pgbench database. The modifications are: * The SQL queries, that make up the TPC-B style business transaction of pgbench, have been implemented in a set of PL/pgSQL functions. Each function essentially performs only one of the TPC-B queries. This is on purpose convoluted, since for the sake of demonstration we want a simple, yet nested example. The function definitions can be found in [`examples/pgbench_pl.sql`](../examples/pgbench_pl.sql). * A custom pgbench profile, found in [`examples/pgbench_pl.profile`](../examples/pgbench_pl.profile), is used with the -f option when invoking pgbench. * The table pgbench_accounts is modified. * The filler column is expanded and filled with 500 characters of data. * A new column, `category interger` is added in front of the aid and made part of the primary key. NOTE: The command syntax for pgbench custom profiles was changed in PostgreSQL 9.6. There are 9.6 specific profiles in the examples directory as well. The modifications to the pgbench_accounts table are based on a real world case, encountered in a customer database. This pgbench example case of course is greatly simplified. In the real world case the access to the table in question was in a nested function, 8 call levels deep, the table had several indexes to choose from and the schema contained a total of >500 PL/pgSQL functions with >100,000 lines of PL code. In other words the author was looking for a needle in what once was a haystack, but had been eaten by an elephant. Despite the simplification, the problem produced by these modifications simulates the original case surprisingly well. The TPC-B transaction accesses the pgbench_accounts table based on the aid column alone, so that is the only key part, available in the WHERE clause. However, since the table rows are now >500 bytes wide and the index is rather small, compared to the heap, the PostgreSQL query optimizer will still choose an index scan. This is the right choice, based on the available options, because a sequential scan would be worse. ``` pgbench_plprofiler=# explain select abalance from pgbench_accounts where aid = 1; QUERY PLAN -------------------------------------------------------------------------------------------------- Index Scan using pgbench_accounts_pkey on pgbench_accounts (cost=0.42..18484.43 rows=1 width=4) Index Cond: (aid = 1) ``` Since the first column of the index is not part of the WHERE clause and thus, the index condition, this results in a full scan of the entire index! Unfortunately that detail is nowhere visible except in this explain output. And then you will only notice it if you know the definition of that index. If we look at pg_stat_* tables after a benchmark run for example, they only tell us that all access to pgbench_accounts was done via index scans over the primary key and that all those scans returned a single row. One would normally think "nothing wrong here". On top of that, since the queries accessing the table will never show up in any statistics, we will never see that each of them takes 30ms already on a 10x pgbench scaling factor. Imagine what that turns into when we scale out. The full script to prepare the pgbench test database is found in [`examples/prepdb.sh`](../examples/prepdb.sh). To get a performance baseline, the median result of 5 times 5 minutes pgbench with 24 clients reports 136 TPS on an 8-core machine with 32GB of RAM and the entire database fitting into the 8GB of shared buffers (yeah, it is that bad). ``` (venv)[wieck@localhost examples]$ pgbench -n -c24 -j24 -T300 -f pgbench_pl.profile transaction type: Custom query scaling factor: 1 query mode: simple number of clients: 24 number of threads: 24 duration: 300 s number of transactions actually processed: 40686 latency average: 176.965 ms tps = 135.580039 (including connections establishing) tps = 135.589426 (excluding connections establishing) ``` Time to create a profile. General command syntax ---------------------- The general syntax of the plprofiler utility is `plprofiler COMMAND [OPTIONS]` Common for all commands are options, that control the database connection. These are Option | Description --------------------- | ------------------------ `-h, --host=HOST` | The host to connect to. `-p, --port=PORT` | Port number the postmaster is listening on. `-U, --user=USER` | The database user name. `-d, --dbname=DB` | The database name, conninfo string or URI. `plprofiler help [COMMAND]` will show you more details than are explained in the examples, provided in this document. In the examples below it is assumed that the environment variables `PGHOST`, `PGPORT`, `PGUSER` and `PGDATABASE` have all been set to point to the pgbench_plprofiler database, that was created using the [`examples/prepdb.sh`](../examples/prepdb.sh) script. The above connection parameters are left out to make the examples more readable. For security reasons, there is not way to specify a password on the command line. Please create the necessary `~/.pgpass` entry if your database requires password authentication. Executing SQL using the plprofiler utility ------------------------------------------ After having installed the **plprofiler** extension in the test database, the easiest way to generate a profile of PL/pgSQL functions is to run them using the plprofiler utility and let it create an HTML report directly from the local-data, collected in the backend. `plprofiler run --command "SELECT tpcb(1, 2, 3, -42)" --output tpcb-test1.html` Since not all information for the HTML report was actually specified on the command line, the utility will launch your `$EDITOR` with a config file after the SQL statement finished, so you have a chance to change some of the defaults before it renders the HTML. At the end this will create the report `tpcb-test1.html` in the current directory, which should look more or less like the one, presented in the [Overview](../README.md). One thing to keep in mind about this style of profiling is that there is a significant overhead in PL/pgSQL on the first call to a function within a database session (connection). The PL/pgSQL function call handler must parse the entire function definition and create a saved PL execution tree for it. Certain types of SQL statements will also be parsed and verified. For these reasons calling a truly trivial PL/pgSQL example like this can give very misleading results. To avoid this, the function should be called several times in a row. The file [`examples/tpcb_queries.sql`](../examples/tpcb_queries.sql) contains a set of 20 calls to the `tpcb()` function and can be executed as `plprofiler run --file tpcb_queries.sql --output tpcb-test1.html` Analyzing the first profile --------------------------- The report generated by the last `plprofiler` command (the one with the --file option used) should look roughly like this (I narrowed the SVG FlameGraph from the default width of 1200 pixels to 800 to play nicer with embedding into markdown on bitbucket) and I set the tabstop to 4, which is how the SQL file for the PL functions is formatted: [ ![tpcb-test1.hmtl](images/tpcb-test1.png) ](http://wi3ck.info/plprofiler/doc/tpcb-test1.html) [`doc/tpcb-test1.html`](http://wi3ck.info/plprofiler/doc/tpcb-test1.html) Go ahead and open the actual HTML version in a separate window or tab to be able to interact with it. What sticks out at the top of the FlameGraph are the two functions `tpcb_fetch_abalance()` and its caller, `tpcb_upd_accounts()`. When you hover over the FlameGraph entry for `tpcb_upd_accounts()` you will see that it actually accounted for over 99% of the total execution time, spent inside of PL/pgSQL functions. To examine this function closer we scroll down in the report to the details of `tpcb_upd_accounts()` and click on the **(show)** link, we can see the source code of the function and the execution time spent in every single line of it. ![Details of tpcb_upd_accounts](images/tpcb-test1-upd_accounts.png) Obviously there is a problem with accessing the pgbench_accounts table in that UPDATE statement. This function uses up 99% of our time and 50% of that is spent in a single row UPDATE statement? That cannot be right. Likewise if we examine the details for function `tpcb_fetch_abalance()`, we find that the same access path (single row SELECT via pgbench_accounts.aid) has the exact same performance problem. ![Details of tpcb_fetch_abalance](images/tpcb-test1-fetch_abalance.png) Of course, this all was an excercise in [Hunting an Elephant the Experienced Programmer's way](https://paws.kettering.edu/~jhuggins/humor/elephants.html). I deliberately placed an elephant in the middle of the room and found it. Not much of a surprise. It is what it is, the artificial reproduction of a real world problem encountered in the wild. You will have to take my word for it that it was almost as easy to find the problem in the real world case, this example is based on. We're not going to fix the actual problem (missing/wrong index) just yet, but explore alternative methods of invoking the **plprofiler** instead. This way we can compare all the different methods based on the same broken schema. Capturing profiling data by instrumenting the application --------------------------------------------------------- Sometimes it may be easier to add instrumentation calls to the application, than to extract stand alone queries, that can be run by the **plprofiler** via the --command or --file options. The way to do this is to add some **plprofiler** function calls at strategic places in the application code. In the case of pgbench, this *application code* is the custom profile [`pgbench_pl.collect.profile`](../examples/pgbench_pl.collect.profile). ``` \set nbranches :scale \set ntellers 10 * :scale \set naccounts 100000 * :scale \setrandom aid 1 :naccounts \setrandom bid 1 :nbranches \setrandom tid 1 :ntellers \setrandom delta -5000 5000 SELECT pl_profiler_enable(true); SELECT tpcb(:aid, :bid, :tid, :delta); SELECT pl_profiler_collect_data(); SELECT pl_profiler_enable(false); ``` The function `pl_profiler_enable(true)` will cause the **plprofiler** extension to be loaded and start accumulating profiling data in the local-data hash tables. The function `pl_profiler_collect_data()` copies that local-data over to the shared hash tables and resets the local-data counters to zero. With this changed application code, we can run ``` plprofiler reset pgbench -n -c24 -j24 -T300 -fpgbench_pl.collect.profile ``` The `reset` command deletes all data from the shared hash tables. After pgbench has finished, we use the shared-data (the data, that has been copied by the `pl_profiler_collect_data()` function into the shared hash tables to generate a report. `plprofiler report --from-shared --name "tpcb-using-collect" --output "tpcb-using-collect.html"` [ ![tpcb-using-collect.hmtl](images/tpcb-using-collect.png) ](http://wi3ck.info/plprofiler/doc/tpcb-using-collect.html) [`doc/tpcb-using-collect.html`](http://wi3ck.info/plprofiler/doc/tpcb-using-collect.html) There seems to be only a subtle change in the profile. The functions for updating the pgbench_branches and pgbench_tellers tables, which are almost invisible in the first profile, now used 5.81% and 2.60% of the time. That may not look like much, but with the access to pgbench_accounts being as screwed up as it is, this is in fact huge. The difference was caused by concurrency (24 clients). Collecting statistics at a timed interval ----------------------------------------- Instead of collecting the local-data after each individual transaction, we can configure it to copy the local-data only every N seconds to the shared hash tables (and reset the local-data counters). The collecting happens at each transaction commit/rollback as well as when a PL/pgSQL function exits and the timer has elapsed. For this we use a slightly different pgbench custom profile, [`pgbench_pl.interval.profile`](../examples/pgbench_pl.interval.profile). ``` \set nbranches :scale \set ntellers 10 * :scale \set naccounts 100000 * :scale \setrandom aid 1 :naccounts \setrandom bid 1 :nbranches \setrandom tid 1 :ntellers \setrandom delta -5000 5000 SET plprofiler.enabled TO true; SET plprofiler.collect_interval TO 10; SELECT tpcb(:aid, :bid, :tid, :delta); ``` I am not showing the resulting report for that because it is almost identical to the previous one. Collecting statistics via ALTER USER ------------------------------------ The above can also be done without changing the application code at all. Instead we can add the **plprofiler** to the `postgresql.conf` file in `shared_preload_libraries = 'plprofiler'` (requires PostgreSQL server restart) and then configure the application user as follows: ``` ALTER USER myuser SET plprofiler.enabled TO on; ALTER USER myuser SET plprofiler.collect_interval TO 10; ``` This has the exact same effect as the last example. It of course requires that the application reconnects after the `ALTER USER ...` statements to start collecting data, and it better reconnect once more when we are done profiling and did the corresponding `ALTER USER ... RESET ...` commands. So this is still not suitable for profiling a live production system since it is too disruptive. Profiling a live production system ---------------------------------- #### Debugging as well as profiling on a production system is a risky business and should be avoided if at all possible. Unfortunately sometimes it is not avoidable. For that reason, **plprofiler** has options designed to minimize its impact on performance. Like the previous example, the profiling method demonstrated below requires to have **plprofiler** pre-loaded from the `postgresql.conf` file. `shared_preload_libraries = 'plprofiler'` This by itself is not a problem. The **plprofiler** will be loaded and place all callback functions into the PL instrumentation hooks. The first thing all these functions do is to check if profiling is enabled. If nothing is enabled, this check amounts to evaluating an `if (!bool_var && int_var != ptr->int_var) return;` at the beginning of all the callback functions. One of the callback functions is called at every function enter/exit and at every PL statement start/end (only the statements, that actually have runtime functionality). In the great scheme of things, this overhead is negligible. With `shared_preload_libraries` configured (and the database server restarted to let that take effect) and the shared-data empty (run `plprofiler reset`) we launch `pgbench` in the background. After a while we get one of the pgbench backend PIDs by examining the system view `pg_stat_activity`. With that PID we run ``` plprofiler reset plprofiler monitor --pid --interval 10 --duration 300 plprofiler report --from-shared --name tpcb-using-monitor --output tpcb-using-monitor.html ``` The `plprofiler monitor` command is using `ALTER SYSTEM ...` and `SELECT pg_reload_conf()` to enable profiling and turn it back off after the specified duration. This obviously will only work with a PostgreSQL database version 9.4 or newer. As with any database maintenance operations, this should only be done in a connection loss safe environment as losing the connection in the middle of the monitoring would leave those settings behind permanently. Leaving out the --pid option will cause ALL active backends to save their stats at the specified interval. Fixing the performance problem ------------------------------ In this final chapter of this tutorial we fix the artificially introduced performance problem as it was done in the real world case that stood model for it. We create the missing index. ``` CREATE INDEX pgbench_accounts_aid_idx ON pgbench_accounts (aid); ``` With that in place we use our last method of capturing profiling data once more to generate the last report for this tutorial. ``` plprofiler reset plprofiler monitor --pid --interval 10 --duration 300 plprofiler report --from-shared --name tpcb-problem-fixed --output tpcb-problem-fixed.html ``` [ ![tpcb-problem-fixed.hmtl](images/tpcb-problem-fixed.png) ](http://wi3ck.info/plprofiler/doc/tpcb-problem-fixed.html) [`doc/tpcb-problem-fixed.html`](http://wi3ck.info/plprofiler/doc/tpcb-problem-fixed.html) The performance profile is now completely reversed. The access to pgbench_accounts is a small fraction (1.52% with 0.44% out of that accouting for fetching the new account balance) of the overall time spent. The access to pgbench_tellers and pgbench_branches completely dominates the picture. This is how a pgbench running inside of shared buffers is supposed to look like. Because the tellers and branches tables are so small, there is tremendous row level lock contention and constant bloat on them. The overall performance of pgbench went from the original 136 TPS to a whooping ``` transaction type: Custom query scaling factor: 1 query mode: simple number of clients: 24 number of threads: 24 duration: 300 s number of transactions actually processed: 1086292 latency average: 6.628 ms tps = 3620.469364 (including connections establishing) tps = 3620.869051 (excluding connections establishing) ``` This is a performance boost by factor 27 for one additional index. Not all performance problems are this easy to solve. But I hope the **plprofiler** will help you locating them quickly, so you have more time fixing them. plprofiler-REL4_2_5/doc/images/000077500000000000000000000000001465735455400164415ustar00rootroot00000000000000plprofiler-REL4_2_5/doc/images/pgbench_pl-1.png000066400000000000000000002362761465735455400214260ustar00rootroot00000000000000PNG  IHDREVEDbKGD pHYs  tIME&9 IDATxyxT>w-=$$a K ;n UqaqA*Upi jXZ+R6 aM@H dH$F::U;xEEE|!"""""NFALT$^y5CDDDDDmCןjh}>Z-Nر(**⫈=׋.]Sjjw>Zi;G p8pm+*Pv4 F#]=)5pY_rǎHIIntJMí@[.LCDDDDD皖 J\NTd$ zlBoV`Nkֶ\`q̖:IIIơCq;Ą|܊oC4y[.g fΜ 1Ն@Q~~+Wa"]}5`q6`0buk~` Ɛǖ?"""""0}Ͻz_׮]5{cú_b/q=vuu6-DDDDDDaχPh2[ng2[B~[<ЖsC߾}|ܞ={9MKunIDDDDDںvʠ?]vmwPXxL&E˟$Ip\5s4Bh2AѰ2B`09!wl*N+W5:ˊmfRǍ3gO'9sf툟!iO iхDӵIWN~[B&P;y DS@PO$x=tgBݎX()^o`2Ŷ꛼`45'p\hW"7hٕ;ɺΧ-QPttGQ^Q)-(@T$:+iiYeJr2_-VZ ÁQFbqx '7䗐҂)Heez DDDDq80nu+o޽EUWBJJ M-Mm}ƏߞŸǃ(L?&Qp8*D|N  Gl!:oŢ~ޡ|6PA79ONINFFzz #=U~z<جVXF[c<<_nEYp\0 $"6* C~cTU]jhZhƖ ǎW~m^hIбc#|VWW y4h ݻNޏK!Ht//I^ Ꮘ go~Nf9b Eii)~G{zVX?(,,lu6E]3gSNp8(**Q^^v9~_aƌ}϶ٳرc4ndsFAUu5LƨV/né ~hL1DDDD :d0 :$oϿow ,I^/:Ⱥ}QA!==vO 4|pO>itIqNVk)OtM DDDD田ކE,Ģw 9XB 2Yx<0Hk[VX,h4 bQQQӧO?|M\ve*Ok:vo2y%&&ck@AA ѣGF +$նh4tlkFX-۶?k$ >III^Wkd2%駟/ѣ &f3 2;v ǃ{ϗO֭Vk]=z=L/FYYYx :ϰrz#꼾POqF\r%tغu+^u9fL:_|1bbb)7|\.a˖-/zkjqeK.ǎ; ;|pjB$TVV.+ |I\oi&L&[^ֻwo۷Nk`CQTT`x]h4t:8nb6 ~v$""":zhѡZ+..0ҥ M?NɄ*̙3'{ƌÃ>I0k,̘1?pcŊx衇䰧Pmʔ)Q]]3f;+8tݍ~ߏE!!!w^x111?> ӧwŞ={C$|#F !!_}\.~aǎr8$ GBtt4bccqqhm4_->طo,Tp^ c@F=_f 5Nt:]ADDDDI_v~zZ|~-orx5⩧oyoƍ{:mۆǏaKcĈxwP^^ ;>|xؓEjZ3-Bqq1 80oB,z??믿K/Z GAA~:߷oO>\r 4 Z- p:Cǎ_TTۍ2yDjˮ:q6vti2٬7.{A Le"_HyBwv;6^H.*9#d:cbbjF,**VEtt4JJJB^Wll,FcFlh4"&&z8qD/**^Gdd$$I^ǭ %4trNR0zh'p0ֿD, U߇ 6u5jaZPt%Coqk<8>#\5jdmŕFpp`ciVx܈j~ӧѱcG9r! >Oi"8HOx6S.]v0Lԩ=!Ik-ZC8|p9)^޽rºO!:: uBhѬj^WUU&Qh &n LO^/$U.22ODD_zi>T0}tK@zz:RSSre<8p :t耮]bذamVnݺjsΘ9s&֬YߏcǎaϞ=5kT :틄\tE4>]%u_rU zxNU]Ov|ML>/2~?n݊E5k஻BNN|I_¤Ipw#** EEE8z(n&Q5kɓ%K]w݅YfAѠ8n}qPUU;wb֭_+:%%%8uTȓ»\_}EɈTU@ jਪbn׸fCMi48N;x0'yaj`Uxú?T~N=!no:Vkj[+V:Ȯ]mvMÁ_l6C]$r!.60lSe|0L|cMoϿ5|* wt~ٳ'.}aZt`R?FNN Gc[,6 N.Σj'u!!%Yah4jh4"==SL|pBB<dz.jAO015GlL41h L2(**G}ؘh?R@Bסe孜48Q{kZ$IF$HqG aZZ$I&jh4IDDDԎۯ^ F=QPtb83bFJDDDDvyf(Ibݓ1Qz<$ADDDDN$ ZIIг .,~i gVvϱw$I袋5FQQuYSSSN:1ш8v_V g#""""jA圸ᏈGDDDDDGq0vXVh7h4h4fL:oN̜9$o -f3 t$%%Nqjժ ~߫W滒aOv$ 8W㑖xɓomt jKJJBqqqHRRN*a6l3)) dž ХKdffXOe4{uoY }Z:Zz$A$h4|>h4x<t:|>~ 뾩NCȁV/5[a\Yam_ݢY$~G]_:P6enc.IZ-Z\7uJDDge]L8TTT -- C ɓ'n:8ΐ!C IY #F#qFlܸE׭뙞dii)^/R:vlh"x6{GHJJBݱwv1vXzdggcXf 6n܈\zARE!;;=z@=&:tkaؾ}; СCam{1qD1bG}$ȑ#!-+--ŕW^5uyaOh4l6d2bd2`0b@b@$ 'XPe4|00j0 df`l#&ʩ,:؉P'Ihh4y^gF_mz`0zL&t}eS9eYEV\vV NV ,h40Lh4v cj}X,0͈@dd$СC"55:uBdd$bccW׏K^/AA˷T9@\r& FoQTTN/>d2W^8qDfRш+3gɓm6TVVe'ƃ_^elذػw/:nݺO>Ɂkv Ʉ°ɓ'aۛӦMtڵErűE0 t-s_ז\8N,Zr uWmm[jj*>|EGNg>@z6VQFӧx衇 /W^} .sSĠk׮ ynǤI͇"`6aXDEEn#&&0͈fCDDVfD@`0I ^`_y/Zu#~DS QV&$ &&Ftt\'шAtt4zjnjEDDn *o[Y_ N`0 [FVUOqLıVûU`娪lF]">>qqquHOdd$#'OY~xEGGn#99HMMjfd2!22Z999~f5՜u=mZmYj}[, 8˖-k?m U||<233}vdggWRRkl6\&T())AII 222>ҭ~@ tP2$%2oQeIL|Eh3L$IIl68eYaYX#WԩNC$Ip0s"""䮖zı]kEMIx`4c'^ cjr`Za4aXx_lOY`U4?>nu\Q' Gzz:ovddd4t (d"Fkvk/H[nm0Ek^ sA`֬Y |tt4~}bKj…xZ`͛7#e .O~;E!//O~}O8}VŞ={xb )) n7ofC~jl2lڴ}RM`ȑHNN_-=zȏU^%NuL+[Z-|>B@캧)P. EPjxEȑ#֭?gAaa!, ΝKb֭;?w}|GEϞ=i&,_ϸ}㑒Ӊ}᭷ ʶ5 n >qqqr… !IN>>UV!nc̘1uUXX(wOOO_phil φfiӦ!11Qny}NQQQG9sPm)҃6XN\ٳgc…?>y)SGŕW^;_~i.::ݺuë2Xh~gTTT`ܸq6mo6 ?M)SX\. I&a7oo[nnpѵkW0 ؽ{|e0Dːh'+0aCjuR_D'r=ljnEM"t:< \.L&N'fw\h4poNѢ$2|*C2̩2\WwkU/H'ZqUeDD!NP[z x  X "\/X[Y1Xnưav1w\\qذaN<3/_m߾V„ 0}t<a_ʼn?!%%s\.֬Ygu IDAT`=܃ٰl_\jlcƌakwPy桨;vgQll,ߏK_~2e {xp)~QC؅S-{Xp!z)< )))~"jtt eqV\#>>^B}Ԏ!ƽXre[s6篭8W:KJJjҵFf t: 2k׮Eee%6oތl6W_}=j8p@E.ǏGUU6mڄdK.ҥKvvtR 6L" ?***+--͛a0>i4 8{^GMM !_t:.555pp\x_`(//ۡ:ILLDnɓ }*-uʟjJy-Q_BB<ñc~zvd`Ȑ!Adz5j ,Y;1bDv'&&&U 4`]QHJJOݱcǢI ][ߔ*$:::N= 475.SSLDEEAwɀQ)jjj-FT$IAϞ=$ÇZJ*_p(6%2XK2V`= Q=Pb}#խGue ФV` Z~}o;1zuM\| mC슪t:rPѭSt`-oe8TUUaԩ(χ˗O>cTWW߿?y05__gVQQ͛o/݋˗OB\Rn[!&&'O +T7TV5\IPQQ!:{o^2-{%" ߡCPRR#GaԨQصkWd2!##F#222pС6lƍr_ 1Js5999HLLİay5 ##.f'r.(KP5$!!!Ǐ$$$󡼼PIˣ(:tH QYY)wt8r rq[ٚ;C:)QfDDq r`4BDF4kaDy-2m*I}׹ƦaPףrQ¤2+XP9O`e˨xOuW٢]WxuwbeWcqeWߚt:GuuuTF믿yyykyfԠsθ뮻ꫯ"//x[s)55*,,o͆n <̙r>UUUHNN?F؈ABDgڵk1~xL8999p80LHOOGdd$ӛ< [;qV[sf4 g;8Ndffӿz:P3˅;l6)S͆45_Z4`1^[l Ʉ[n7o{>EDD -- HOOǁekOuu5N'*++QUU-O9bN=S-RԏW_'p~rJ1m^[Vyz 1n(MUn2~*^UnS`V34"ԩz@eSrk >UvSU(t bzeO]U9݅x/)yڈQIO:R;111\s yc<~eeeZ⋛$ٳgOtMꫯ̲l:t;&yCK m{ӦM[ѽ{wDDDࢋ.ݻl6G9U|mHAA1c $'' Jx Ӕn},; !p8իWMFDMp8xbڵ ݺu_L;v K,TdR:j(?ǏGTu>зJv `k:t:a;cǎEΝm6N׮]ѯ_AWk:ׄ3gS}7;7|onv¤IB$dggcɒ%MGOO<pX͕Á`CMtSъ#'NzDK^tS(Zɷ^OhŴzu;+)7p:0 at:jjj I*++ljVCFeWXI_OS)(Crj1(T*8eH}]̝;YYYعs'mۆN?S&}!IfafUW]znsm/]$a̙0L(-- xnYY&NΝ;p`֭/|+kC~?^yL<gϖGT̂ ꄽ9 é9~ܹs|@P\7|3ƌ?PNDsAfj ]7#3G|| ̔dž;Ls/o'Y!4;)l:1 hkM7c͝q-ǩ?~}9/> u٠ &11Q5O=h%Rķb9:AD9bzL7KeT`'(W)Ell,`2ryՁK>EE=( U-c^,[2tFDDm̢|ݟw?sm60Q{رc!?0LXvm)$֮]+_Jk)v`8k؞lՋ/\l1VٕSݥ/؈n,V+XߨowC,o,.?W򈈈J&zE? *A+QXr4ڵpصkM`Raзo nƌIXwkX]]~ |r˖+ѥKn+~`04<&Oce w©/J/23W<U0`d<Яtɭbݺd]pス0zZV K}X +a1rdG7,99sv|]>J5|r%:vryrтO>}·'j:](>`v횀KGS'J|/Mk徔Ð5k\޽Զ:.Xp&T[W (|ؾ},vwƫ$6c7GM|Zp>.KD^ޭ+iiv'F^ޭr̞ݏKM:3WeᆯnݺZࡖA ^̜JŢCllmW9a1xpLןiivƚc06bǎ۸>!m9B=&jJC_Z ٵ.wdf]0ZۧͦGnv\{m*Nv,È=9/pr={Nh?˿nB7lX|AGxsoK1Nsz>}ΌZZ ~<&5Ն܏PjkEAoS;N DDDנ0USСβ |[bӢNjމs-X}5Fn7,6ʷAoGD>'*趔Su7Ǩ VoLhpIS5ҚrM9Of'Ϟg[o]Q:<սn,*(;'H8vj׿{Vi1m|`+PRT3˭V*+kӮrw*/wXt;mQ6^&Z9&-Rr|A qTv6n|6o/Au7gγ?ڋmN駳n-[/:PZO+\gB=&-I*x" ?\Ã??W_j]q"""Pi`+k 2 3f7sqx &Mw<@\zi]WX{W ڋJ?hbРxL}}`n߿m|!9RU|'aU9#\zibm-zܸ.c8P_:pQӟx8Ǥ%͘!0u ^^W?}>I݌{s 뉈;#o_#m^A\ G'F{65W/ܹѯ_,V=z 81 W:+Ṳon/8C/$'[khi=#Z{v/  &QnG=3.יcs FJʙLZw;mQ gmX9ǤXYp8٬ժG׿.o~ GDDD8_qRnn.j52pVXZ&i閑~*ŭ~ vc%cBDDDDmvj3`@ӦM`1[7.>}ӟ`23pm`S'q]"""""j?:*kK{K{<_Xw_ fV-}DDDDGƍ7vBZZ-۷^oo IDAT(ӣpu0qb ^0GDDDD tN}c0PV #"6111111111_+C!?Gw ݻV>Q҇srf4jfرq5˻uJs)ѵkD$˖c|9p ݻG[t x'lY>~ УG$nn- ZN}4-ҥ-GM:Xp0sfbb~% X !>ބ8qE<8"""" 1v[wjWA^pM`4hw nx!/_;[eˍx2"+T^G~}!fzj v xꩁ0tX0֮-hvXq_~YQ;!;{^{RǛ_^-? 'm/Ы2\vx,TVz\ի`#0x~ضm,_}-n;RR k{i\uZ<{L5I'g3[xQS;]Z&.S `8ϯ߄'rxltdW6n,_S1{TTxCm4v` C,]ZxL./2;**x=XƌImV})s˯&11F̙fyn%$@QQ >xCT>-[> -͎s[oqbf6Vp8Nw=v>-/ӃXtx|eoP;ϴ zʕk yrPxG'g#qD1$Xw3ӣ0xp<åv@R%,{nNx=ؽT+|pC: o죏ov:o~{ez㍝︔ѧO +lSEF .Ό^؅%Kaݺ?""""JfQo[J/(GZ~zk#-Wsd"!'`u3';G[r|QS3W{mQj(֧$hYl.:XB^؅  qS^~ꔫeBz0GD&c*̛7=zD|/,9S:5et`v:I X UcG+ږ'uHN:u;ZFSmsʔ8}ڍ;KuI|!lPO›o^Уƒt.]QtmHNvĉ}3씻iHA!ݺٛTPгg|[MI<bKPXX%]PP{[@TTT#G'0&ƈ#;b~Xb4`˖jdQQ1m~WXH6m*ƍ7~O>9(/[(7.7#;{Bлw}US1jB9z ߕDDDDDc̬݉'JK]?K|Ϟ{..'[1x*̟GTP\\ujGTv7_ߏw9ÃJ/>0>Iݚߓ'׮܇ ̍J/֯/ĬY[o_~Mp8Xߝsʔؼ֯/ד #Gv_o7r{z)QW\,ży٘7/;౿m\sMJXW`4,Yr>g N p-UWh_+Xao  vm~~W p}]Oe Q:+-8}ڍ_*N7?Oo_~ba0hj6^A~5פG!> QAg$%Ykk`UW%wL`0hhuWz&]F੧'o0 Nd+;KBtQ~O 0t3ouy:v΅K^yb< dffhԢKL _|q5|o lS"""""j]o'Y!`Ŋg0)9R#?{Us6rxCP]4W--53]ֶҕj[k%oXJ+3MHM~Iaa~GPesΙ/(šC_s.ÇÇU93 cv\.q5wjPPP6馉lW`r4jfҤѻOڀ7tdnۏ|tF#o;IDDDDDM)&>W^هW^Pv} cxQ`H$%n&MPVw@ϞHLlQF#::4M: -g}^5 m,++Cǎ,'pshY`##c_}?+q@L<W{h׳o95Xn5r#`NC߿{oKֱkq8q8:vh{ヌ####klQ&@LL4&N| Xmxw1$''yNv݉'a4ѩS2RiKld؋|,ӧe>Z"f3M"^;'޽^ii6zs6;`_0``jw1{ Dz 96miiѩS brv1/v.Zl[o;vņ >l5:t֭cԨ1曭шnxpyG\\kL<ڴ9sػ~@ǷAJJ%TjY7zw眿pQtY׷V J,._v.b1aSȟ'FFF9' Aȑ#Vk+*Ap@02226Fff,x: ::ee%^ۉwh#:tP!&M+W0}T}o|;[++`9q=?y;qW;سg/rrfaѣG^_>< gϞc^^];iDZcЯw Cbǎ:t]?N٩S2F'NDV СaxZom}['O###c3k"wyC-W$l޼UjjQVvcG۶՜T*ũSRyدs;ee瑙/̷D_1### c>\xn}e -Z93vl6nŋ`m<X`zk:v>X L3h,wCV_]ߴ<(y'OC˯9DZws$%uÌo xaD$$$ORѶm̚ { 2e_w?@]1etW^gR0cƋWs-#y<.~n.L4r>m=v8:uJD@Ć ѣG@}aF1222K<"""GѭQVv?_Fn]c####cfȑc0v >_ǢE=.9?ș u5g8ș |}Xfdddd(*!*/q<I$c}7`ddddd[:A5>y5k%^ q#r[g}q9ʢJ$-c!ަ?9>">U~(zϺs.ՠ yRvDt% ?FFFF&E)=OoNF [Jnߕ?D11-ܯrs}`Q2t׵xcn5 TȟZ| ^֡VtDts2V7¯6Xq*Zr=cՑJ,.ueͮNVY`eEHyt:c42|}|Zl׾]o_ԸRmD+*Si8)Q.]*M>: ǫ\vUM9t^ݏc;V^-лekJtO1Vՠg|R&l-)h&lq}"5}J/~񫾾|% cZ_0OY} k/&Xqv[߱}K;pIgM] G+-x6"1;zfYgr1Xru?ykvVc@T j vko* 1*n[[@o6fGWsq|)1;OD͡w_}Q6 K@r#gJzSw׾-(b8neWmiݣƶqnn$nUfoC?8)CIѲv(-&.9q}"jş'X]wF>51iϩaxː ꣕h&uhtHr7}qwec׷mǿf}kwY_\mWa",۽9NZ(< }t\n=vug~/ \gƈv7k<%Oes9G`Ԧ287%=sorv]mn>̚^@v#G"W:UJ1N,~$.9g)BCԼWAh"pA ѵy^S(:5j5mMƞ} L.o 0$ K }CgPcCYW3O ;IL|-dM#M-ʇ>G wG;6%H<:0#| _ROx^W(.D |c=ƦpQL‚`|硾`ר @7b||D9qcR}qˤ\y\1QI]7hf[Ҟ'(uxwo""""jn\54QI`Yԥ}OŚ/5YAD8GDDDDMmb B Pg_'yMDeUXVPdddddddddddl^QbarJH۷Es毉9+BT_2'BDB._n&y3i`t" ?nxv9; FK~4bɏF| mu{fDܺЀyr :׀ L#׀s XוIKF IDATlͶODDDDԜ K3XCFΓJ&oge?I=owf(cv>c@χdV#dIpp9R|7Ey[M<#`yE3H{?DDDD,u^2gk :s#`i<6ngDZ=lff|n֣@]w~~zj"ZiB]`D׹yμo%3kL3@H1}#&?ѫ 艈9\guE@"ϟկD1%u&I)u߈X-Oӭv[߱U@T0~W捯MFݝ"M?$智3f`Ó|[tXEgh7my?YT?DDDDԌ ?u̦ͨn*?p󡗺(+uSw׾fTퟳ+RtJ4z1Q=@MN݆5wg̭oD`z"S B#ZE,6⽑|7{c#:⡥?)_dtjq}*jS$`Ŀ"Xys!`2#G"M]ό3Lp"z}_7 5gZ)wz?""""jΚ̟0#G@[EFt7ʟ]>_^h?4;X$է#:`MY>ՏQcېrBѷQV>oƞ} L.oI$W3͜?ʠյ 8U&Rē4K""""B"7&{͟ Il2F:ѽn{IԾT"#""""jl_l25MS_DDDDD䛀d 0m⯑~xx/@@_hPUKDDDDD~37&0'IDDDDD3Q#"""""3@g`H+s"""""/x_#90>ODDDDD5M2'"""""bqŕHZ%""""" i85zH$vW+tc぀ 3WTg3M#M:E3v bddddddddddddN @02222222222rZ?"""""f3vD>/@FFFFFFFFFFF~ׂ3͠>yƂ`ddddddddddߵ>122222222222E7=fdddddddddd5͠g9?"""""f3v ?FFFFFFFFFFF~,#"""""P<ȣDDDDDDt8GDDDDD⏈X?""""""bGDDDDDD,#""""""DDDDDD,#""""""DDDDDD⏈XQc(:v\ܰ}] ތΝ"3sc׹Qnp]suW}ۗ~%"""f\5eˎ[۳f0DVy>T4>5nĉGo/\v-uf""""&WguoU*.Oo/PXXQD L}[mw?m[ݾmj HM]_?w_r:mo q5CcxN*o'/oÝ+Ojt/tޗ_Ňq-[aʔxvNcoK1~%(5С =&mÕ'5xB!2R? |9hHO\\.+5 @nnFlx]ؼ-߆݈a?G99{ukWb던2%vVaoڳg BϞ >Xd5=z{8,[BCex[,=Eӧ+?S۟r±cjDaAvd)/cMDDDDWs׮K-[Ƒ##.GA39'N|ݟ+R`)A-wc1jTW_wdg'c Kgx'{<]GZi#? ?˗7)+y|ٷpg^HHF)r|m8TbԨصa۶b;W'4Lk >7o^:u Z,[v&={JjESby.>>nY}.9yEY]bb)-RCbb"ۂΕ7('筀o_cR鮩ߌW E%._ɓRe)-[\j𙿔(|8rv]矟e1g|}>+Wjp|%:t;g*"B~y H|R48 L^llJJ|گZ-\\-ڶ `9T* 1ټwŷaxtF8o_ιsHJ2*.cpsE@~-ѹs${%r """""G >쳻~'7M7puJ_aa]mrsʕ\TySߢEAIAR7C.k1l7.]"r(.œ9N륥E_-⊠z@tW} FXh>˗}w)ոroqVԩ߾}[@3O#"Bݣ 3mڄZNK%""""r̟vwÒ6mBy9L|+Wj0b6mM[7o<$ųϦ\@}wk|qZuVrK4BCe b O$+ /ѣv_W_<|j"`1':cǎ,^\ŋM7+ ஻{wX|] F [7.((/_k?l@ ­n[ʡǷoX$/СH_mD]?\i  11r!?>s= wrNaXr k )8Z52s-f##$DP,rꝻ9woߊHСC8^z]nh}9V_,K@`&1:il#2<@[c+'Rtې`k-[ <^z',KOBZOjp]r|{<D?DDDDDM۰aѡClذL Bx83ؽ}6&L?{k1bpp5-wyiiR4_L2%;vgLdTKZ-b~s>-3+*2hQ^C봬LğF""""XM6 8}4fϞ-Zf jX˽oV %7OmKi&:ZJ:+WDƚ֍PZz=JBaZϼ/dgӃ [OӍ] `Pt.uj[Wo O_~ǭY?""""&YEm̅Ri_I$EvDKd.-+.UKC1p̩3 ޽E:<oBDDDDdEgϞh׮^}Uh4נkA `F=4 7W y өמӿ Vsg-t)wNLknrWR8`=s~RS.2D߮Fe%`A5\/#C<, O1"˗נ\ĢE4HӲ30ujS/5jO?AJ}~=zHHDDDDd pVV}Jm۪+`ӦpsX>J` RiBYu?!6LFD^^yAPZ w A׮P<-s^=utbj:Fp=6wnƎ@|2,[c ,XPPGhO1c*B2,_Ӳ+uXBS79v,,]?3fTsgsF$&JHDDDDa?(9rj-2z#??'eSx5ܴZGrl D` _DDDDD͝ZMzz: ;;{ 8(2fB2B(Ybn:t&l/H@R𧜈5/*Jhg2^lEDDDDD⏈X52ų]5{i,/wD_:źy#ܵ}݆}yb4'W!,Lrlެiʕ:r̗6&k£G =GRĢE5(+j5EZ-0~|%l1Yu0 Ϊq]%5 pl~\{{]fbmۮ&̟^+fJtXō㌆kP(THI6ХK9Uشy6d Eyj˗l\  4TA4PD4A*-!6VVt99Z߷gDb̘@̜}-f ƪUa1i֭qEӥsԖy;e/LV[nE23e5K_jm8`ɓxPocsU J@NN0&N_޶L"~Հǣ{!>{kw:`y}Æ ={6T*.\,ߓp! ._ѭ[9JK,lO},J?D.HM-ǥKQ.3©SG(-BLZ 0P u:v\:j˗+@3svOOԢ @I49'&PP8S;8Բ -hJi299A93oŮ]p˾K`ٲPqSDDDDx燼7ߴiPXXӧOc~oߢ B!@]y` %7Om<[PASokFxJm;_3j9맧qRDDFZN uhwLRT" .W_ Dx80}zvi?ԩA.SDDDD<\>Ebf[. -+-oo=r_Q\ZQ4}UW+?SiӂP\@y~y|q/PDDE/֯\\:TW_>۵@|2Ӳ-[jѧҖZOoyi2ZΝҥ:11JJDyj߼sOzK*` q/G׮Rt"N'"7W'Nvq4NݻKq4*u!rv5*+ 1x;cfdȐg%Kj P8}kP^.bѢj $ic[k=-&5 0++˯HѶ 6m^4gN&L0d CRiBY!6LFD^^yky']C򘛫h`99Z߷gDb̘@̜}-f ƪUa1i֭qEӥsԖyْZįpx{/fYۘ4+VAQ`֬ L^cq 8`ɓxPܵo;6RT!3Sb Y)L^dغUѣwo0rCG ڵ:7͜Y#pT jk/ 8tȀ8(*2wo)$kG~ '&*6l؀tt6l{)SyMyj߼̙>`Ξqq_Cqc_"|jfD,9~r=_߾]Ŕ)AرCoɓ <_~iZe#F ~3 -Mj&-MvӾ3"<0JQT8}"7< sU̬Ȉ^¢E![K'7<ߴiPXXӧOc~oߢ B!@]yjaaB-r%6F@#f7TD߾jceLM̾M[h1&F@iPD=p&)|*HEG P:LPvp٦q "24:lK{wk1h1u*#"""%;7J}A fEvDKq`~IT-+.UKC۲}-1_+IDDDDԤEgϞh׮^}Uh4נ7ZdUT}Tt+˰j;w֢Kr()-ǭz@Rxڷ!CxjTV Tc`6~83gj-|@R8`-7 5Ur{_!C^i,i#F`X|Z6cFN ry$""""YYYʪ))RmBlM-ϙb}! AMהRiBYu?!6LFD^^yAPZޤNv-J17Wmu瞫ĸqh^%KB[c3wnƎ@|2,[澟#FZ^m-z(,Ժu:Ж7/GW`j"(ZyyO1c*B2,_Ӳ+uXBS79v,ɞs(oG}Gv#G"W:o|R6WF[SQU̟_]+k=zcp =hp`$ysL""""jj5mسAhT,RP[F[17ܚ\eSfXGF*@DDDDQQQf5橈qh 0xDDDDDd&kR8`@~j*ѫ{Xj)Fa!?GnWy(@7?VaŊ0WdI BC=,;cTg | Ox@*0aB |~ĉAtI_FDDDD,Vb"#رZ;θ8>^VBms]#>^MNAqAP17OmNHOzS[u-c4sfFSQ((瞫Bnn0T*rr1qb׾ 2+0`̧>dZ-_ 8~< YN!5UI*bE4f }0rG ڵ:7߷<= (4!THOWӲ޽H̯A=ޛ)=] A0˟|R[y |RJSq9h刈>}Æ ={6T*.\,ߓp! ._ѭ[9JK,lOdz},J?D.HM-ǥKQ.3©SNOLT qq>yߎQәf8 㖚1qb ڵF*>XSN%%"\yuLe2%rr0sf0zK]j^1l_|ݥNV|mMcpݦFiX.]x_'""">?N6 8}4fϞ-Z޸+jNە> 0^rԖy]K*´\okFxJW}(-L7Eqe0Wt5v쨵d5h*g]4<\@vv ÁӃ{wOӍ] `Pz`pj^Cm-$`=>ݻKqF'*u٦k!xJW%Xĺ u:K1n#FlS:ӧa̘ $$л ˗lJVaTlDrK;l=0) bf.V[ojok+LKMME\ܗ|qdagfif3\399s3s92'iSGw.ƎCmDDDDhUPenRVCb9֭m;t:gO N3sCDDDDዬ& L+T؊VsTP<T*QW?OOGh:pGDDDDD,>\,]|3K{\Ty)qV׺ТcG5:tPA]W7,4n͛%:OhTfm[cX\*^rᑅ^r̞{HDDDD,x{(?6,˗!! C栰K-*R)UO̝,6|x.^~ ^xMg#2JKH(*7 )õkE CY,$&# 8q*{+B˖9uJfͲQ 6{w RSEBt6c,)qيrG6jvm]gq1{ÎZnK7֭ZgxqUS'rK||T[WeJp+*JJ4Il(Y UcnYQr4n&Mе񖫿7DDEiH \[mBBuB}V+$-\Ę1pq rΆ / E8 j 0 7ێ_"@rׯ{a0gL/#Z\⁗^r4)'`r7LW}6E aa6c,{㺛M'\䉤$]˶\"4;6ӧ@¤I.=pDJ&3{{o]1uj9q.^yJnIЮ 7oz!"Bf1cr S* NX~RSХ6[A* N_05RR{}?n^͞]ѣ?e=7t tŋ @$7;ͭLI<3g-nu(%E>}4X Z9v">,8:fA )KIDDDD|{?~<),pt0\T򋓈QwONE,k˽sG47HzA`oleo\Rj-YY?vVB/HMBv#Jɷ hWPP Ź"=o!f*]޹#zqY2vc\:aCh4?@&$A|]ejz!tfƎvիz8ێ_3gڶm bڴivBh4ܹGoݺ֯/FL| ]eI2,_^{h,< c,{:VKqt C\a}mR.%Ѭ#DLoo.$e鄙3[GotƊEjƍӕlVqxɵnǎj]s ̚Un糕[cqZQL.-Dv9s %6qbƍS+ʮ8~HL,Z-"1}mGDDDUׂί-Ѡ >>6l(O]MKWA wtuBCBHLtp=ӧO 4qq%ƕjcb\4jEJ pªUEbjժ"8YY+^~9ud!"B_~<0K9[WaReˊPqJndsBBlwx\̘H9-s#nrWAyEJX &稦Zƶm逞=58q =/40J/lSqSc+[MQMTrzBP"""GU*<=Yý^Pp#""""zt90DDDDDD,(Nk,frQQj~2㼛\Ju;ѡuE&Ovn81\mmfkYu79<911NtIIZI,=PGvdy 2LZqZ-BLѸ1cr S*0p`tʕEV_""dHMB.2A* N_05RR{KA0< S2W(\Uj{ڵkBR᫯Btt,yNoh"ҧ~/Yqn[<-g|¥KUKOb,[qU[c 'OzW@jzTN,|RRx 4T}=  Wӧ tmXY@ II73,,7n͉LI<3g-nu(%E>}4X Z9wzvCzAD ^+^ϻ=!voHNN˗Keg)Ml-޸&JOA&Tr'뫂 d_4]/ez7ddQ05њQ2oڂN33EddX9qw0|3݁XJj 7vGxe0e ^}䚿9s\fM6TNN#"""qONE,Bp ܼY~wcOOAlRb,{2VkqxxW^Eÿ#vP\V`Ȑ\<.i,ɭ .VNlٰ4Im"sD׮ܴ: Ç;KƦM\ wxgO94""""ߜ9sжm[4lӦMwB/D%?~}14`|d_hv5;˰|yբYlx{ HO+ֲKX-Iu IDAT qq-ZThVtRz={:a|֭@ӧS'5V(V 4n$Z9񒛠;Cht.ܹf*@nrʉX-(& K";[Ĝ9Ij81)~XŅhٲdl}`bܹ#bB-EDDDDUBV] :ZtD*ذ:O?u56/]VQ4< uD` ; 1ѭL>}4hD)MK_/6o+7φJi36K*.\\j,\.4(f .N Y[8?p:4p… 㵧Gb\ FhQɼ8aժ"S1jUbb/:u!/XmesK/n]{LKږ-+BBBƍ+͹s q@|#Gb<4mhߍ'z oqUj]UF'5*a5$nmܡ={jp%"""GKUEVRm*njl\Cb9 AA%*YPxO"""";5txhf[UG݇ѣ'"""""bGDDDDDD,#""""""DDDDDD⏈X?""""""DDDDDD⏈X?""""""bGDDDDDD,N*ޝn6iӕX_Z e 88/,B]A*N/8t(8DDDD`{oEpp"6A&s:t7ZZOZ_ԩt"22 [|,,?4`[~?Ә1-ODDDDt?pH&ΜɆ W[ﮂsBff!@<~;b|=}秸o ]|iiس'~~ tZa+vڵ.|}س'SDž8'܈-VkMX0Q-[zA0oxQwex?kj{?I=DLIqӦjQQzc+"e;sjDGoC0p\Ç3ѫh CƝ;!_vhq5c`,]z[@VV*w 5@~ eNNpxmŊKP*exv Ƚ>Srr>$0bD<}ZAv][wP ۃ_B۶kҤCض-~Z.8x뭖c8|pck{?qj.->(ѣƖ-7m[*Z2NĮ]Y\￉:`\L\ѩ= [os?<$%ebHI6fz-|1̘A8}t$'gho"""":v0pPTڵW*K4V`ŊK ;AA>]…:}Q0[<^`~d&. D><ǎ 7<vŮ84 uѣ苿^꣸XK; Glyg .GRRqd7,֠{{$$\tFܠRe*$|ʯ > :~\nw)JiS>Ƶw:M۽;ݮq4k8s&:DDDDT/AsB&LVVnYY98Y%K.`޼S:O?} !!Xõvc͡TЫW}`v;ɓOoogi'O쎫Q#7+&C񨕥>=u3&NlcV0hPctrѣ-Z˜10)~;N:4Դf23 ?ۡ:DDDDTG %%z_DtZ8|8AD|Y8Nx~q㵻;6mazk(J_V*7 [ݺiƛ~/:IZHK/ǘ1K6%?7 |\*UiAprr֭7׋9cbY]sr޾]p_?J0;C8J?[jvl{wsc쳁pqq4ƣP:N&l.GQ%ň`q#4uZvղ'~lЦMmdfZJE@jj^ ={Gff!~._A۶~W1Ǟm:…qsC Xݷ, """"bÆprr~&%E6]FS:nBgŪU (Z(ٜ9'VE {pӲKAn7_Gf+ѧr&'߱zOO?BܺU/8&oy:õk6,t㑲w,M4(#}i2n.@ff!f8ؽ<84Q?1iSpaJV[۩#7אw779u辐t~|zOOz֪%OƍװnUS`W_m~sOaܒ^~پe=L},^|˗_4]خ]m(2ӏo{;vaɒ XizttCkoogdf"&f;ώ7l?H3MW(-+0P͛cԨfm=EuNJ[T]өwO xx'U.e;Uq޴i-~}!_( C`ڴ0z:}nCxq!YGd~aBH$} CGpL.!ѣ>( [\ټIiΩѻw5;ը/88yE`gǝ u]Y{c׮4DEՕr"x¿ZNɼb׮4ܺU:;j-L7&Z#99ɻ 6埈U|#e+bs` X?""""""bGDDDDDD,#""""""DDDDDD⏈#""""""DDDDDD⏈X?""""""zD'>p$QXBo{Dpp"ݍCw#88{ެ飲JW AȂ dݳrXYwygި0cƱ;U.#2r#BCWc̘?KnbÈX3Oڋ6- ;C7DzzӧUh [{ZY8r${߷}ؒC2;Ln__>3afZS$`X|@^^I;ڋIoMc*9vyY:XzZ~ᔮkO ]^څsM}zf@MKBQ޴o[u/]zO=͚DdF,[VOj,ol.]6EU0)>)y(m[m3ZXcg.V,-7㑺{Rج;ŪՊxݿѲj*jh߬ϒ%&'OL'$ G OvM_;vҟ9Kl/k𔜜 /NN8||3קOlvmw({7v7*pj.zi5;xpѕC&a˖ݻx<_XcƔzFU!NhGh}i˿֢m[Gxx9=X>w7SHJԩm%x૯"tiWص+~??tCq5,rޮX,ů׋59[_LJ 0iRc+O~(ΛH]݌ESQv0qbk?>u%&>g…m[ ;QQ{EfU}}V.]( ǩSϙ ?zp0(0r%jLWUl۷ ?VV#&f;._ΑnÔ)11ACB1b^<7i).G߾OUK)SZR~Ϟ5~۶CeV@Aз<1{ `^}l>O77鑑rHKq@ǎÕ+zܾ-KmNDDDyWUbcCQ50Oѣ:uEJJt*ΞJ(2  pJ=X3rdS0dHQ9\]Kg9U')㑺KUz!!puWڽ3g͛BQQ=~,ҥv݄3A.c.7M3V3gtyVqV/@;$.FDZcwжmmE+f7tEvVDNjK{w 1yr>T*\ߏ?v_7Y.IC9nnn1gþ䓆-v47m@ܵLEmNDDDOrÆ Gөhx{Nru-BS!)bj.u$ry(xUIx]OebOΤ$cxݿqJ罢v{ͻ,Y2kVGt-[ |dg9{WE}zxȑU{Xm8|}E0aA"._pr*S;7Ĉ{GkWogFS  /<H/' zub;æMnfTViGF?v.FPzO]ZSGJÏ/B)_r40ٺ;͚D^[w4Xi5 pťK @FF?w֌0K7diu%&jUxUΝB\1B.Nl*󙓲*:M 4k׺KC.V=:tBf<`rs!CB[-//' qZccC''jiq=zìYQ2F7[*(/(41^{<r!;`ԭggGbFlVey*Z*TX;:zts]NNh@d:O>8uj[)3pڥUYǎmf}1qbkx{;V-9}6+WtZ)2> 3ԃ<<3'CG ZU x/V-J9@.6u@b-'eSkv5)R9=X>}gΜA~A>:Ú5kMxLnezAt6?ħjoMmNDDT3Ն1'%%I>|8I2Yfͽ,jR,5cyys?dzǏgysՐ?~M[b;ۜ!⏪]M*XI3iR3f3M<-S ]KoDDD,a+pw"#0ߏp~/DDDr` X?""""""bGDDDDDD,#""""""DDDDDD⏈#""""""DDDDDD⏈X?""""""ş Q1aV'**\bٲebr T*ѳgOjxǡT*Ѿ}{:tH=!""ܫFYYY3gnܸQgϞ5EF%,!!ܴ7|HMMEΝ'ob ;b+QkIZ#0zhܺu ]\M4B@hh(vmjKKKC=T*JU6mk׶ݻ#55 ĉ#eZ^^^HHH@:uŋ#>>5+̤d 6 <<{"66۷/6nXn>>u"""p%Sb,J> AQQooo:˜*c݈/Rn{FDDbcc{a޼yBigcWvڅ Y<Ȁۇf͚i-?}$%%I>|8I2\###;vDXXc/A?Ҫm>>>'''}邫c1n8ms5kаaCb3ڴi:uTaŘ2e ^}UI7駟"''_|Felm["""""z8H.t2djtc>01I=s˖-7IM"99wAϞ=gwׯx7mNLÇ6Z1l0(J <u222$o["""""zĊN:aŊjhܸJٳ'fΜ<ǣ[nw7jRc*}7N㿊37^اO\wŋchݺx},\Xx1ZlijMbɒ%GBB7onu˖-CNNf̘gggܼymKDDDDDXG߇FE>39r={6MvWA͛7f#EUdѣ@afׯ_xGO? .Ă $-~oqqqV;w.ШQ#L6 7pA _jEU IZv/7|3~ֳSK.Q?""""" """""{}+U:nW\ATTJ%ziWJB^^z!;;Ԧnnnhܸ16o\ejL+Ξ=[Uf5t7|HMMEΝ'hb #22׮]C.]h"(J\zsŏ?XBBl"""""޽;RSS!N87n ** ĕ+W,ηcn...h޼9njWpeg>>Xx1___(-- =zRDTT]?^a޽틍7'//ӦM3~a 8j€KHHСCQvm+W^x饗pa~J/33>>>ooodddXo׮]СRcprr2b| &___z̙5k֠aÆ3hӦMԩ<==g₂@~~>JÇ;bccqASq֯_ taaaV {"""""z?___dff A___K?~}`ʕs/^Çuv3ׯV}bbbtRdggcΜ9f(wk%K[lrrr0c 8;;͛N:aŊjhܸ1t:?%DDDDDR7o<4oga߾}S>?|3gD=HhӦ]^^e}g8r1{lL67zh"00yyyfbׯKEJe|wo>СC8q-66ϟGݺu}v̞=Rc;w.ШQ#L6 7|Gx#F`ѢE=>}gΜA~A>:Ú5k{,-- -ZGPHPcHDDDDD֩j@RR>aaaÇБ$(\ +wUf$"""""jg=*G.]zOǣ~DDDDD,9"DDDDDDՏ}#""""""DDDDDD⏈X?""""""bGDDDDDD,?""""""bGDDDDDDz'i֦kչ1vE<5j;Uq;ոQԂ 0x`&) ytSO=@Rr"""""V~(ĠvX|yϥ_GGG6WѣGѡCrI^-Oj|Wv˗/7{9I9F\\,M۶m;wڌoРApwwǔ)Sj֬Yz:niϟGFl&F OOOIi[<[+5/;v@ ͛7֭[|~/9s> k֬zÆ qt|Ǐǹs.:^k\^}-giy⳵|Kc3#88s΅NC+_پh4E jQ2e|PpppNi j(V+;RƌEy0aFi5ϭZʕ+ѢE 46A+< fھ!%/ pttV5˽q:^""""Uu߶m۰c 3g`Ϟ=_iZڵqQoժ~ghr?rrrBZZ-R|eU46:}v66w\ؿ?z,M ۱c،ϒZ`nݺI&!66fgytX&%/Hͳ=%$$v^7Aغu+lbs>ke1HDDDD5FŐz Ѣ5k/_.(N4IT*Efyyy˖-3wE\.OEȑ#bELff\fY#Fʭt|g-l:D///q4Jmsb۷oYSD[Ν;'v~`ilra+/ )yWj^v%6mT4mժUح[7QYԩSb-D""""]vIg-kP i\lުhi5 "%5щ'н{wKӫW/;z_RO٣LKU#(;/ @T?NDDDDD5⯦Pl|2^UgmU4U_G16}'xb4nܘJDDDDT-z`nh""""bGdMSjAy/XQ)bbbPvm,_l7LG OOO{4mǎhРl^7g\ f(e ;Lbu[n|||УGYKPPΟ?﷼Ի} 6ƍgVܼyvA%K0p@8;;[]^pp0Ν NCE".....4hRRRL.{u{c5߹s'\Ye˖1czqXKAAϝLn,⯰(2e [j888@A OtvvF(P(hzR,^BZpri 8~8w2""""⏧}ݶmۆ;v 000w\ؿ?zY1U@l߾iZHHv^o*v8zY'''[n]4h[b˖-R^v܉={r""""j#c ^-޽{1|QO駟{Y1]}9r$Ν;QMGF:4mhԨ X^ؾKuX/+vڙim,Y/EDDDDՆ}};g=N8ݻ#== """V}ݳJ1788,*1ϣ\US= yBDDDD5} ppp@@@ٳ*grl-o޼yfZ 4@ڵ_s """""Mcݺu4iin~yx o8s ;fsyRH%#[ 6LVm18REtYЕ-AA)S~߱c4hl޼n:G6#G'J"""""z8?A>\UƌcvĒ%KP~}L>견=z4/)L;+6mڄ &&O~ oiY(ܼyאQ_Q ''?{U=\B#EAE`*'5,5_=)]4)<|VZwxQOM@񂊩\* L=zgZ{>{|f O&`xxlcSݺu ݺu.jb Ġl kcP*V-4 J%JJJ Jq=BVY?"""""̟J‚ uV;vL~mڴ#Gpm>X_lwjb={ ))l=}2IDDDDDdMVݻwGddd^~'Ǜm|M} 3ϟ/پԯ[9j(kUUU/Ƅ pEq9ѣئT="""""{-|X>} B^^ADDDDDfYs'oޔBJ9s0DDDDDԤ5!.$""""ņ! """""bGDDDDDDL1#""""""&DDDDDD䏈?""""""&DDDDDD䏈?""""""bGDDDDDDLף}\ܩ=:F9cOJJ܅  _+ֈIsSSѾz1cRоzssNZol|P( P6YyΉ&7\RMo͜3\)CMJ^ ܹӧwjݸ v 8xڢrX?\Mŷ9z9e v:):.]d5vRqk.rs+FeKbLV'NĹsE]vuGff!ngO}ƥ?^ӏ//%f)\_gӦ+P(A>r9F7ks"""j:/tu>6VQ[lGF-DDAp&J2~V+`{+zڂtTUiQmc^} BNk"!>={nAR5tQkU!:0_-;vudɹZo\]t fϯ]+LJ-TKKs7_[ ??WL:txDv;=Nc"e+?_iӎ}z5*.vv Lt7nޥɓPS9x0DZvmmC1ڒ=QXX;Z8qltCs[>`_O=u6Ua έĩS5عӥV-uTꏿ~>aa))СhRd (wуǪoU*ݛkAii5Lսٳ~:mСIu]Pb>j;>X*qZ@U IDATQQQUtKZV5(+V\DyF\f؎$'n#8JHx YY#3g^4;YYY͛^YFvL f\?c8#&ݐcj퓐psե{::.o,KۗR #pʼnV[1/E{ mom Ĥ! ՠK[ 眈~ơC3'sdeK%m-/!..S S+}[#i?:!:"_|)nowd6 ǟw}ᅶ`8wYa|'>>VxZc7/ƏO98`,^1kq˥>|SJ/ۑ)+&ȈRrȉ n˥,˺ڹ |P>kӧ߽JX֬V W{lP źr95/V޽=TڢS'7,]O[v9ct)AbyLL0Zp ZH1xcXZVNv\7O`'66_􁷷J[_;?],mpa(} xe?Ljq2f*nq5)C=ѩl^?nvLƥ!f쎖-pssY!sg11׆Ǵ5#'!!{8559iR {5l_EDDi]V;Gm0cFweiR5 ]xgre3QQXR??6XÆnSOىKk%xK>s"""j^"ϣ{`͌Rs<09q&?4b/23 ~ķ99'""j%%KOOOHH`ر8'oS%4AoS{q5ݐYB@ff!ݘ܃~l4>W==%K_ll7yN;sOO7sNDD䏉 CVv\4OȆ! """""bGDDDDDDL1#""""""&DDDDDD䏈?""""""&DDDDDD䏈?""""""bGDDDDDDL&  N ggz sf͚:}:y$BCCPddde:thjx7ڵeDDDDDDM⫯µk{ *`u7&MǏ>#eˠR믿">>?Cˈ,N6l؀;BT"88)))bYnn. Jpٿ[n4hrrrP(piqLhժ<==rJ$$$^^^39}ѫW:۳ꫯΝ;'%''1c?<֭[2C3͝["""""zȒh$&&3gDLLX;GGhh(fΜYg@3y{PZZ \xK.ɓqdggcɒ%+O$ϔ bҤIuaŊ}6k-[8{,FYCy{{cӦM+'NeₘܹtӧO+"##QTT3gW$B?))))hݺ5? M Z<:u x0yFYCm۶ ӧBBBpZ  {: pppjVg}0eɲ7x| cĉꊱc1118zhˬqn!K|rwѣ2ooo3g}`r$wgRRz%뿓kJQFرcm۶(--kt;w d["""""zȒ}bݺuh4GMMX6d̟?HHH)ZF!Or}~'1 VBEEhqGիW_}U 6ؚ5kPVVy["""""zȒٳgc֬Ypqqql2O?EZZpB5S-jPe}2ǒ%KV7`bl̢㘘dggGž}j}oev!..["""""sPD Ο? ͛73*M 77;w-(dh3+))'$$0vXOK vMww×_~@Q5w/evjMz<1krLD}1-ROSݧYNN

#eˠR믿">>?Cˈ_ ( >}׮]Cxx8\]]WJۿ?v '''bϞ=uθ=F;ѪU+xzzbʕHHH7j%fg!!!jt\HLLDii)fΜJ1uTM&1=1j(\|jHa̙_Uo>L>...Ν;%ΟnuӦM… -[II N:l ?ĉ-ncǎ+bbbpFɵm6O> 1Cyyy͛b"%YW^;#߷Az{{Dͫ'wgRRz%뿓kJQFرcm۶(--kt;w@˗/G^^}]="""")S!xܾ} ,%SNܹsVq%:h^C 2Gyy9jאeI UPQQDZ#F`(..W_}U{ -3fa޼ypttD~~>o߾Xn4 QSSW Ô-ZHLd>S:tZ‰'gI֛?>^z!,, oufQ kO?iii… wcɒ%Po|rp6xfqLL 裏b߾}72cAvٳ1k,`ܸqXl_%DDDDDaQQY=CyfFsVP4{&Q]%%%tƎipR:޾a#z_2DDDDDt2;z&=g59&"DDDDDD}1#""""""&DDDDDD䏈?""""""bGDDDDDDL?""""""bGDDDDDD{P($,9iӠT*ƴ!w_Sqn ׬g}_ү_?/O dp/_K.=0\t)FWT5 K.e NE6xǚE_ݻwc5$wߦ8ؽ{7G"""") 1Xvm7ņV~z>y$z {{{Y ې꟩/|||k.LW^+f̘Qoj5/_nR䴧P(0rZ}xMFv^/2ahB2M}Yv-j5^|z2W*.r[z?3aQQY=Cyfoo2e P(cz.^XܸMm{1~xp_Igr˗ #GDVVq( 8pիn߾mv|ի~7}6g*REϦ+ETJ0՗UVᥗ^cl}m߾=QSSH{Rא9st̓V+Ux+++ DDDDD@zz}BBBcǎt8)`gooYw777TVVLMqM899՟Jq7>T{'JjF=XI1c/^ؠFR6՞q_ljRatdME;'w(-- P*'RHuT*QRRRRLܹsLGDDDD$;{ѦM19<<(Yu6LEw䎣M6طoXg|~={ ))I2dw#"""{'bX! Px̷%@<<c&""""j & 666s6szkoѢEmܸ_+.V["66V&B?햶תo`˖-8REGGךj*ns5ٖYI&رcQɓ'7v܉S}XoؖT=G~~>CFDDDDD˿^$~eee ~'?bNinݺnݺjXbbbbPTTd5|n1(JTWWCJ%%%JR׸BVzh[=י?J `֭8vڴi#Gȑ#5۞;}Y믿ׯ#88jM+>Сj${ARRzdȚ}w:ߡL/O7ggg̟?_}_5sԨQh׮ /^ &ŋr?ѣGMzDDDDDD}[:}4 <̲O; 3m* sa0I1kB\IDDDDD C@DDDDD䏈?""""""bGDDDDDDL1#""""""&DDDDDDL1#""""""&DDDDDD䏈?""""""3'ۯG޹S#{LurrܹSdq+55ۯǘ1)3&ۯGjj=m5e'ʡPB(lYCI5ӿd'K͝{ 0}z7L 0oީfߍ ,l7!:JKe߾̙' !!?!:rr*-ѧVoqyybyQQbbǷg-L=眈/IIv_'c9tM;W]ѵ;kWwdf"-&zT߷N>|ϞkRb잲;kU^Qa3re9gc˖_o͂+^z67Yџ>/]k?=@mavddBDo+$oh,8޽W-KGUVluږ:՗0`.taa;f7rbHOϞ[t O?;oԩGɉÇc]y#&O1f܆8Ik7{KRL];U?KMxHJfmU.a$t ;iU_p0&w"(hRеfK}w 6;3's846,..WGϯ]~ GRҳbw 88`ʧu3.l2xl|J7+K2xxյVR5̚&>v~xNNj+/R/-FLQTVm+\1yrcsT\ζco46Nr#8lY֭ )Glq+?^OzӦ3Jc|c1NpLĉ n&+=]!?>uV_'RԲYE7aoo_ NUݻ;HM%zX}\8IwԨ2|ԩHJ{{{`Rꎗoc6Ո{\br8;+0z:uAZP] :{|555nn Y眈^5;66 |3~/Czu 99֛GcSKҾoqp0WE}V+`@dfǴi]4^ልfq;dMaGq3N}}/6+23;YtS׊}΍ҥO~kC?Ʋ2 vuDZc/ 3s8?q_qbO$n|m}u"17*Y #FÕ+e**j0c ~u;tXqRj|Eq~IdT^DEWT|/\%~{BQ^^+J`0…NTXpC9sPZꎹsuuF=,yyrs?@>(.^WqCO>DDDG1r?1Wuo~ }z!+km{Ƣ/\}wJeQ~Ԣ2aB0zt0ϒv' IDAT9{ƌZ}vMwpvOU[b9qatGܝuv3gZ(J=t+_t';שuuTYubI&+~eTV #fN]~e8uw0^z̚=hW^񇧧 KwPAJѾ}1EEB}qB $n3gt )JѺ:떆>ip.i%8~FLs"""zxYSΏ?TV@RJQuKon iB&m*dl0{ް8Ƃtq3KӐX39ۗ{q\}л~lj*?dKb\߲O77{Vaǎ"*j/N;^_9Lzǜ|_PBx%:qR!99@v.شixͭ_-[:YO=u6Ua έĩS5عӥV-uTMM铿TCڣeK5PܭGDDDd=5Sto@֮3 jWiϞktC&թwB{u`ժlkĥunV?\DEE V-kZՠ8Xqqa;֊4f,\_{LU'$o96IHx YY#ę3g^ݞ%m'_;?`^3>Ǐ<`O yX#]鯣kݺ_e;^d^mBF-xyf &BϞ[p [w=LW*J%ێ8p'Nx@qЍiJܾ ,ZcPn{۶6hGhn'&ip]BV>DDD!!j:9s20gNZT1uQqۀ>۷5b"5tBt|Ef{x5Iu2f`B[q0IZΰaV~ESqo#Tvu6ztJӅ_e-ǭZ9 ,n3Zwذ2T:z6MROWfMZ k+U_e+U󳁽=`]0l>w@u B%r9="ϣ{`͌Ƴi8q~t. *j/23 ~ķ99'"")))icǎt8)`goJ5YfJS&sͩ/j!3B1 #;l٢iV}9'"""=ל?all7yN;/{ߟ~zo̘s"""2>)k.a81#""""""&DDDDDD䏈?""""""bGDDDDDDL?""""""bGDDDDDDL1#"""""0S(A0uT8;;׻OxxxﷳgϢ_~PT޽;=*YիJ!CPRR"bСc>QL W_ڵkֽpXfcW_EAAz-L8Q[o0',}GmTDDDDDD4۰a:vR`eYoEtt40rHdggKKMMELL 0l0رC,KNNFվss爈Db̙y ̙3&o޽b`qJKK/bҥf;ebB7nUUUpttDAAj5䄚zYYY֭[TChh(bbbbѢEj&gi>7wc";v,I;{{3۶mCAA郐a24|_H{g/ """"f~ԏxfV^ݤ=0"""""eDDDDDDL j=sK?M'w{ٳׯT*wGJֻz*áR0dȐZ_LMMСC%$}""""""'.\j,;v,^}U୷ĉ%[ CNN|I|bG}FODDDDDdoРAɁBӧq5aaazdk׮prrB`` cQϒi4#11Z'V\x{{ b;BCCk'ORq؈}|'LA߆Rĕ+WdےMFbb"JKK1sLj'""""zɾɻ}||ooo\~!!!vZz(ɽYa=7n܀vAAj5䄚'ׯ#44/_nt𲲲["((Ns=P?ĢEjM5|aÆaҤIh۶-t""""}͛7 P(((޽{NNNVb_ xyyAP>vff&""" W_}͛7m۶hٲOkoζmPPP>} $$+!֠ 7oA///z y䠸 %%% Re999BBB@@@222p- 2-[_;wN˗#//.Fͫ_j5~%>>oƂ 0p@zԩ0w\j\tɢPC Q^^Z5O L6 SL71 p-\#F_5kPVVy}bݺuh4?-Z@~CUV8q>3zѫW/7@n,a{ 駟"-- Xp!ޚ5k0eʔZ3_4-4ibbbЦMcܹb̢,ڵC\\gƬYqaٲeډbꗛΝ;7VO!""""}{/d ٱ0*YիW78GDDDDDLO""""""&DDDDDD䏈?""""""bGDDDDDDL1#"""""bGDDDDDDLaO 6Sۛڽ<5ږY/~5sR߸5DDDDDAFĪtR5'_I ݻwcM\r !1M#ၵkyCm8** jׯ7۞'Owްа jOng|kBV_|yveщWP`„ hѢx e˖fgg]vfϹ~ZMגf%1Z";rHbƌ~֭֓;^""""f%bX! Px̷%@ؾ}}v}6rz:tSnܦ^z K.+?L\Rc_ؾ}e1sPPp!==]ԩEj*RRAGGGlL֭[<HmkLLΞ=+8;;[Ν; ۶mu\q&""""%,8`vؽ{7> VkɉKb*bϞ=HJJ8ΖĀ辰dgdd!]VAT*K/Z) k֬{{{ BZZгgONr٥q uk?LϘ#11Qpww^E>''' BaLm&TZh!4ۿ/ O>s#T*!**qu '5uȉƍZ- 8PP(ٳBΝ.>f|ˆ_skOƠA'{Cb:t(?Q"""""O=־cL* s̱h?KNϒ3%z2fɣ5c)w)_~rJ """"j26 YŨQtRh""""bG$1)7l_ ݻyB5dkĈڵkkmWX||„ hѢiڵPxm/|||~oN߮Bu 㾘׸_ #G+f̘aRۺu+<==1x`LټiOz8۷LR+aG^oAP`ժUx饗h#>>555 䄑#G"++K V[+O6mo>?ujb8yd+>7՗0>T|}}g$%%;8pC EDDDDMʎ! s-[T,Y0qD<3xg,nkΜ95j/n[x1&L/Bqmĉݻ7mFBvPUۻ(}߁fq@& ^/%o(r}P^^TjTVVO>_|9s&!BoQ]]ͻQlzH$(..ƨQ`ggQjJ⃴ ={bj[lAtt4oXt)d2 !Zn2㳱AQQAL&P"""""jgQ VΝ;gynnnFsttİa-o&/^ ___Mnj >s8z(K.FwQTWWJDDDDD-B?W^5jϟmK.EXX &&h{[oɟ'akk+WWEEgϞkbĉ[ Xd 1p@~ˆEhmDDDDDDd\cn`8}L?""""""bGDDDDDDL1#""""""&DDDDDD䏈1#""""""&DDDDDD䏈?""""""bGDDDDDDt׹ntoEERєkӧйnǟDλqtV}EESh5D$1眈'5lCn֭x'Ӯ|Wпok"7Ê2D?#GQYY~{}{[@e|?;T*9cB?k'7fljǷyݞ-~Ή0o9<5ۓrC\=ѣ#GG$'ŇxyeEsYŋ7|Ŋ+عW2l \[5& #-vl1DDDDd>}޾:?ZӶoW^9/= <;1nXjҞpP ݺ}9sΊ3%5%$dcР̜ovj݆8;vLS1v-l&IDAT9M<}㏿G}qQ~"wCifbhE}@nm2Ϟ}++ A8thi;5`9zώKw08^}MMU#$m>C_tZZgVwBa"ܽ;_|Q] `mlT !$X\$#G>:6bD1,-PP -|pr%MU!:,5H<<1sfwc}EEU~G.LnqdڏktIF}}JnQMRk@zz);76 HJz++  paNiڵ|x8}:Щ1-XA0~/qb\X]#9WaΜR:++W!NTaƌR|`*DEU#2g // \TUgh^9Bu&LLTW/K88HLs""""jfiWYjܙfWҥ0,X檇ve}@XYYĉ ?ε m9s|qʫYc7~Z@d7_ܹ=4|%c„.HN~4cq2Ԍcj?Ʈ4{J ^c}Ͼ}W^f˃pt…-F>wM.*+G{)e7&N<^PO)?x>? {x Mw=:kի57<}Z%^Ȓ%r9brM? Bf ϫPRk.U=5<0`9'"""5h_jec;bPGСϡ_?gߛw7 o ZdX <RfE!&ONcɔ1cעih_V6\z"t\+C~P%KhZ7ߔ չzU@Κ%sfkhE$i'p|$:DDDD4i5˫!Y[)Q*5[lmAAyc15X5ӼoXjŒt1dOC1xoۧ*qPTUv.Ph۹WUaګ jMiHĹ ~N7 >[kVmnT[o<=Z&ʪmfaa˖(-UkXq2%nƮ~]vŧ檪uVڐy|99͞;O?i|mԨ#IIpv֬Uٿ`ذW_|e2` ?n 4q<~8hiri9nWW _* AAR1)1cnaRqkR{AiʜQӐ_2?{2*F^M…<=Arrv~nDs""""6܈x rR++. DHNCrr 99L ~(ž}5Ƙ*9Q3bGO̼y=1qiXqE<{&w hq?$眈Y_K7DDDDԌ,""""""&DDDDDD䏈?""""""bGDDDDDDL1#"""""bGDDDDDDL1#""""""&DDDDDD䏈Qs$ ̙[[z s~s_K/APW^8{z/_F@@$޽{ B@pp0 ŲB"?ӧO秓O+//k֬AZZZuoܸ!E*"":rrr0}tDFF7qDL:٘2e &M$M>HOONj/>H,ǝ;w AO'=oϞ=ڵ+d2|}}qI,##CB@PP߳gO@6m3x`C" %%E\STpttD||<ڵk'''!66...pvvF||Yc2_|(r;n[/55:lmm1a\vM,;}4P8p@'kݺu㨽Z9""""""&&B||<`DGGegF޽,XPJޑ#GzqT"$%%͛ظq#fΜW֭[ذaNI<^8?zb˖-())A||֚ɚL&ݻwx`oo ofO9"""""?6IH((+/~ػwފ ԩSST899!;;s玘h9<*++acc(JTVVB.1=T`S~ KɁΜ9///#h|Xnj5୷Bxx8z쉅 طo~olS>!抈 !2}GNN ???:uJg@ΐH$pqqAFFF]T*DƔM`ڴiXt)l2Y{;ڶm+^_/"pm46GDDDDDfr͛7#33o6ƍ'PYIyyy_c2ugzz:7!!&LB@xx8Ν;'yzz")) F۶mŲ2J>***999&13I~k.T*tI^ Xr%JKKA=֠J%233 SǤMk3w\̚5hMmۆ2[, ={8=Z,ӧn݊b|'FcccŊAVVVsDDDDDDLLxb,\vvv8q"6m$}Ǹx"\\\zj,Zn:!{L;vYtVwZ_bÆ P*ϱyflԩ+JKK|rV^v!%%V28o;vĢEЩSz爈L 闑nݺ=$L0oCDDDDDOf ;>c E6Gn[ھ}폫~DDDDD$L`,n$"""""bHPPP3P{ݻ ( | $j[CNpAkXʈɟInܸѨoK.:}t"==/">#lĉ:u*1eL4I,۴i ~7૯۾6mkHOOD"AJJ {{{޽{z;v =z\.7>lj]STpttD||<ڵk'''!66...pvvF||NFF B~,ӧ bYjj*^ub„ vXǣM69r$vڥ}cmXYMWPk߳gv L___|'''RDNNz'N?!=1X[[cѾ...x~׬Y{m۶iӦaҥ(..Ʋe)#""ٳgo c}+3~~~8u㈈ JC1tvv[ofϞt4E @gOOOO$%%!77h۶X &@P <<ΝQTT@U*EcmXYmP'q͛~ƍo )S*KLL JJJj* 4Ho=oooxyy˗/R۷`*88+WDii)bccu[69OHHك\a: ضmPVVxx{{eGQPP5k|,6m;v+V î]RЩS'q,=BBG ]<܅0xAAHKK   KLL; >CV4Y`DC ^^^)5 Ϲ ]<o7|nݺ=#(Levy5 _q;"""""j2gQVoD'QO""""""&DDDDDD䏈?""""""bGDDDDDDL1#"""""bGDDDDDDL)#m% As 㵏55s_1~x$$$17)=˶nj'&#7nDxx8?aDDDDD"JKXIv5)OxWz5}hJgV'H0zhi;w@|Rݻwmvrehr-نjϔ[бcG1W*ؼy~hݺuIsksN(JkU\Dc7cǎ ۷j9KDDDDD$$tpuy?ݻ`b?f͚[n|Lfٳg͛:۬y&M¤I`eeUoS\}מ#ɐLf,;$ mۆ/wj:wTWWcԨQf}O"pqqA>}PRR/6o \c"55ݻwNJ+V~3z4 >7""/^\&ʼ䯢 LMlmmCV=IN~gh|`Ϟ=֭sϟףD"ZwL߹EEE2'PUUZ KKKT*d2KDDDDD͗#GرcpuuׯԩSzuA<֦M\|h>>>غu+T*U6kkkdddOaÆFO?jcйѣ?hҥ N8Z- ơC3tLZi&>}6l̘1/^ީSz TUUMAM0yd̘1* Wxx8:vJ^jO6m"""0m41Đ!C0d_\}ߡK,Axx8h-Ccɸy&A Xlx [lS_=}]&Q3kgK~dÓz)))g9sf ;APUY=}]"""""gDҕXY[ܼ66аat䉑#n5Qt7d2zZ^npBζqBP;n^@ Ɔ "؈\DvDGgaӦ4! \\#OZV,UBDn9u+q:xQ:l <,NPG_}=U>bacdYsbcs^ϛ5T`OW31fI<8vlqCjj"DD6wD^,\l~[QOx1{$ॗ!jC=*z=aB mLPssHN.޽ ذ!j$%E  X^#il"DDbwutAP;>3{{w'<I uЪ+k%K:kWr#ȑ$l"DD֠ZN7.?tKƍ'!#!!ԯ{~ {غ5HtٳCuJ&sҐ*F^"xy9sgO [6C7ќ^%n$+qD zLA*U-[bذ:BSц4hPC̛~T`z[-[СP*c߾y3puCP;^{`>U2И,nX_=ҥGHKPW5 LSɍOCtt=*[WbU--jz[]SmhmգGE ݯvrw'`xܹ Hfb42S IBRR!llh&4r%WaC @uҥG3CLiYڙ5EDd`+u1b?^~9{Dt%Z eO\CC٣-{KT, >"NHYgɍTEP;[]b yS Brr! qd ~5+WvCÆ7שJZBnhUoGG2@,'Spd FjK#9}-uDHK+ŒqBv GX&,1cT? Νt3gRqL*i -w:ZՖnCKleP(2,MI.]zɓ[cv͕b:w-/&F7)IǵkH$ ̘qw`yyR̚uA+3f5kkX.Fdd…tw~½[" XIBBz2Vuv\,K/׻OTT&^~˷s'G:iddkMlZʟ>zݩEmo]oX/]֎wzM)k>LQn~V#M2X0ǖ;Mu+[E[X㶺:hXtLMkčXG^}׮eYͶD$b,=o3'D )P M렀96lI2:w. ˖EqBhn[zO\CHpA ˗Gn]OoP#b>,=J>o^Viۢo__xy#'GӧSwא"V̜yvS/9>f]@vD=g1eJkl B,|5 X< _}յmhNص7lazj;;[͛yڣ{"22˖];*kڧ9OU0w?j殏Ŝ9Fb ԃwoa߾y* >-Z5XUn GϞ `g'Dtt/R?k5COʌmQhCKm23fM> _K_^ٳi8緇v츃ղ0#Vq\X=PݫaZڵퟕ%ZelGJ8>˖]DX۷qc_oD4u(X-[߼z5S踋;wz^FG1tdeIG<ϞM3xx;u$S'O_ҫWÐ!Gqn`{1||+݆ezCflgzWhzys |ɪEEr:_4{j[/""2←Z;۷?3ܶVPgFg2?| ˗w5XNl|}Ӻt¦M1thiH8tn kV[ -(FVX:1119Z#gϦyXzLEHH=|a{|U6 Dn_֚ɒh"^ӰalNN~4`/:vGY=\"qf9uYui ÆIܭ!]kUoBkESxm<*Q;hNZ?6Z׬(_%V -Q:XLںZ;-v5wpgSOaPG7|VZA$)7h i&OoѼLj-q[m67ZkHˮ|X[J˚*_̙رMވ\LN?,I25ի DDlD"! O+R3K^wž=hr%*e{sg/UjCK=)N||{Z5{t wKDVbfg<7^}iϩIUC&.F/7nrJJUQF5PvkwNXX$6m~}}̚N降mN~~NeQ\m}R߇9< ul([7Cwe.|坾^޵um kVW ao$Vhy614srvڵ=ubOO{^CfInGsSDDO\Y|K1w_y 8:tSdozTlJmhN [ށT9E~ ?xaaNNFaOby>k[ݫ/FjL-SQՖnCKn-F㡪򨖚ЬY]ܹK/ulX?,(""dX@PD[ X^P R5۶1xKջH~g>%^ K2ĕ]M5n-݆F?M~)FNҹ+%ڿ"a焜۝cGniwRMvЪGDrOV&ŽwշɔX< kz+JZ>rbcsѪNtzر)e*{T͝WT[XcߪlmtU1m9(?o պT)g^Cko޼Nk""&O#Mn'駑V~kk-Z^9|rs8i{7>U&=U}*/ iޯ!=:R%HJ*k,ҊiXizNm k[nڶN2Ӧ׺Vr +VtӺ63%E봭ڿXׯzf|mdͧYP֣3 e522!*\͛1jI BCif[vO?ĸq̙TJذ!ӧX,ǃ@nV8cP˗3t'gzWOPX(ٳi;s iXK*{-њ57QP Çb>ejff1^}$NBn2DEebsػ66N3!feI0zI;<)b9ΞMØ1'"ֆm[5цm]*J6Q +Wvz&ٳiZؚmuS7.^LGQRcƜ *,AAV[&M\p@=JիHO/BQ";{z5[S[.r -*n[w}栰P//tXK9EEr_h;O2c~={رd>˗3QpuC˖WWݻb}Ԇm={wvli.]0n\ v2k-jodֶmrL|V.]0yrkˮq#[\I1yYOKsP(߆_?_lv7of#/O {na1lXcBWޣ~MآR\$FNؽ{7[=UI< /DDDO\@d7 L0#[y1$""""""&DDDDDDDh1$"""""Ƈ*0>/DDD G4&1$""""""&DDDDDDDL4&1$""""""bIDDDDDDL4&1$""""""bIDDDDDDL4&M""""""bIDDDDDDL4hM""""""bIDDDDDDL4hM""""""bIDDDDDDDhQm`[5kD"!ж iF`TxӦǏB@U4ٳiس'Wf"%b6qDǎ5 t2yBqH"^DFF14@PRv-Oqq'Oldfn֭]1p`C?sX}Q;wwo޽|H ),B5DHH'm>@DDDDdDT":N=֭^pv6ߪ"#3'8vVdJ̙syP?su9mZZ]xuآsgOY&R's^dJ̙s]paGl {{ԭ+ {wj ͕bٲVgͺN2 ş>c" 6Anع4LMcѢbR nUwɵpjKG4q 3'O`X\"95s믫F9x6nCLLU+WF }ƒQNiӕ@6nz=qbs>]Suj |HǏ'#..gϦҥGF4#Gp<#lN5wgz@_~ÉHHȇT)mioO?To 5z=iuWЯ/U+Epm={cٲ(ٓ"9^Tя#:: :yDDDDDVhj&漎R{D4ѴQ/:#P7ndcKwzokM@D9,\YYɲ|^^ds˖xf I˥Ku8@M! Fѣbɒ+[< zk֏,#uFƏ4KaccsPXJ:vԾ>Ss$P.Wb~+q|jiǎh^8mv xuUy]\D6HH 5cF[;> \JԱE׮^Ç}һFJII)xyHweosF<<쑗'ApsS 5]OLx]4ڜ7*JO/k5z2J?֔/+`8A֭0~{Lv^%e i`O =z7۲C IDAT\}Cw5G.^LW=lXcϽjS#v5knb{˓+(7Ik#6嶁cWK)I:+b>:d2:"El+uг)E]~\Vk8CCisGDDDD֛hVC\H4qV;%6uQTwWr7Uc<='1o^{\ϧ!"^̈́X,̙n=Un֩cN d[WdrgJbZUu"7WU>X^޸ko":: ɅɴO)V(H}w|֩UehH?.Ks#"""""K4S~vvBɟ"姚7p)IT;ݥ;9&oF Gܢ"9OƲeQ\y]h:-]nnt }|,>>NQ'ScORFP5N4ЩIN,۷Umk,E4/OJ۶zwKH$ #UBY:yv5O( i5O>{6]TzdeoSUoV'[Æ5F\ˈyZPppibYKj+ڏhVg3-..W橎~~#w|.&&z.&FH>Ȥ>%wG-^sx/Mr۶Q<}ȐuugϦawѸq#[wxq06P`OK|-SVDDDDDL4+Hο}iWΝSbǎsVΜIEFF1V6ȈSJ.ڦsgU}<(_1޾J2ѫWzO獈-wΗ%'D >>WĪU7 4|,Hٶi:yOUedc(*2|Lď?ިQc"""""&}# #B;vs !/O ū׼9U5_~ iiED>:8x}_\2q~֯'D?͵C᡺Qxpb:e((!""'źu1TK~UWDRoB7)|Y$bcs!(#}1b1ܻ~ vT/C5_<ط>V5"c,_gg :")\ubЯߟZϹ4UUzWY A`VJq];c&}H()7W :wދl <<>rm0cF[|U I,  _6XǓ1{r7bc#Axv:bƌFOTh\zj[rҢE]\wݷ>fμ3С0pَNN_Ǐ*UfgKGpHIwV3gC.^*CU]~\^%"""+7Wudddɟ L0#[ɸ{V-ԔH$rnK4jTli7l…Ѵ 6oݻyppA۶xի: uɓ}>yɑ@TYgèQMФmϞ8xᅬ,ddp_cfrb -[С}9.R=ڵsǐ!0dH#W@J 8嗛[A(,9"wx]vm\4$'"7W xzڣa:޽>16TޕDDDDD֠R#R5 FGTͭpjԩ:EډDDDDT9)ds̛#!8xbcsGaKs8W D1Lkײ+żyላ˅X,ǿKΛJ ~)<@Q\>GGdd^83τ 8k'7QT$ļ r޽q8z4 wA"Q:x`x9j/׬YaMn#1vvBՁ-L"""""biqUqӦim^'6ĉ[=8,FDDDDDDL4&1$""""""bIDDDDDDL4&l""""""*\epD̊&1$""""""&DDDDDDD&1$""""""&DDDDDDDL4&1$""""""&DDDDDDDfc[+o; hM""""""bIDDDDDDL4hM""""""bDP222222222222VS dggdddddddddddD&###########M&L2)I4l24+###cbn!4ر3<<<ã>6sǎ;תU}n#~Xz&L5}!!rƑTh׮P/ڵ3f#33WĄ [}yV7.^/#8=WsȐ|\Tdf!!6l nn:u|}ĤTmaah9j{-g~?O_022rD&G4+}DQ yybРvJ,^,LL?thX=KNh߾>tΞ=g56i>U k"~_2oj"VcÌ$h222>#.kӧ6l[<4i\k'O??Ν-3 )HGVVu{&g] @L&(ЧO4l8L<MpSRQ5x;wDHH7ĉo!== 0r N34oSE_ [n``$>nSoxxGFFF[vaj;ƍ߿}}BpVlOyJTWgOĭ[1&CO~A=ѴiKԯ_7|gV_u޽M43tpxxx[gP\\ >ӧ.yuh.~ @0##G4#yDSsztt47onMC?u qY;!!]8q_֬a޼8q$T׸^:e!<=ؾiغu;c#ѬY3̛1N:B1Μ9l]L***ƕ+W0m,,X0_kÇh 5kyPb׮!1{L^ǟ/͛.-С!So=BBByQ=_ff*<==*޲7TS}u|L̛7n]1ز7$$aҤ)8~Yz?2mBBeO4?N:Bڵ˸?/8 Wúup$&aEFkE~?;)S'xキѡC{۷rlܸ [loAR=>} JaFFhR7E+H{ZQ쌌Oiḣe™3gaa`Ĉa:Yb>L / ҥS`s!,l)>C7l=ƌ9sfsC]q Gkŋh7f3g&:w@tw-K9sbܦG|UY)//'Νѭ[7?pO_v({3ުϲ

OkhP777t? U׿v-ZКkעoh;VΝ0`xc`"7֙OF֧u떸~%ż5^۷ѻwOuj͛D~I2ުؼye2\PP5_ك Wcc#ʕc(,cM4% 84&/pU|?~I1ުO ;;2WD=.X5wfj? wE!10{vv"t ޽i DhhT/УG_4oaɒE9X,YAW^[4q`ժufYo٨ջU駕X+FPPnjy{?_ߦ7Mx56ުO r^A ƽL= SFGIyBC{!)) ))HJJFRRBC{}zW ñ~F|Bnnz6oގ޽[n%GЭ[ v&u6  """"">xDSYCZXsyM&d12222222222dd#L6h2ddddddddddddD&###########M&L2y&###########̧6ѴN##########M&JX}DS8髉h-`ddddddddddHѴ`K`ЅGv9D#Uv®U]}5addddddddd&MhV- <:}yd#O%[hs pK6⍧ .Nw`sg'T48T)d@P&Q„Np0>M&GPC| KzioQc屔~]guiJb(,VTM8sUJ2?I%zTWZ>c)|%5:5۫lU[ԟd++UYPb2t!A%Hjt0)NT F-EL/d L/zW5=1CWKjc"+Gɉj<'Ffcf); 뛼܏ebSl:I=[,ݓg6[=v}$z<]SU^4۫lUVC%x7~< 7KL(?![}OOQe"\NBEUb%. ^.Ĭ_Tm67" ďQ=y=%&4,T5[/"*5x T~q6[ +e~u슢ԩwRg!o[-)q _uXt.8 0TgܝU_.DrjqJux>Xnp+)H4fU3.o$קip{+:j֦,ic~,=ESNK[L=uo%K3و{(8]dLeJy4&f0txPfmʂ뛉X/M%j2꾑l!`vU9BwHvɿJ/.F9)h4% n:]Ϸlzií'w cSYIB4J|do#$ ^\*ECo{R')=uRBfKrsqɺۂ?~6>E^~_~C;@0Z&}2< z3B6S$8SH)2^IiFOͻx -&I!U'K:<JQh<4#TťRħ(u7cux[N9@Och%9JK^mW]I 1dYKMX+dr` O$EvYuw:M8)kBGؤ{믓٬{o~vV^,DJ濤;`j{?6Beҷ,Sʣu`@-2w"]^ied?kmsm|7l-$2`x>8Du^nuT{` CVXUt 4CEWO N/kY9i擤:[=UUzZb)Ϝ%"bD6&4>n6pb \] yg\k|ݷéJe22'suM_ί d=AP(QԎv|<y=Q^aiq*.>u/Yr)ݩ+ax_+߿.K0<w%RA}7xg "ۮ:B\g|J[{#TLUTfc<N8Wy$# IDAT Qm%9ʘ^B(Kiyŀ\(0p5@%F=#D&"nMG;@(^:V MKkD8\ )Fsѫ6?b::&""&O[v426BweJ8/WByT޲L)Oe+S\2}-;{{ ;8OL2k;f; L52+(LuhNW*yls+ bH;XYeJy*^yiWY֊L2TI+8ĮbXyb%Ҕ(,V^Bu}%.V+кvdz<?U"ߺT?T"l?Z""&O$hVs)(QXЫz{'J(O3AC6F_声X<[9 #/LU:Zb۴2կkW'd)XmCGDkn0# 4 @$)V'6.D~k#$Ў[*3(*!.Vb!9,m{W'VyJ X=ZT tS݁2ZVEc^ֻjbMfZ6`q)%71y*1bMAJȀ:6CM~F"%6W @V:8 LkV#Pm\`p:\@oN9tEtiZ_ R%Ī ӆ?^""&L4kgi>0- _+ot/4 6dCdM )I᫱k,.ISǒ{n!F))>K>>{niZeҷ,C1F_nsIFSRto.?ׂ*Szz:} zal`ޖl,؞s&6z9z^I?gb)3mWc]t&'cޖr1>JJۨ- U;Y ԫqn~Е"|;N{/7u%ER$*aLň/dX?D6tk)@YRt%E\ _U}W//c/.163tj䗥7 c_H-e*1el:͔r8A}!ud vt)lI[RkeX%J<~DqN!UW t-(>cUJ+U/Z!s8PC>L)`+:?N^Q۷T{SvSP G;`H!DDL6"1v ݻkE`UGV/Faʡx]{4iB" 762Yc[Xڜe_gbŴTv ׺FkLUؠbzEnRT+ꛛ[N0#[hV 0G =rC[uXwAfcEsk,SXY/z+KJ4 tmΫZ d[;C ]A]`~ɻW FAglTzTUTd2}JCZ\!p;Uvdޡ@ @:Q ?bB2W*Uf5PG}hKO5DDdB@&UPKOje ? .Zn/&5H&8=(@=3Q|{+(DDDT$ɩ;̂OwwP+Sge#_crWY9AdDΣG!j򊧍\yq/7'*|tfcTR-~fl>$ǽ&$wʚ:ހ8-U={(T**}{\dgE33 ]rbNs ޙ FL ɹzP?!͖d^yn""""|)&,aNmh@qa"4Q=N#@h/ũ^zZ/QTk+qkBS2\wkm1~JbZ#-F۹|gl <: ɭ^] &Od0%V%.p=$ɵHy[,WsPJ%vKrB[ m%t^B[8 cxY~Z=@!HxIDDDDDL4kqg0W;'QXy`vAбy+4<>gd_İ=UP+P hhjjZ,n(hE"`@@"{IΖdYLf23l@7ss{;sHpaPLKZ9wS)v"^mFo[g Jys]ZT:~{aHafa vpa({A~KXQ⬰!X<{u ;됧J\:m_s$/Yp&n""""bDeN8 #5 xeķsˆ+.9'S{atL":S>Yu18 /e})-*Fjm5V:*DRsd)LIԞ'NcVI\XjOZuQ8N]p*ԶƓէ.oߵf{W] i9̥^I)M&dFABt(3PRXӵA kL]+J=h[chhY8QѦ!AC-*ҥ#^B߈fB %N*- %զa&rOl& Sc:޽LJ퍲wZ=ӦMDDDDL2/#}R(! aoB Vxeujj[ hwE>=[. [J$8Cp ^ ~lU!CAoeBv<ץf@[l""""jyՊ# X‰Wp>T 5hpoU8Q%o=MWY+P-g/LpXm+P}*^hw-x|P[ pmk r)IRp&9 ӁvkME5",գJۧr """"yMn ] Qڏ#O:f[æM_;_.~2+DDDD$5c+"u 3~m3/Q%"""""&L4 *>XlZ:]ҡQ YgbyJ/sWUh65+ Ui}mrviE4cP$ty, ]H2ۦظ uޣp[>A'""""&_!.~n=ׇ[2a.DDDDDh|Չqh榩ۥ$""""&Tc5UVb(, Bףgu<~܂ JeY;gEL^;脻 Jնhg(Y֮=EPZjB葂31lXF:dĻŽqo~V!?UUv,\XuNয়,ٜ̌ȑx䑞hJNVCo? J{vt)SW":ֿ-UUv{_v۱h.waΜ=S/݈J4z} Iu^Ϝ35ر<>8>pO*ׅ8v`CII%JJ*d#X*;l93 .-,f <lN26_?1w1>50yFws>֯?oAֺpIkSxt c?FBzB=;?+mvk;۷]wurvC6"""M^|Gºua_ 4yq- fDju?}wg|ޅo3[[`6"ounFS8:B1~ؿNZu+ڶs'goukZ;P[@\  Łwb͚ЧOK/kGPϳ` yU9iR=;w߁'W^ S1kٜX|ps>p8ux]78N2︣#v횀;';:{6G_{R bϞ;0gN0%GPRRQ;.ƹ=xp}gO}(Iu8t;{sn= 0xpFDDDL4]vp_gMbb- [ǫѵkHT-;@>vlRtFv?ĉO`ң- u1"StNB)x K(-VBvvhHIAaf3٧0h6cFww_W$$A~~Q>8=ǐ!鈏W3^mǂ$i(1kVo 55O<]oNGSH+#FdǮC۶qWcҤxk8{n{Dz&;vs2cn^?yO/ANNlDDDD%!2YvoϠZ4kDsj4GekO2eJWe&OO;iRg׃Տt>lT|nݒPZzv횀ͮCig3ϑq㲚[듅GM(w6~oFQxޅX.R|=k1 W(<1c:/.h!s;/{~Dul>i>ɓ8yRa/Sx,5_?p!9M@TGd лwj @/ܿ>ljq7+I`\ɕˏ?ַC^h(yvD.]}޽{RP*`c,!A'ԩjGx޷pIk}yޫJ.q܎UO4Y3gjpm 7#''6}I)M"""j.͈c]G5ы)!?^HX,ۉo{.ٗ3SUUt^h(yV]XyuAd rJuX˫Ifc}pI">w;uMHm+ѣ֝RᾗؕF4U*ψ,lF4 'zall|۷?F4"_B}ObUIIFht*S@TS4ũUMh\,6V宧~L:%C&{WN9u,̚=9RESH+$9Pz7!s{t_݉*'Z }ʏX4(7t +zJEBB'rV^իGcƌɨҺ ~8PR>shOώ/+<~{gԠG`/hg_f|64gD]QB?oRu8g$2K=5юC$vnn+ܓ/w݃|d56m:NZy,1Lyy+kJ9w Y,%GYG/AEEޏ1eÆe߰;Өs=:R?癧['x>@JK{9ne f>>Wx\!ӕ+˽9ߧN#P!v &!ZVB~.$zl?={GdyԽ&1ڎJ u=}Yee-xYϚXƁ&7yrgt*OFv <|1NѣfEXNN68K!79sՁ?< N!-M}w_W% ºu'a:w0>wgxΒhQ.=*;Nܹ{ql#2{_:U~Fm?h3ڎ-^CS}9GՁ/,+-5Fܗ?jLr+WKqf KrrT*f} 8\su uQX{7U*;a߹S=WQz%DDDD[v`@n?,_#yj[Q3&L3IZ; Ë//o=thAl]{=gRiӺy-khg_?wgz-- XxbG_unc}x>7FjG@'7=]d6pIRWDq=;?_O#.*=|~FqqK1w^[OG4"""6Mbhd@iiZ|䓣ƩS0өЩSFlӻ{=~z86X u  YYXV|1Y3L!))99ɸN^ ^ ]$OF`=(.g+=v j^Qr'xJ2bh3֭-N<`5w7BЪoYBj򇈈BԨ͞mm 6ZB^ 6YckٔϷ` 5SPR BI,X.?@^F^]ދGee*><'O cAAA}={0kmX z}*6nLDE,YR/ jjRѹkf$""""&kժfH=Wxq{Y:߄hy}}*N/jl/[Q^Ĝ9q|pTW˗je@icX$Agʕ/7lr8xЉq,HI1@`uS6m HL0p ˖N"s@PTpo3'r{Cw-,ān +tIQ4(.v;zԉ)SiF#!=݀ɓ-8pd_o&JHI1`8 JKK ffz-:˓a5Ja(\v鲲N^avԩONr ~k-zwJDDDDD3, F{`,NNΝI0Â-[am[ׂ^dlݚy[ӧ6O)QGO6{Ry].*D*JKCno(kʉ-+5b߽~ىALشɆO>I$bժD&>8*.v`X3$I0 ?,P*'Ǹi\/+>?'Nmرҏ<_ ak|/]}`3 B3f`0<xA-~ 5Dbpb ZvII ^{-mr ΟxXtDZAy7}Mg0{oi<%+**8 FB0p / Yw bR,W!1QNz{ @1z[I ?߂Tbc% j"g Xd &McG%~;'O ,YR| :wh&]ӱxbwrxbh.?ܳ6^8dHmWQ;*a07}ޓ'$+3S~۶%9{7ݤi2Qu%r [olItT |PN 8sƉýP㡇QP`ԩ11#o9BBrwaA*|MZͫA]?p4DsŊA- EfwZgÿwϕ8n-oܹgq%]iia0 uyJm(l4#C`}W2ϙ3N.B.:-O3tx隰z2n݌߮CihQ<-D*|et"/0u: GUh`T s]F 6eG@PvnlD:mFYy'jˁKJau)+Kݼَ q5$~K "$c~mj+""""fhN0gY~~~D<{;Өtz%ɉgRRY)fEhbc:tPz-7}^רk\#2f=}`Nzlx2P^BJc

?SS$e۶}C-[׮`xmZAL/1;m+WJdž64f~0zti`6ɀL}ٗ=wN[Ljغ5 r~ l{X]̮]W_8""""MFr^^DnFΝI3La;m4 V F+x~5%SFe(ߵ ]^WT)D*G4/-Xʆ;t0~ |1$ ݢEF3&Opa<;fw]\ڛI|G""""""bIDDDDDDL4&]PHA?>aPm Տ?:1l11+]c-r1>}L6";ۈ>}L0{hR SغՎ%KQYg:^fδԁetit`LKo׊6t$ bg".kN*nh^fBFmfٳ-~e஻b]}p[چ~X}US᷿բ5klהϷ` 5SPR BI,X.첞3T}*nh^aVjVh> <\ ji5KZ-];355΍Ej]*qA laTWL^c~4U6:]fu|UV:1fIIt: yy&\)%e+NgAJ:\mG*ic@bMX,po "{99Ɛʾmo o] Bɓo>-hFBFUʜAh2$$$H<ق߯À&ɗk›o Of}׭NmFr֭ *?/:_:~.v8pmdeөFiP\@a>?zԉ)S)/=݀ɓ-8poz;4!6VBJYPZߞ ka3gj_w zz9ܹqM֧ց(hݲsD"??_tv^TzѯQ_o{EREa] @ﳝ](֮FSi996l5SMIűcQY'~Xhn+P*}~SdeD۶XNnæM6ѡAJG.N/F0"$x*ԩ3o^b p 2"3&> e}zz1|&O}9w\?>//w֭%Ѯ$6m Y;sgHLԋˮ]vEnw7l\P*wq4(8|9JYz1п] DDDt3avYEs]o,MIEJJ O.$I(y>^J4X{˗ @/&N4h>ج^]^vChz1l)$-PDӕ}wm4~Z+"==m0Ӌ'^d&> u᮷bE}?9#CZrr 1 @/ӗ}g^ -Y⽿K͜|^9^{߯m4ykJZ$ص.&L0Itzq &/{B-+Bm] DDDDE&&L~MQ"s7'Ot1h6ܦyRЉfŹsΨ$i7}hUJz>-/;K/V*Q;mFڧm3**锗+,? kWy'N8C~'N8#J4jyݞ= b͚:a6 eMtbJBjRIaS>زցhh6{4WXԲPdfz7U+j{[ZD6nL -5> uiFz7@e"6#C~^ˇ_*;}zvUb&JP@DDDb &,Ϗhgzrtz}!GnB~p41:(֍`꾣`t$oѧX [8jH׋_C9~QK5*kӧ UoW}l<Յގ+IAXew!'sPʆR"""h.^&L@rr2`ڴix"_{?2oOPﺄmol-[׮Q`x\͛ONh 258h7ɟjf@wՍA$}:"~i};`4mF^j(ǯm[ngVƕ861c4=:s&yvu׭}Čن?.Sȏ Y{;G2ԲMWT&\6:Jzk J/4mJݻ];Ih4zQTT?Iԩ O=U-f!bH<5YZ4ع. Bi|⍆w4ItdeeQQw%?2wNd67}hLTQ;Dl^|E.z0Q > vR@FZEu~Z+Ə7mY=rܧl_]/s53M^}Y}Ǜ>Mnw6e׬ 6Tʓȏ7YN\Qh4B1~YTz[VQU%Ķm6ѹAEq=B-i5Yg/FyCkɒjX{&3giiHHЋ[o5RQNfV!+[};x~@B?m׉%VEfr{wF: fz͛G򗾏7NNb7ogp5636-_^'5HJҋ1cb~ȐZ!m11bbn{De/Vifc[v`@n?,_ŏкIdJvZ6!<sdo/_&[^DnFΝIQpf2ٴiPT\X], WR4jrZ>ˊP*y.7/J4H&Q 0qVٰcGN``Əנ CDDDDfDJlSshQ""""2qD 5s,[Kp3gZZ|VᦛHL$a>Qr'xJ2bh3֭5Y矯B!5:cyVTwKDDDL4):HA5}|6ZB^ 6Ycku|+-4H$CP`$ - ?8ЫWQY?OɓcXPPP>̙c wo68hR3jU;*xgA&DÇxrs"5U]x8tĆ >\ f71f @*f4> cjyh3$ɉ  """&-Uu|XV:1fIIt: yy&\=r7˕JA'ƍ %NBn6ѣNsOڴ1 1Q&,[81ٷBBQý͜c e7ؾ_wfDiG,uk gBUU_R=HL,{6E~KH ad *+ET]+2 h4 <ق2V^F@&hZ Gex t: v8pmdeտ;uRb( (,7?59~+!%ŀq,(-lHoZ^%OIDATl̙Z]Byy2>8>FiS:0ʷ9kpss*|it:gCDDDWn9]VG/Z:]@/T*(֯ E{{wP.Nmkk )vy[]SNѦ$1tЋ?m} j{w( ILԋM.L&!xZz1i];:R0(/wQLЋW^ .:^![ǫSZ9غ;ukIk'Mllb !ȐĈ&s⭷bdP* vϟw F/~+>+}3wnOne^tfp l"7(J~\ 2 @/v4y|C)HiCzѽovمZs4oIc?Qsc4:n9"ޢY$$ӧ@L>]HQ2;V}T+7hz`6/^Lh:|19Yνqjb0STCio&^XQ_3gЋ!HHЋ_Q]VJHO9غ;V>(%_(,]*3->/'wY6_ "'oiw2~ m^b2u6d+-M^e&EJ$t:* d$K Q[+DFѯQl'L4jJ4ťӧOŋa0/ymޗ!޳6^Z7d|iz|iOQ 5; СIOW$Νgkp挼<;[ 9'F}o`.?rwto&Oq5tQ/Z\{Çm9yRD53SSm:j]=m"m0uoUѱ2:ՂSaC"Μqbp{mz6L3b>{v pj+ZL例j%>{6\]Zy\3>৥)ڇ7}Gp~fٲ\{oYm zz&{ 5~*48!6m^ bcP:#NlߋxB۴Qzf]]Μqzmȇ˕=G˓ ~& o7kCZ,Z>'`N^~ي!7WAs鏈pZLee-V(oS^v{d uho4ؼ9O?)?W/#.kquixj^4̘Z |>V LHTHgȐO~]%%ʺ91h 7۱`A|3gr*tٱ߃'&LY6ϞT[YA猣MmuetZ7ZioF{Htsb߾$~<֖R O;4&3Sc5((C]dI3= ԏZOLWQWHfQP׌.q}q♔RJK>E0k:D\:6eexbL0iӦ^h_=e'.|ƶm<[ȯ=kʍ7m\~rB0hu%ơ7}G0i[pH}pDsc5fBѣx;om+u'W"]8B62e˽zTsbV֭I0 %MQT&"""+&uh7DUwEvh~&ΩS1zZBȑ&_?;"j^  olbN6F2Ygw4ItdeeQQw%nFg2@/mj{wƬM3G~$ʼnNa #<y**u ߱:})22Gl*?l"+ 22$q3Jv!ڵR^gV͛'gI$u[P*gO()ovmzh"yuxP⭷JmlsgӋb{Xe"jq>qY"""ZgbbXHNVźuޏ58sFN$jѱ$XBTU #/JK}WG.$$&?~yh^j^dfJ!7}Kh ![E>F zquF1gNnbLc%ǎ9}YDzkĴiq#y|y_14_,lq˗׉}"&F/bؿ.22ǎֆwjkjDvAȏ7ݻaU(~6OM=#JK4ݲsDra-~ulW$5 XQ^5srݼ<sdo/_&[^DnFΝI|R'""""jL&SN6 E%Bz \I9&W[{Ifh:NLS\RDDDDt%M!R9IW,8tȁӫ ޝ'N`*vH l$3&5$7/\,rG/M2`Ѣxx'[99*,\hjٻmN}}5ژIW.9%iӦ|$"""""ɦL&AR('iX6u[b{d6}mDv}`67$ld2$9ӂR-Kҥ 8xЁ3--]+VpMf$&JHJ0d }fly<'O cAAA}={0kmX z}*6nLDE,YR_/lf3**6lHјuq7|':ElK-;GXk0 /_I>|68Є"%{&eKdȕҎK%K /τ{ddea?DF\ ;w&AhYN\w7ݤƆ ;0fÆ!7΂׆q-,㦛K{G nƷڱzuƌ/kеG$rr8tȉг]v^ބ=8p ';e2^wڴi(*.A.jѬ?Tje@icX$Agʕ6Fp-W*7΂t: f[G:q=UhƀD lY]mB!fN1pj_4\N֯c@bc%0nΈϧ߯À&ɗ@k›o OC)7eJ23/}<ق!g][7#JK=ڌd [pU8^< _jг%o{zЎnMN2S'%FҠԥb ffz-:˓݉ceG੧t5JU67W't1B]8u?$޵ʫlvʝ]ݲsD"??_tv^TzѯQ_o{EREa] @ﳝ](֮FSi996l5SMIűcQY'~Xhn+P`;6e^tfEEva2 aMR7Fz1cETT8EESLb^̙Sr[!ZDvش&f!v찋Ν "1Q/az1|GOs}?U7瑴=nc?.;}gg)z.r~ѕĄ+Z)%Zk9 厄DDDDDDTLEWDk-iGu8f84E^CLCDDDDDDTLcZ^^|w90{IENDB`plprofiler-REL4_2_5/doc/images/tpcb-test1-fetch_abalance.png000066400000000000000000001133001465735455400240300ustar00rootroot00000000000000PNG  IHDRMf6bKGD pHYs  tIME;) IDATxy|Ld0*b +B-Fi5Wk -z-˭RKtjiFHhս--SK BȾJpI&D>+/3g{>9s9ϑ -/Q\. ADDDDDDbpFLҢE I"""""(++yõvY""""""&DDDDDD$dL"""""""&DDDDDDd2$""""""LSM&DDDDDDTaYw&DDDDDDdViJiydФIʕ /֬x=14'Fs')(,,|g-f<oػ6T6UXX{~\m/z@&K jrL]r4ckDDT Iɀ!?3;j&ܹ''ux`Ѣ??JKGOԪeTnY8t(Eg[%O+NL3w]Cm:y5U5_~V+}/[F5#|}1 99_}u;5c,[V3gQZ <έ ;iu?_)pa>n-Bj 0}zR`B|e!Ν+Aq1Ф-ƌcZU+U$TQdea˖34Ǐ =Nf:[iM{Z>,~9[\n˖5k.h KO/wp |(:>ܹǵBlrǎBaj]OlxI^TT$8~GG˩:$NWeȐR:WKY<'a͚Z'KM;vcfM/0iAkX.]<̮K0g=qԶyKv׮OȻv+Msq''+RTT#ܹ i͛sHL4?gtQ'?|Y.OFT8u*|Nv]&A;A\\3vzsק,Cmn]!ƏolڥQX훍'KaWb|=[\a9lXn}5ܹ̘b)-nDDT93˞VO>9?kG'2MDE gcdž ( X*܉w}kۗ"8yz_™3/!:_gPLKo)3g^ͽQ~hg믭,ʏ?M̙uk_9ldz9֑IJugn,Y/kǨ9lǎR 8ztݷvhB|Y۴>_~y kVe˺ܹqKx6RdÒ2Y:64ѽ6j;q38Y+RĶm//{{V9]0Yrٳx}h*aܸq|Exݓ)SP^md欏.ڀ:[ QQHOwNVdI2֬)flprr\O*\ڶʕ is%Y>R 0@PVPgBe! |JD-iDDTy{2˾UGxGg=Ҷi6tą C-^3gKRԩSavmnlžgOr4؜r*̍%eG'=z4ʪk鲎?Lǝ;z-**>-˨㚩m#+ѭr:;?2#ҏjɑ|v RoŋAip/}ͬo̕2/\Qj.NoTPP2D{\NRSLF>OmO?[w|DE핈`z,YGڶ(U*{ܴ;0VVn\\[m5 Sudn,Y/k1MVzXM1muj5,և%eXcu\3mhD׳܎&gK\prҝLPn->Mp((({:cL=_v'B tvhDz2lEW""I9 KzB~p'8YYtj ggtrr"*SI@nnV2=zFZ33N '4yudn,Y/k*E==:13&c̘fb-Y>,)Ú˰v64b89ٗI@4g؎땞^Z%>>s~vl2l{>oT j&1pqĉM1 XR)CfqXh% ڒ ii G#^ij{%"Sh}>dϞ[V+Gq:#7ro /֛ӮR{E'X;`V}|=UG֝qd#ShAC:č<9ac#Ӫ?K۬%eXsgLm EffI{٫ĕylD%}>|X;9dj||_T+̞Yk} khF]t'?O?0핈qyb`t) 7jUEH|lbS(.~L4_l;vňKB9ztJ?pP*q],_4.$jSGb?tSYu#)c8v*+Fxe?ŧpJ6 J7WwxYE矓Wv]aeX{1m{}+_sXÆ/%Y89`y38~r~^5lXc=µk9HM-Qk\4'NhOE k }͚#99?ڀ5L\ 7"1ׯGGpX쀑#ؾpD ZԾ'7xZW""<~%wzX':vtGZP(о>ou+dv{=#uw/۠qc~;[+qzZJW3kV[[=W..r`Ӧ^aEEX Li ӳ6ÛoӺbj %PrDFvoQ9G/2"Δ8^#cjU7DDtD۶N h6/^֬ky1 mР{ lc6dzE:0>L)k `uu9L>N::z㥗|"w`ŽZBqcxٺPaoo__%ƏGllrώ:}ej֠T;ah9eppZƪU =OGm9&FիJ%`o`88uK핈*̿ehqqqFyٗq"z oYA}krcގػ72cV@uJDd,sTA.WL""lld1s~{E7Ç7 { l +QbU.cߧ4]^]<2næMW0jTSDE=L65)UڀwQYk^d,59qbXծm :7QXXEk;vm[UYŋOU1|&3DDDDDDT%1$""""""&DDDDDD$d1$""""""&DDDDDD$d1$""""""&DDDDDD$I&1$""""""&DDDDDD$I&1$""""""&DDDDDDDL2I&1$""""""&DDDDDDDL2I&1ɴ"LL5ʕ+ѣr9+""""""zʒL]f̘x|HKKc-i&ӧOFXX<==!Yf!77ܴYYY:u*||| 퍩S";;ۤY|9lmmJ?~<㧟~b1ɬؙ3gCEZL?""\"⩭ȓ'O"22R#G 00'ODll,222`X֊Kii)BBB~z]ذa6nXnڊc„ ؾ};JKK2Sкukڵ ֭CZZbccA!&&{3L fͿsUEEE3f y͛B\ݻwBرc1aݻҴ111ԩS  0o6ߏXtٳjΛ70h (Jt Wܛ[ " * ԩ֭[Wn۷ochذ!^{ /^pk֬L&Crr2ӟ<{,d2;&ߢEr***L&CӦM`8;;CR!88III8z(gggԩSÆ ݻw-ʰxb\|˖-9СCݻk #iT#4h ~'#'|4-@]La2J%+?.ѩS'ѥKq)aaafmug'N;;;Y+ @dddh ?| a7DJJִhڴuai.\ f͚mDDDDDOLBCCѱsڽ0)tppDff4,11Q(Jѿiشi5TT F]%L~HwUnYXY E6mDă*޽{ bΝZ?@( i -((d bVK2mDDDDDL2'&.ۨQ#9sh޼9{ni:3ךo߾ÇW+ݺu>׭[WXnxzzzƈ$$$`ݺup{666x뭷pAdggc۶mXvtZ~~>êUz+))ĉQFaرV[휈3)ܲe zj4hm۶Ō3p_pwwwd_>ƍU&g =>m<~8,YiӦI%V{ػw/<<<зo_CTTIVP,Fͫs9$&&BPGg݊oݻ4g9MΝ;?_p=rrrp?eB{9YdN:|4_J m۶iqx{{cʔ)Qno6KQϲ̯9^}{СCQN={Vk  oH_}U?U#HCMMME~xK.i}휈,L2;uꄴ4L:Ŋ+<}Smŋc=z4ѵkJ(ooo#GS)e)EEE3f y͛B\ݻwBرc1aݻҴ111ԩS  0o6ߏXtٳjΛ70h (Jt WܻjyYd2;v [h!믿0rHԯ_[.!!!7n܈Ν;CP ۷ի ^d5o#-**L&CӦM`8;;CR!88III8z(gggԩSÆ ݻwʻ}6Ə xpZ/˗l2:޽@$BBB O6mZjYmڠ ̘1CZ;{&\,\PTpvvFNn:Ao BCC1Zî_.ԩ#ľ}Dvv8rhԨPTŋz_| Ǝ+ݻ'ݻ'F)HK׼e*JW?~\deeӧ SNK.ԩSZ¤SSShР駟DNNطoF?c8qBI5BDFFÇ Oָqc@hM,MZ\֘V… h֬.;_| EPP >#ADDDDTeff*[E=Iw@|Z7o,7xC=z B$''Kî]V.yI&?HRRRv*7M6m4@DGGk%ѣGWz(,,mڴ:t~8,YiӦI%V{ػw/<<<зo_CTTIVP<\:t.<==1{l_ ??[l*uT6 0rmŋc=z4ѵk*S!!!HKKɓtL6 Bdd$6m*M;d 0VΝ;QPP۷?Ghh([YH}MTIW]NRRc,Y+W:uꄴ4L:Ŋ+e7...&_3&~ի1n8Vo,߿ 899!,,8_("""[Pa XrXj F]n󇇇,Ø(kڵ4hHNNwƍhPl%!u։(@̜9jS%eҺO0Aܾ}[ 1)>7S5l} Lng"''GYjgF)UVO?Ԩ}ݜt8q O|cL"@8:: """%33P2@t(v)d-DK戋3J֭  99Ykyk兛7o+C3WMӪU+$$$… 7;hPl%׬Y3\|R^j+((=lmm UEJKKakk WWWܿcP|>CQQMj'%%f#-__xyy֭[HMMB*ch4>|||p Z1oLC4DN^Q3%>u֭z]]]]ViPݹsGZwww__u߰aCQdVtG񥥥(**2j:uhSTunc ɿYk-_[BK=pW3ǏǮ] @UcڏKݻw\~gӻS'f~VOӑfQRRgϞ"""d &&пr'YFllQ}`[l16~oݻwC #F={VUO>GGG?^EPu5_m17xC899={di|tt_pqq:Ǝ+Dnč78u jժp֯_/ڶm+ ֭3u ///accPl%E&MѶm[w}Ʈ)4iڶmNNN_7}P1|}ۧ/>ߐΝ;I&َ)h~֭prrobhƶϊڿsӧP*W^L&3I&.]"""YJ|zUVӷ'N`Ĉp_ xzz"%%ŪnѢ[tؑ&""wCDg)`R0gܻw6me\p &Ukv A`#~?~~~ܹ3+V0(DDDD$5D2ǖI_Mo7d ՐqdI}O&A.'O^ 9쟾eLSʟ:usrr0|h nnn>ި2+[v1c///lݺ?x3g΄֭'j%GPPfe7...JDDDD$)((R凅?/7]۶m;w{gWTW^?֭[6mZe˖!""| ֬YEI͛ݻwG˕kh~gcdY{!SD2@o%@NN}DFF_+*V61\Ì-ڵ+>>qRSSQZZzi-eӓ{ Qe8xJriɬj&+宆U; ;bZ7nׯ_Gtt4VZdeen޼ pqqݸqC|Yk[.d""""PCll,g庹xle2Sɓ̘1;СCU"^^^tVbi|:uiii&_QNDDDDDL28WWW,Y;v,-^f߾}k׮E^^tR=ܔ_{5( a}mp=_~N}˗}dgg#!!G]n'"""""&UZ`` /_dL4I]|g3f >C8;;cѢEUVT>Ԯ]cǎ6k,[M4 """0e|F14///k?{l,Zqqqk^O>3g8 ӯZuCUGff&\\\鉔f?6 'bΜ9w6mѣBDDDDD;LΝ;#==!!!XbBDDDDD'\h#3fyƍ crڵ*^W2n6IIIlcKDD%?lܢEʯ3b;dljW?i;ts8;!""""""aIDDDDDD+_P~}d2d2xxx`:ٳ'zɚ*oR|Ǹa 2220i$x{{xg +r9իΝ;cڴiVZ% oٲ%ϟov 2 B6J=Y6cB& | 6m 1o<4jJ 㒾&L R :2 3f0L>^^^P(hۭ֬o|;prrBxx8FҊɓM_>}i&Gwy*uEۥ/bѡC͚5o2;;;믿ƒ%KФI8;;c&O }h1իxB bѢEh׮Q<<R '''hm)..J6]\\pYS~۴iLz꡴3g΄J{`>|rbҤIխ[yyyFm dgg#44kF&ML:"**… E֭Fƍbx ,w}'+!C+_tB*5k!Xd?aT9B1~x@,YD޽{'}Μ9SQQQ/DDDxӗJW[35j{)))ĉ1MZʕ+_z_ѬY3fuzC{Dv팎gq*cv{xmilj1cB& eQ\\l*J;W0wKжXx,-OoQU9GL)gzP2@t(v)M)%%%ҁ]yEEEpqq)W͛I( ` C{xxҥK&I߼-Z~ܹ#̊WLScijvBYg}7m>mI)תUKEEEP x* @,\Pߪ&dY&##C 2D6mڈk׮Y{ɒ%"99Y 6L﹂@XC H_~dN2+LllиڵkڵkC.233kh:///|ܹs[[[>A&oyo޼ prrƍV_MbN,/\xzzzշ?1evϜ9 4w}\}k1z霟#ڿ'SC!JKK͚W_EZ[4ڵDĘyeLk?Xt)rrr>PQƍꊁ",, !!!/ƍѣG@jj*&O6ATu QQQ.kv;W0uWOu%mKAAΎ#":IիWh4+))Ql2Ԯ]3gz튡}||?UFzz:[(l\Q,u9wUrjr=%&&bʔ)ظq# 52:7Ɓ˿mCjj=88ۭ\}~dXfMq)))pww7z[ׯ/U$;v@ݺu^FXX`ʕy&tn4hLe˖!33S GamC߼Æ ;uÇƫO+b ə3g:s.C IDAT5fߦ\6vxZG}P7GJ={ȑ#FųO>>۷~&$DFF"55999裏n} ={ҥK3g֭[j]-5-l޼Y$GD5gϞ=~ZkuY!ċ/( {I&+WHG)bܹaÆFscM7e@ 6LKg̘!܄I777W1B8:: '''1eQXX(ߴiPTQFbŊBR b̘1Fo 4gϞOM e˘XgǎaÆBT#F&MHl/YeGEEiYƴPGCm~uyjڴQԩptt BS;wNojmY&,,L4nX888;;;+f͚%ݺukimذяssv-'pwwݺu3z[ /  ҥA4F[gvѢEغukW']~uĘ.]Yfƀ hժ<<#ƌ["""*d&%%!))鉝H='cFl-Uq&${MJJb/f趻 =1K7}E"""waUUjM.^^ X\6zd2mNDDL2zyElU*ܘ|J8 !1175k\f͚aݺu5Rك!C@DD̎={ ;ҰR }¥xӚowyhtέM=l7i[w?SzL@Ω:TftjkL~C4.B4y} +\~v;v<}ʕ+GÆ !ѪU+l߾䲒u5RVZ!""ږsss{Emrf'pY+6IDDL2l֭xn:/EWc+ux饗piddd`׮]HJJ«jV̊FO?'(0WoǍH9QW=vD#X&sl>XMH(=iVbH"n+n$`*U_~-˃j}ki-Y[3\)Rp-/_F^ТE ߿ؼy36l`R9xW1{ls?SN!==v;wIp§k{|r*bXt){9T*XjUx;>>>>cٳǬ-825Qk_ 2B~v CvX ̵*"Ji^r,k,quF -KKht]=v*;󰨎eZA-aq(FD7@$ %nC ư 1&B%AeFphi7@{9u[}+ayΞ8rBKchuٷoo\.lldwAqNmGPPW^yǎSM6OsxbA]] "(**)TnkV͏bxԄ#]_K.!̐(Ǿ}СCw)Ob8p ӧJ]_{P|i`000 .իpssǃƏ߫A(ͅ1WWW\|jy@x8&ONf_KAlYo= ܨık.CO"Lr萼>·p4c&]8|y(-'Ͼ{߹E Ta{U غ}s$dAÆNmOF ---XYYaӦMxX 8t[#??6l)SZM_~="""1~ϙ3^^^(((@UUo>MܻwO"7[nF nݺ9>C|ϱm6+_}^˗/#)) Q|OOOرc?~~P d |}}<UUUT#!!~~~Rf%j_Їဖ4MmF~Q=Du GQP`ߖgS& pQU5ӟ`Cx o v8~kjĸ#ao}9mۓ&ngG5?ZǾkwy(++q!|ơCG A***@<ݳ rZHMMR6[P 1ql#tae3=ޚ)ď%+߻^NmCLLx$sСػwS/^իWcҤI2cn¸qrJ |駈PPZZu|>]w=&ܖ?WMS?---_u7  __g/cǎ.y=z#G`8pܑ{ AΞ=+q̙3pppgggh6)BTW]\7Lsa-ςW|̋ux^_T) +z|G)WʪF4;+5 GC;>Ѓ_@k=R YB,Ͼ{0FǾꏀ:0673}|$iKO1zZQFI ̙3ؼysѹFacc?i7oƢE`gg~O<QY)U{{{!77RI ;cÞF)-\*PmqӱΠ/]999HKKCuuu>lfGkS]]] ,, "ׯ;' >>>ذaΝ;dffbƍi3gDjj*x<^lִiOq#\T?c?]->}AE}gոSa#gkjtIYh{/7@yw߾WF<(oć;jĉLj +_MP~T}k䯯>El+g BjܹsQVVe˖Fpp0<<|a_4:h5̓CV;FB" D_Ov@hd@؈W,>Xg#|ED |B/Nʯɜ=M ëq3v/+S~쫈eёGbo\`wySoooΝO>D<V͛7cǎF]] ~["Gpp0W^w###͛///1%%%öm7[.{3f̙3вr%C~a͛aggFq0MAAA–-[~~~-^W2Q|iٳ~~~4iΝj;wʕ+j*,]TB___lܸYYYPSSÄ }vs" ±6 })ے 11׮]庺|UfvJy_M۫xuzQ#F_^oVbVJgڵ v|>JKK_$ ^z2~g4 /x{{[ $T#2Xͳ K}8_}vwAA( DuĐ@tKyGAAtbiaa.SWE,,,}ߓ?P톾zulC% BBvMjW7d3lKAD t?ٌ ?$ȶAc~ǻ;\*P}>S̘1C{ƌ $ API*P(Ddd$"""T#""ZH   2G!#&&666Ѐ HO%99NNNpKK?~8`aa {h$ #^a|_=z4~WxyyаcLII;\y}}}.\"̛7)))Xd AANhl/dϞ=ؾ};\\\ɓ'c֭صkٌZ4?ӧASSHJJt=srr˓rp(γ8;;#''`M  ($z pqq8˗/J0gxyyUUU¾}:]RveGll,JJJӧٳ2())6}   2^@ Ox>-vebڴiAtt4,X%K'O(|>`M  ($z -,9s c \.\.Wb# ;AANz+L 7Z%sEYY-[" <իW{`dd@^#F~~~ضm<<]6;;cǎ2|UlFm 螼mӧK*¢KWEf!ٖ  ; tʚ̮6 ٖ  zxwdDO?Ō3Tg̘p@  BDFF"""B=""$  {?b֬Y0`ttt0|plٲnZhhhqqq '''u䥥?~fff@FFdj]}-V^}(((ҥKajj >+W_}p8с-֯_   2f+W# ,ڕBbb"r"$$0Lz|RRR0_+W"!!Ċ+d3oM G ǎkc(//Ǒ#GPVVy)t}AAtvÆٳgF:*eww\]]/qllʔ)=fTgҏ]t[Аihh1cưNsȐ!?pJ755eYYY… L޽+!s-fee-mZ>oxyym۶)u/" gzzzL(ʔ///g OA=yTUUٳݰl8Y\yqL< H!77...\]]qeS ̙/// QQQطo_YZZ SS_xzzbEII BCC1}t={VB%%%o̙3 {)QŶp);C #N:yuuuGpp0LB9AAtcz\???DEEQJA  x"=t*++1a=z/O^V̙3q]|7طo'N`ѢEo\"/ik0pv]ѣGo~SfաkkkTVV">>^# vS_>RSSakkK+===v,듞JR[7Gqqq/O^Vzjj*MDGGcXd ,,,pIr v,//5<]ЍHMMŬYf(ѷ o_$FZiM&AAPR˃ɽ#լ ZLY1XZZsqqak׮uBx333ciiiׯ_gW.ە&:t([r%?_cK,a&&&LMM 68p@!y[[[#[NN{lRR?~BGAl.˱6 })g Abb"]%tW͞QZZ#55Ul_AAA䇝d[ {mƍOP" MUHKK# ap IDATAAADGҧfggcر]`WC-AA. ]R]Z*B6#ɶAAtqUSdv?ٌ ?$ȶAc~ǻ;& z~)f̘r=c Sݒgg L :PHDDDܵGDD ""TD1g$ى  [qy,ZFFFׯF-[@(R &&666Ѐ HO%99NNNpKK?~8`aa {h$ At?T=AAAXjvcccܻw۷oǂ H5  ÿ/=+`hhk1z`??#FHsABKK SLAAAA ?3 <:::JO_~ ###O=fTԧd2dٳgٽ{XCCC詭̈́Ba/O^^zcc#;y$[`[x1;}t0mm.;zov'^z 'o1#'/+=55ӦMCNN`,Y8yD9{ocԨQ/_~:::9r8ҥKx7```~eW$K|]kO ߷o_ӋSTTCkݻ*Vd/yKԼy]+cADOGMUWW >sSJŔ3g|*++%\DGGKL,q…_'ĉ 1V\8p̅ z&]7aܹHLLDbb"Ν+6|,X`iz7EoiUQQ̔>mmk99_.5SIz\{p8NO_nh%AD 2NGR<'''])`Æ 8wjjj7ח|0TWW iӐD"BtHHHh6'/+]__\1. .+ݝ`MEhZb=fmm-ѷo_txH(--ŋ+}}*J{{; ++ uV^-N_byA$!++ fq -+O///ݻxW&@ ^dL__ikk3~zV^^Nk2ew^fmmՙ5kFFFlڵĄ0___|>c4455ssse! c7nhZOQ&''3>XZZי!Uk27mmllM'N`#F`ʊ:tB!/>cؘY=yDcl6l0p$)_1cư~1sss'Nkhh`ޞiiiѣGnŏ2k8x 377g\.W"]"[1###k._EEƃ ^&c7l8fgÇwxD\vK._!=磴T%5<<ϟGjjJ z×d[hbL8Qb m{ٵk㖤Aeq-ӅԨBy1HSҨ2,Y>&&&1{ݻ)$AA&A㐷y?#;x71ea̙زeK柛KF&w;vl)"d3lKADٖ/n YXXXtiٌ ?$A}UN.ەd3 ADpBO1c 3f <<*  AA&A8BPk@DDjkk"  (~Dhhhqqq '''u䥥?~fff@FFdj]GԄ%|||P^^"Oyyyχ^}UR_,Ŋ^AAdv9X`6nH*D!..刍EHHKa|`bccQRRPL>gϞ155EII .z!88 s)̝;] sssDEEQLAdvݻe˖a044U@ /㡪TׯҥKĄ pԎ_TL6 999Ƃ dXXXɓT vo6|) 5kִHomMJKK[,(   23g`-qpph1e̙3ppp =_ɓ'+++%\DGGǧtttą :\y'N'N ((⍀8w}WB… O$|y |'T&Oa;#//mEAdvC=aoނ6l؀sΡظqx0TWW ԴiӐD"BtHHHhҦ7ʓ.+N\.\F@wwwj] ())g}W^yEk裏m6޽wޅH$Bvvvl{bڵIAF&}̝;eeeXl0x`W^z*lll<{ՆFa۶mһjv{{_2޽ӧbccoܸl$&&R"lz###СC-_1QF!##~!6mڄF >l!'NԩS{{{ 6 })g Abb"]%uu9|>zJxx8Ο?T^uvom [C Y7nނN?]hhhH&A^ T4<  L@AAAt/}lvv6Ǝe vuٌ ?$A= QXX%`aaѥ"d3lKADW:eMfWnSXXH\f1.A۶.AAКLPS9+*{^sVTbG*H{=2sK~zDc9Bkv}+֍=Nu!j3hPƘAtD?CzeI.=}Ə?Yfañe֊ω 444`cc#E }=ho6.1`}ظ^y 1jC^aG0<VOw׈ySO:l$g˳o{֯?CI-J 'AACC#F#G.&&&h"_~1blP6igׯGEE]ާyU+VH'^﷢;w444`mmݻwK?zh2GM 122j]%%%022Ri+Oof+W# ,lG0š p}=ǗWACj@Cq^?4 (쏔E8xBz?tj◣(6/NqtyBiLjq\}kE[5Ƿ7XNeoi!S۷1i$ٳZr`lܸQxPPf̘˗/iii~`O.//Ǒ#GPVVy[+?##YYYtЉPV8p{J8p_|~|UUrrra?UA)4440h [J(?ɓ';VtWWV|Nf_H5U_Q\ڀOKSl0Z^o)v&/HIINmWm-8CرIt|yHx۷WD^ܢk*CɃlS>¹u4҇ N> 9ZZZ¦McC<ذaLtl ś PUU(۷[QuuuGppNݡ|OOO#**M&''㨨oUVIիz*ܹP[CpIa֬YyގwDhh("##QRR (O8}4󡦦>ի-kʕ+˖-Cbb"JJJe˖)U3gpU\r?Kp59sU~yyK߼y3JJJp\|wAHHR'K?y3O>\<9x`+%_R̗?ʪ_y| p\r?Ab{jN¿o ##Ce fϞ^19r$,Yzz:{WXXXX]g͛1Ƹ\.D1|||cD"֧OjoLqXMV{ӄ_cƌRcğ1XF {w6&!fr\436+m\֎Y6a'`yx憱B*+2oym,l M]7r3mr/x Ӈ͘1]v=~l bٳgǞHÇAEVVC a|;{,wkhh~YjjjLWW2lѢEL(ߋyP"3؟ɌYQQ|QSS+qnuRX^wP(nee.^(5?;;;vm+UW^p\ fzq;jgΜ9lS){V?C)MW'^WtKKK lȐ! s'e8q2e ߙ94hy&suue'OTؿm_Eb~jwt^xUUU?gfvÆc_c_g=jz-$''w}wӧX~=}'^84= Q|6&h5w70UuG׌q'ƌRgk^7ʼf.{#5Ys;y?ד!h[56nFrL QSJVoaC$wH O)ط'/?ψG[=v|3*ع>ď%ܻ^NmCLLx$sСػw/wXz5&M$<p-7+WT 4bݺuG|||~7/#BYsrW>XZZk֬QJK.7ހ8'1i%%%JɷA۷DzQQF!5" :T<`bbw*e#y+~tuuQ]]-_uu5tu%w^bZI/Oqq{qqKћH*bԏ<"!+D>Qמ~iS0uTL:Nj1]V^ooldOElS=~/&S]]ׯ_ǥK(Nsppٳg[L9ppp8S~!D_|]E^<~Sֳ}9 Py9y2LzLYXoÆ=J])yy<=+$}J {u5ƎJK-בʳo{/O^Lc_GJr}>|>Q֥=RS@.4IDATQ*:EgͭZdcc?JAlmWe;T\DGG+=M+1b!}>l;"zzzJ<(\`1&+j)%-r*#/ ssVם59mhh45j%ιx"^}Uco&c6meX?733ß)eo{'~ڋ>|>_¯ߴ߹sMzp8V   ow??e륽eeOEkzǮq ŋXxR{#fΜTxLM l1-z[vP"F ܧEYl#>Ff&<.3q٪%lZoe7Mu'ufПr`)&1 tɼW7 ;KԄf s\]HbCeܜq Vzdff&swwgL[[ذ׳r9ڠ"k2fbb|}}1Ҙ d͍]v[l$6~v|cflϏ=zH_d 9q1b`VVVСC-֔}g5kְ'O(,Dvv66lp8銔/N8ƌY\\8EFF2{{{F͒]cEDD0KKKƆ ¾6m۷3fdd|||Ǐ"[1###k.pʳ_{GWWW˗|rV]]-Necƌa֖%%%)1vAfnnθ\D~RQF5y{{+&rdvD؞IV?fؘ?djo{3'>DUdr gl_BBBk׮uI"M6SZl^~0Ի-8?ğŵ":$9GǻਁGWmKEݵk4΀"p 6RŘ8qZ GTE݇K7nނN?]hhh@ܔJK|`n`v7a:=Nñ7hꫯ{n`}Xd >#?ٳ(A}@A&ѵd1$#ݒ\2A2y71ea̙زeJ\5^=} T}}K.eɝIWC-AA.g[zwg`aaѥ"d3lKADW:elWnSXXH\ClKA=wå"O?Ō3Tg̘p@  BDFF"""B=""$  ooo 41b9B+@CC666#=Ud899ή'/-033222ogg'''$''Sp8R?M+VhU|߾}akkիW:X +Ln߾ "88 ͛7b̘1Tí0_ѣ믿 FOgJJ ϟ˓닄0ưpBI-g޼yHII%K voۊ=##YYY?~TyH;wرc4iN:C  z*? xWp1])ٳ۷o tuu1ydlݺv"={񠩩 GGG$%%u999pvvpKrp8hll؈brCmfdd$VZzhjjbСGpp06ow   2 OF ---XYYaӦMx1ծrss"q/_&=`Μ9BAA}u055pJ'ϟX 44ӧOٳg%dLMMQRRBm'8s uAAARVVǏСCxN<7n jW @<5PUUEz*q%TVVb„ 8zh_3gݻoo>'NE v#ߔ&]vaǎsܲ Q^^.q1&wZ.AAdv뇘HСCw^$&&RJAOOǦ`]__T4bݺuG|||[LAJOMMŴiӐh,XK,N<)QNqq1|>.ͦOs,--5k-A˨Q8b3g@z'OWVVJ#..O .teO8'N@PPrJ$$$xw%d.\GGGj7祤Օ:Z  SuY|9.]D}6V^9sPJ6lsPSSLlܸQ\.\.Wb# ;nػw/֮]"yyy–-[A3QWx{{ѣG={6 1w\|'Ϩ[aܹ(++òePTT#88zjS `cc٫60b۶mttt0nܸ.]aooa˓W&ݻw}All7릷w'ߔ6dĉ:u;=9455ann̄8iښ:_ %p glq<$$vZ("d|ĵHMMU)_GPPa'fgv׮]@VVVOA=^] Y7nނN?]hhhH&Ah iiiTy= w  zd{ջqsQABm#YXO.&KJiA ύV)!16:aZ+^.\GI~7k?+g4̼_9>I?xx<4(۷+###lVD:`yb$+ HJJ uHo:nK]67z={ݻwjmmU[[N<,k0& dZؘ-yͥj*IQSرcڼyۗNŨ?`ǻ566*--URR|PiiiO>Iڵkx|x֘>s FLznjkkyӧMNN:s̶=N>|u)I TNNݻI}YSSSaytNaa:::~?gi~%4M9F@ޮB\ :EqTSSիW3o>СCƍUSS#ө/_FEn:~%''K2mٲE###:~.\ ө+Vh۶magQQ[.Z/t:YA/5kD'#Hn߾o~Ynz*,ŅVDϾq8| z-|ܹSլ%ZWcҊn"!*zzz<cbe}~+g`[ 40 yMB&`2L!lv$t 2Mkȴ Ig v{L3kȴl2MCv{,i2Xli4L)wc]IENDB`plprofiler-REL4_2_5/doc/images/tpcb-test1-upd_accounts.png000066400000000000000000001232101465735455400236210ustar00rootroot00000000000000PNG  IHDRY!bKGD pHYs  tIME#5H IDATxwxlHH#BRc"RJ*\Q EQ @" )B '{?;Mۚ@y<Μ3303VE@ """"""2/^\ .fH/ߥ f=rrr ݌IDDDDDD dL""""""bIDDDDDD PyXDDDDDDdTO j 0k 0 4-X}DDDDDDnݺuL""""""2DDDDDD dL"""""""8l57,}zuc[_A(ס!uu뭏ط&&MB=0`Cs,e>=!eJpi9)*ll_[%"d0hNKйN 40l;b֮'{P*oD г'J\g#Gґ]wwkt䆷n ;}n"99BxzڠGOL__eX￟>? 'ysF<\yGB@F6&Bf @CJD(yoݿ{ێ]͛ѣMc[w呬4h/vL;E()Q"5;w&aȐuwaРX\Aqq9JJHN_àA,)}ff }矙*Ai&~R3g~ב%JJHJGLu^$$54$K%"b?W!e#G}QCg]^.qU6>BWTyINN)M;%ms@:"k {}tRRܿpԋ1 @^^J?,^/1tDŋH/>rȑq``iEE7Bg>W%""өg2=Sy~nn):vh{cޛ p!J@6Έ @ލ,o۶kx1EEŐ!~7ƳsoU}zuXw6\w]m:c'sMtƖC]^^:tpsF| ˋغ5))Ā^>\] '__FL5$&}6̙nmpF,[30.cȔ}DW?/c - k4q￳חqDn,R)f.]1i?n}/q;vG挌bXXn]=0fL 8IyoKWvuGӦm ܽ[ 7)is^8~<M\fpK$G~~}76N2yZ.ݭ]BFϞ>r=PXJUWOSڱK2(@Nx[ly oo3[k>*UbW9ʀ!ǛoZ&DDR"'6]GT 4'NdfM/YKa$&_RiS?!tXƴF\m  ?mt9*R r޻WM 9ieڵ%%%غN]NeSOnW]Q}D#⯿*NpSR'll71eai^}HK+ΝI[]g_XMbc1kqi@RR>c 4R]w!NMQo9]Pv zyJ5S*Mysj_TT^EE[m6gWW+\[e@MX ^]  4{Ş=xYQ\ 뗋S*rܹF!˰mLKˊM{>( aؽ;[Y8$g&>|u`tri{ &/mw"qw'UmuRXXOƍ}p?w}иc{^edl ΞbcIܸ _mDrZOl>e߸tg-[?q%_}6607a.믗p?0}z;)Xoz}P""GzҜ0d`vRlTbT%"d6o?S()Ql}` G̝[129￿&}1=5S fprk +V͞=zxO?ٳ;H6mnT9t1gNGt;; tY*/&fG@`+[O]dv?T߿;zyw:d[6lxOҥx饦69FxJ}}Cu}ͽ{%jan.9^y K{VҤK}}&>M|a]+ؤo6@ UuSQ~AQQ9p- US}WVZ˦PXbƌvٵ.ܙv]^?L7l''9tىo o'km3m}%r\NN2oo+lU^:bc+"(vv@p/6ڵKm. l_zߓYXmT<zΝxV}4M…!Fo/o;zijÐr/AA^kKs=$n#Cɘ2uwO1ގv[w۷ ZR4=Mo\DZX bf< O];g<i\4T܊(ΟBhtKwyи򾒝}tZmh*k 85HOZٳ)Ef{LE'ˌDD@-)66Z3U `Jwq8E{^^Q\V|NN'e&+rʨuZOl먾ˮN3$۳sgM;VvV}Øuyt9vLBy@nn)N#0OWxyViVoo]͐>3vvͽk?MWҫ\q␚ZѤLK+ŎIKP*̹%I& 3fR)'aƫX2d׿:\׵=ߨL㻭27WJY }Iw+=XLf_fU/6VOLEW`HJMWE`32*0YggT:(պF֓1e:2EZL+jJyȉ[pP;Iݗ8V[[ ,[GŠk-*pWc` z)((&Q𥳐v >kO ^`yP 0ǎ} {J+5=puҸ7`o/S;ΈJQC_իƫQTFW""zăL+?ӲoMG5>]1Arr>ZEǎ {k=1h1*4):t[-[:5S_moZOlTmkٛ7WH[&OEF ddL?7!r}t..V mw~TS ?⤽<}&S}3g*nMJ苍>'bXY?9s:xjDnU JtQA-B ЬK@B@Zi{TOǎWt ~DD's&b׮d|y^)nذfeĵk{ FY[ԟK۵ҫ2K !>6>b1cF[ G ǎe#/>wpaB멫6m )]Nz2Ut->8'oj|i _~IEAAF923K j}/qs!66()Qj\V3IIyU֣>Mo:G> 齖 FbbӋhQ诪ч Ihi v׾{z֭Bc޼SHK+Dvv~Xo_/_~%.acG?hjV͚u#yy]5<WV.\Pbf' F_&NX={!!?K.O?=/g]^.4^x_lobϞ\#=Gj1Mg$$ܿ1n\5ӧ&NlU.֭Bʁ*i|MK.7CNn5*ڶuƈO\JU=1fZSmTmmO=]WꢎLSѣ[ 66ׯ ^ѯ_} |5JJYС~'3f3O//sGOMoȾ\_DZv׮ش:^̔ɀn:u=zxjWENS=fl?ѿ`pzwNzPMkÇqf~~t665=*Fxxy24jdk~MvѺ>G[⫯z{w(4CӦ0n_Q}Q#5&B XYo;//[ ~Fv vݻc fh/ wWM:)1cpu 76S[THPXXnl?E.氷,-__3o'4n5у#kZjm۶6b귌Ⱦ'.RO=36S`?1ђA&=ʁƆsbҤxcq]x@ؘ>i0ۿpiW"d=L&=dL""""""bIDDDDDD 2dL""""""bIDDDDDD 2dL""""""bIDDDDDD dL""""""bIDDDDDD dL"""""""DDDDDD dL"""""""DDDDDD d=AL&L&ӘvUrvvvu""""""zĂ̘1X~=޽Vɓ}U k3d59s OOOra֬Yϯ6;лwo( 888[n5DDDDD SٳgC ,XP媥BG233ʦ7`*eWXXf͚a-:ǎCPPN:["++ s1hР*m|r!((IIIHHHL&ðaðfm=AfQQҠ?#pYYYd$%%!::d˝3gb ;v,&N &&FJ{e̞=ҥK͛/IS 3??C֭P(舧~W֭[0a4iKKKxzzbĈtR_r%d2RSSs pq)e@&Ex"BBBBܸqGGGase4uP(  ,YL#Gz1}РAd~W(--Ÿq4ҶiIIIoM]DDDDDdD9f̟?Gƭ[p9b„ X|Ν;ڵ+ك5k ++ 6mB|||8z|RF nWmUcƌ8s lmmѿ; Zaʕ4iϞ=xѭ[7U[>wTIrǎCQQݻ͛7+*@AL]DDDDDTޯ0133C޽saڵ(//@QbEƖ؁.\\c/5_tǎtڨnӧ4SN eee*h2vj|} 2 777\rEK/U ^x-[48q>>>:ujmual'"" 4* )k߾I2dܹse]f rJFUnՎdۥlHg,uDDDDDTVEXXEtt BCCEJJsDddઝ IDAT.##C4mT؈۷Bqq d2ذaZWG.u:]2օ KKKѢE qQ'~'Ѹqc$.\Pm>@ܽ{VWWWD޽Ebbwx7|*IDDDDDD 2A&1|d2^򈈈W2I˖-AtNN|MB.o&rssMc ֭bccNkv~8ϐDDDDDTGA :66)+xF-++ĉ}v(J*J⫯ªU5k`ڵ4hPZ|9$$$$@&aذaXfi.UV+,,Df0x`Q?Q+36C[o} Aff&ۧw͛7#>>o&BBB`ee7o[Ji/_ٳgXt)Ѽys|X>iݮ,YIII6iZ"""""20wX߭[0a4iĈ#pҥziv!&&1=88Xc>|W(--Ÿq4ҶiIIIo JkvUƍXd ^{5,mmVyuP(pttO?իWDDDDDVEXX@cZRRpss"77W;vL4kL( qҥZ/[Lcǎ"##C5JѵUuy+O  ׯ8qo ~iѥKqiÆ 򧧧 ___%~g'<(|||rNX۟eOi 2-Mx񢰷  e ׯ޽{8Pk8K#݆[ߺu&}>U)MS=9`ѣBtiZNN`јgC JkvUl2୷Z~}Yf(Zl\i!""""zTd<==5RSSk_^^+V[npww\.L!\]]չT.F6MJJq䇺#JKK57@HHѣGXJ3g4(ۥNTbƍDϞ=k]>i i&j 9о}{̘1.\dDTzŅ껶W̚5 o ~WdeeU (U9}5FmW`޽{3224 i{HHH0(ۥ.>>wA}]v8<~7̜9J˖-C۶mw(DDDDD 2?۷o&_5#̶kUFmmhr[[.u {E@ΝUz*eee\nPZcKݎ;Zv}VVUwވƹsvZ#**G!""""z|n}UbԚ111WH L -[hL?q|||0uZWw_]zWvҘ>rHiڨQ۶mHիAi.ug`۷o߫{rذapssÕ+Wi/R7Q貾8rBq-)E||Sc򇄄O>"&&F vvv+* 2[L!$22R4mTXZZ B!#vQ%g033???_L>] KKK'yQTTT%mqqXhhٲE3gۥ"QRRI[y[_u.Dpp8Bxʊ+Dmڴz!VE@UngGWjj*zDn`Kϖ5dW"<<_;{rFcوvY ?6lvL}^!xTV""""""jA&Gb$""""z8L""""""bIDDDDDD 2A&L""""""G|O&tNXt^$""""""aIDDDDDD 2A&1$""""""bIDDDDDD 2A&1$""""""bIDDDDDD 2A&L""""""zLLLV貜ö v'N~]AfB@_GYDDDD 뀔WZmuپÉ'Э[[?u|DDDD 27nmۖADDDD 2Q]A0aгgOܼy?ݻ7nݺk~z-Zu^5_[~C/55{#ƏoPШQ# 44z>}:7nƍc(--ջpuu7bccuY ֭mۦW|}}t_2Ə'''ն|mGmR̜9믿i{  6L""""5С-ZxDEE靿cǎXx1ʯ6/hܸ1.]ÇСC/T~]ohmٳg#../F Zٳgqilٲ?#"##u.1g̙3~!,Y6c׮]y&Ms2?#v܉\111z;55P( j_ XrOel6YC/::K.ł cʕ?4Μ9ػw/-Z=zTY"""ǁU@kЪNWlTڧ<<<%XNj)))UQhMiڴi… hժR_u)>ۥKo7n޼t!驵kT*anngggܻwOkOʕ+ߢE \|Y浶c^^^u+ 88zՋ PRRKKKU_FZZ~iՏS,XXX~~~~:WRFi,G[~S?&''s@@xx8x v rîd:;;K'Fs*Fu럟_6@MuuU~mۧ o>t B t.`[} ]{ƍر#oߎW^y7_25vR}G[0yiclS'''oߖU?  333ww*J%JJJt1Ouaaaۥ ^*?fj5_V~]Sۧ:̬rJ8qك\W_|UiP}EBBӱh"cz-CufRR~,>˯խeee|վw'"""bY͛7cT9I;z(nݪSر#`ӦMz߆on;w[oU{XI6+WU~]_[u9֧~շo߾|}5 p;88oѻ=ztWMӧԾ8~8vQ]ȑ#Νӻ}կ]TjjpEc̘1@LLLۯ-"""z h-„.bܸqAKJcbbDƍ5jrc ѭ[7ӧO amm]c+Ѿ}{akk+իk,GMV^-Y믩֯Kkkִ}III[nA7Nk=Te+kfP6XL6Mxzz OOO1m4Q\\wsM{nѯ_?acc# ޽8tWΝŔ)St_}LӶ-+))͛7VVV}ᅲ߾}[WۋC B&_/..BDDDD IvvO',,L h-$% ._0{ɓ9r$.\yHegg HKK3rDDD`Ν eEQ!uԩJI(ddd`Æ ={t+VG}kg׃^?ꃟ:wLb&]cXDDD Sďj]6f B&M¤IXDDDDu=Ua9Q'Od%"""DR7^1eV1$"J~%k׮?pAv&"""bIDDi۶-`ƍ """bID5S]A0aгgOܼyS#g}FHLLvǏ^WRSSѻwo8::bU旖b̙^u]R>]_ӕH:o߻wpppaίkQWճ:cǎ:u*n:iٳgqiׯ"##{*033Cjj*;{lO?EyyyXt)V^ 9XdNWª UcĉѣMC}?gݻP^^[ %%; 5HVE@ضmk.w7xyy!55JZR sss8;;޽{UOOO퍛7o"==B)*z'qdee׫g"uISSWM/**%kMWy"99P*hԨW[B.yyyq^t/^\e꛳3}sP(RU2<<<^j]pqq2_u d[ ffڴRUߤI,DDD 1$gK#""p ٳjh訪(33ګ|eeei PSA[ܤw5ti7nzŝdv7o͛ U߾}TXhrssqy3F#Mǎ6m1@Tzu]>]մ}1tq9ȑ#L"ԩSB^xbi___Ǐ7zѭ[7DEE̙3UϞ=Ƕm#FHb a033ܢEVZi7|^O[}QQQȯձcХKӇ; 5Hv6ġ!;;NNNDZZIoO=+ !"z 6ziu\p&5h|O&#@mzj~~~ܹ3233˗Rd=u1&M¤IXDDDD 1$""""""uAuO&A.iӦ^>_m+/u7|yyyx[[[`РAػw^zѻHDDDDDXAֽ{ꫯ?ԃ03C5m_n 5j.\w}YYYz*"""0oS666;vƴYfah޼9`L:_Nۧ{=uh"x{{UVgϞc۶mpwwLj#K}QQQADDDDD蓵 h-ZĶmXl899iii"""",''s@h%+ WPBFF6lٳ'+$҇:wLb"""""2,Ȝ?>k!}^v-+!Xz_Ɍ}`ԴiSܸq=uFĺ%"":^e>J?>nܸΈXDDDL""""""zDqƐddpww… M۫W/Ջ5M ޫ*ycpxmа5YYY2e |||`ii IDAT WWWt޽A_~App0!ѨQ#tӦM1Ldnݒ߹s.k!X )y[fH<.SN_|ݻw;ȑ# | }YlGAVV8 |1 WɌc-ףUVQ]U7!l___t3D Ǟ={'N-^{5YA̙3x1b|{ `G!z3o;v,d2Eprr񨬬 sAf`ooV;"Rm&N(JC L&Ì3tZOQQ~mx{{O>-oaa!Ǝ GGG888 <<ѣGkWdd^׷o_lذA:O>5 KKK<عsɶK}ׯǒ%Kмys8::bz#Yc/hҤ ϟ:T7{{{888}+++6p9[ڵkLFATb̙P({{ XlJ%LRe t* ",, 666h޼^sDԀ h-„.͛'ڶm+} Ԛrdw^@BW9s 6n(K@,X@k}ZWnGUu}QߺVӧHKK'O^}^vԦW^ OwEr53={ž}ѡCcǎpHOO}5<>#KDD d26meeeFTwB!&O,ͫC7 b…";;JYӈ96VSD 2\:lRDNNNU_[:!HIIV|G"$$D - ._wc֖_:p߾}[[;Ȭd^_S mZw]d3())չmmm1o<> ȴh׮HLL4%KDjj>|xaii)T ,[4#|013zvo~bb"r]%tXx1 ;`ƍ0779{^^^z)))MVd\]_p􄥥'j>vmQzao~ZV7o5k}1;vxWann?SN5̙3s}|IIIFU'GCLU XVV!P*{W`mm 4tɫZ'=}zĈ;󸨪aE@| DELMAM$ R{%Lq TGLsK4ʝJrKEv9?11s߯}s|Ϲ{ϹdYm۷oGΝoc„ ;x}v 8  obԾ&T{Tiׯ_^[XWYJJJ?޽{000wzX xIٳg롧 ϮpP'V Rث^pi]H$jF}6}vӧG=xmkks)n$SSSgӽ{wdgg+~9?u/C>`޼yk233ajjʻ,]tQ (Ȭeee8t} &6oތ֛67oF_J# \<_a*j|P|չUK'MW/)(P_vŎ5KT]^Zi̍]|H{U1b\pƍShhhA~㥷;rssׯ_| Z7Rk׮Evv6d26n3fL P4hBCC{nѣG(..͛7fR{z] (YԩSK.J/WcwQT}G,%%E{ʔ)1g}ƺuƴ^⪪ `{***`|g* Dj.*\.g'OfȈRcG,,,2lڴi۵kd" 4,bh][Cnݺ;ɓ'3;;;wީyqݻw+鋿n[~i:PU\mٳ̬~gϞvvvfL__ 4_*} -Ce„ ֖D"lll… YqqFhݺuS,ޣ׍'Oz:0SSSo?wY=^=,#pz;:~ H/_:76͝k4#ȏ&44 .ĉ'0|p7ЧOb)o-AAm:_PPP|N!B!IzzO.h1æM<_pZ|  P&tH/rJL<ׇmۆ>|  L 8r AAќA /_l'FH3 m hAT*T*m̿z~ Yx3O7$euueo):Yu`vq_=R z G8q;je:YQM_LLLjT*Z92 H$nc̚5 VVVՅf͚Ǐ4ꡩGθlKSh$AܴO"$$}^}U󃩩i`̘1[lA.]C\Ҭ超"XQ`rl 5F?'m$Uôc"EZS0{>ܫ9 74Uyx{Hu[ ?bPU\p*{Bd ƀiqxEϥ8VOc09J***Ea޼yl:u VVV{.add???Nĉѻwo\xBhh(&NSN>FTR NADkōd~  1d][litL>Al*;S"aG- ~]v׻Vsc,ZkC$%G~ՉӥfNҫEj rfltW'8x\VWu4^?kiPY1++e=SWupϊFmSHOOG}ɒ%033C``Z;tb6.;voe_j*XYYA( +Wo+FKNNƨQ`ffHggg8pu/{ڴi055E׮]t;w_:v숞={ǯk$G.]ХK̟?j_C]`֭H$011x5xxx x7?НAd)swwǕ+Wq 2I5z+?n q3@O9RApX!K/{CNEÒuXabSL|G76CXdf"""sV1ƣGDEEaԩҥ ܹ/ `~ڵkqF|ϱqF[5ڵkr 8Gߟw8z(~G믿Od i333Sm?111 DDDDjV c0eWL;k!MLN^%.:!#[)ϰ$W'rw1zXu:r@ gryx_:9J~>wϚ!r,g2?n5iٺu+lmm1rHžGaصkW{{{)))(--_3gB.oٲ/^Tɼx"6oެ1߼q._< 0R0k]ʆoU}bxx8,--ann(NU`P):_H$V?2AL###*n ''J}駈CjfQs×UMbG}60BmƦe8xW#^^_~ꄬϰ }砇c8HYSH5_GObOq zptSQ3Xuh6իW#,,Liɓ1g <^ݰa0|pރ"`O::R)JKK!Jѿ)wBB233'oj D"hiռ|UM޽{wUعsgtCTũSпCmӧ޽.]k  ׯΜ9ׯ_*F#\{k%婕g/˽O] %x63ZYUQRW*me1"s վH\_ck:q[',esS-|7ccQrڷq$ ƤIX];ʦu!55%%%?![o 򅄄 z?aĈ8z(d2JKK!k|KsS0믈S;ܹs~)222c?Wi{Z}M*_Ek׮Zz8K.Xcǎt'EAԠ}wENNOtX[[cҥµke%UD׸ASKC?Q5:gk]sH]<ɫĬ V]:`G/x!=H fZz[K?`њB,jP o?a;p(9|a/+:_7Bvj_mUU\O<~RCCK غH!˱u޼yXt):u߱FjئLooooΝ;_.}xb 0?X,o{ML0~~~ӧ222u x7ZaaaOOO=Z-fAGG6mBϞ=ann?ր<88?lXx1b ?]hѢ&Ӈou/D`` w}<6oތYfa٘6mRh"$&&B[[ @hh(IA58vbx-C|r*VDlj;HfQwaIh!~imU1]7I^#H qtnWa8 `Hs9lق  11MߗH$̤~  z_ ԩSqt4P(ly#Db/5jo)< ]jAm۰uv~8AAdJ$4mD]$''AAm/pss! kb˖-$uG}5j 4y9skiiA RqJ<|P)WWW$%%oA$ L 777}r ӄ:?~~~HKKC~~>"""s&/gff&,--5^~쑑UVaԨQ8sRKKKdddoA$ LPPP033C~~>ӄ: ܸq/_F^^ CsWe=z4}@[^^^000o,+xz{{c޼yuzꥱsW[n:t耨(7oҥK%lIAD@ۉvt?˗/Gll,_,'FH3!H*u͚58<[4h/^L~DIAԩSqt4P(l#A4BBBUAAD+$   4e/]7 7wҌ ?&H[ hRRYi[#A~LA~Al΅R)-PAiKA|SCdDdlu5kPAAb($6\.Gxx8Zݹ!,, EEETAAdjSNaر IDATܹ3 +VM'P(=IvS\\\\\s~t6668vXwtt 77bD޽;ɓ*RRRDPW^y}_̙AAd֛`xyyիx) JKU Att4ASNQm_~PC ڵkeG:6R}_rr2F333D"8;;M^Τ$j\ZZZT>TIII֭.\:`̝;֭D"x Axx8fϞ  ȑ#2dV$''Mi;\B4NǏҐܹ˙ KKK+*{dd$ヨ(ddd`ժU5jΜ9HOOҥK+'Owmйz{{ iACLL C055̐O4N7n˗СCm\UG`عs',XWWW?~\i*&ffջ:::A\\ޭɁD"xe_ߧ  (?>>Sj9366&qPdffO>$]D"ԘsWeLj#M6SL N8Çk*MUJ#-- ^^^kk{'5.ݻwǼy% WWWœwS+1ׯaJJJ)zh$''6m.\xҫ8ǏŋannXh\pݵڰʕ+qY^0d } ##Xn```í[ЫW/+:ߺu+СCDEE)߼y.]Bll,f3fbc߾}5/K/cǎ?ǒ%KPYY '''ٳFڪUM8p F% V{l˗/Gll,_,'FH3!HjoRYf Ο?V ŋ7oAA|!2uTܼ} ! [H&A @GA"    MtK.v͝k4#ȏ Җ  'bT*T*mlll5iF-AA|sPw2sTJ TC<&1=L>iKAD^z' Z!Wg+'֬YCLPcKDo0T($V\.Gxx8\"h 6cLwq җm_ 2ϟ?I&A,cǎӧVX\nԩS;v,:w 899aŊJ7⑑P(}|\1֍%Ʊ>C!5.Ga7 yλ8::*'b m[2 H$nݪ?u#GеkWرcuiT3.mچhS[n ӧ<Ȼz[zW^Xr%JKKppp9sVx۶m@ m8}9iC)))ڵkK/:u;#~7  29Xx1<==q!!!YYYm ///\zO>EBBRBX ::OH:*ytZyH+0|rlqbWgd_6ǎ0c@ /{sl>|>>>*U4նSN!??qqq8x ;N?5kbbbg̜9Se>&LÇ)am4 qi{] <z™3gPXX{oƉܹ'N Uu`ooJ8t<</,,1sss}'ܜb'̘X,fXa`Vb=z`:ub>+,,TJ.{Xrr9۷ߟ2m6s{nfooD"swwg?]&Yf1sssfnnfϞT\턫|j3>zcSU|orm<__YYY1}}}qFfnnƍWUUsϯ6n8ۉu ֢ɬ@jj*.\Cj9rC $''M+Wh<_AϧCO%uÒuX<:q1%fy z8A痲V Q\xO1] cCܹSdffҲV%B!u놑#G*4m5jURU~ ooo ** XjF3g(CFFuS&Pm000@߾} [[[,YżxC?~<|DDD4o6*cƌӧOk׮իJJ ]Y7._ׯ+wv]cժU [DD~HMM6 É'c_aۼy3VZpdddرcx"|Ñ#GÇcټ###W^ŕ+Wp},_\SU>†>3gT ?>COs lȔdҷW3f$zޫW/c۷ciiiRc':tHfobv0{Q- 9z~!&Tڗr֌IZĮQl!tؔ^3gO1Dld=^ޭ[8ǀՙ3cRzzzTɓ'ѣ_f!!!msaݻÃikks!cUVV'N0___6yd?ךL&1ɶ_6\ސ~1:t<==Yqq1ٸqyqdу}g̙3ѣGٳg>C#EEE~c PX[r2_c6mb|A$3tQ={*~);wj#{wVVVJ}Q]}}}o[[[→<UE&?{JܺuCL>嫫U3#W^a"999+V({ҫ{~jM 髹OG?,55޳^?_}\m=]ݹsM6?eeeO?eVVVҥK:u1ÇĤт̪ϓ̴FjnŮ5U?u\z{ƆvҾfL䣦lڻLOLl"`ŎLwemr=z0Hönݪ9† V^ͲÇ%fǏWJsm{ko1Ƙqi xC̛7o߬_~LOObo~UKKK7zzz-]T16233>Ϟ=c,--1ؽ{y`1VURR@EWW HUz==7h5mmmsaÆΝ;zɕ?]$)S\\: !eVԥ]rK-\\ΏO _ؐQW`X|o|4]ttt`oo7ѣzzlJJ \]]q \| [~jL9<}4u-@ QWmg.l++ߕ^8h1]s|iz2M9fT2>8;;… Pm Ӷn:'r9z-N?8p ?ŋ\ݻ*p႒7fR&PmھKuNyf?SlzBtt4QPPM65o6###++--Q)CEEE}6/_]]:yaݻC GFbbb1==].](~wS/ڵwݑ4ٳg 7nh`!77W>D[OOOi׫u4:tW^y6m©S7 m .K6oʷ]xҲFѧZA 2 C!//eeesn]vGF||<̔Xp!Ν;LgbѢEyJQ DQ1CҵrLis+bIAuLju}bd=\TW!EZOb/&JBY%T2@ʘpl)dJbØ1cScQpq;&O3fնhzyy᯿Bii)RSSzjcٲe~hll ---@AKK ZZZ5c̘1ՌKm.m1cMݻ3gƏϫ!$$FPPs̈#pQd2B.7o6CbŐJ(//GFF,Y;eeeHIIADDVXx/СCXtE~ҥu!33YYY ĉ6???ŋ9OeښD"fmmӧOyO#D"ٳ'5k,))I)MRRstt=] |'344ԺzdbbeI 60ZWO| gzbW_ePkݻk:vȬXttƦ?~Ӈ BfkkۧV\B6c ~3fPZ199kL$1vzMݳgbZZZJvU'N`ؘ=zE?F~'~O2إKX޽@ Pɿ!>ҷU|C^ieSWq?\eXoG^߃[|9bccI65͝kJƽ b9d6ʹYϟG|||W 4NKh[l $SA" 33A}O]ӜÇ8pZ k{\ ԩSqt4P(6IO$[F-.li6-bm۶mغuk 0Ŕ)S_˖-øqH AA&ѢH)So1l7oބD"QM899>(W ń O?JOA;d!00j/pss! kb˖-$um4d5 fffDpvvƁIIIpuuxsٵ PYY8fee%>|+7۠o ƍ5߼y{x~prrBXX5v={:t(Ҕ|BhII >cXXX@,7{@pbXXX`ܹ(--]clذ000P MNAAf3q1۷;vArr2ܔʕ+$N4~x!-- Ν;xҫGFF>>>BFFVZQF̙3Ji,--A}%䄛7oK߸qQ̲2bҥ:tƎ#G 77Çٳ6Ƙʑҕ+W"==W^իW/$6tӧq5\zRB`Wɓ'O?!--MAȣG0}tڵ{򤠠@U@@ $N$p \|yyy0`:fϕ^}xoߎ;wbpuu1i$v-鲷o|Ν;5rP\A`ff;;;as cܹ8w{ņ  H`ii 6aUVƗ 6K.H$ذavޭvkC,ĉ:KX IDATj)SW  25ɓ1g vvvH$J7MD"1UJ#F )) 6m/L7>^f͖d޸q'Oİa0l0~8֩Dw^^W^Frr2 i&7y9q+*qq,^5kbbb{n|Ji.\ggg6-+++r߿_d@.[ng>}@w닏|}} Ƙ_,--R@WmhbwE~#  .M-,\ΝL&ٳgh"Ŵ;Bs: ōȈ#pQd2B.Ĥ9fԻurWe766R;҂Vc̘1m7[:z[oaذa8{lY!C l𱊊`nn}}} aĉ BVV222`_՝Ldee!(('NXZ~ANlܸq˖-c}eMkWꫯavvv,**4SC'>b6w\faa,,,X@@c1XBBsssc"0v&/\.gfff͛*?ES7=_}D"a]ve 7n`L&=&ﳗ^zI^bSNq-UvjT?p{79sqq֧O& -۷oRZfnnLMMƍy筩k}hh(311abbSqK/77`vvvhԹ{7ns^:{u8vb?p˗/Gll,_,qs!4D"c-5k󈏏oU{xx`РAXx1q&iKAe,XJ A-.(((w>SNw`B$=A=^ Z TyAYmۆ[RIDL  4\@6jrr2UAUwKпf;ο5BiKAV1]V*B*6@666͚k4#ȏ Җ  u9t\G*II"4S'm P:hXz5<==\<==f`aaarlz[o\k:t !ʐGXXX+[XXPTTDM_|={ω cUhگ ј!AAfTRؚ6&Nرcѹsg +VPD###aooP{{{DGG79?|C1KO&DB8?FYxDV\ >͇+y`VEƱkˇZ TZHDJJ NnݺA(O>8x UL&C`` lll [9]ǎpqqA\\\jJǏc֬Y.0k,<~XegbbҮe700ϟ\:qmՏ_W|9sf)gK֭[4hPh׈Jc]ChA&[{&88^^^z*>}HRxbTHHDEEaV  ǧ3/\#ý320͝pT)*VOm ݶWK70M_bMgdTyOkV.{kQQ?ŢE߽{F^pb޽oygHMMũS8ASNK 2d֮]-[4N.ig>fO%ؼ=kCW$@m|~_|O1X=XZt]; ?Ų&ڞWߗQF "8p#)) uڗ,Y333*Kb޼yXx1 //:tH1ҩ}bǎow6ZZT>TIIIMچ +VZ+++BXYYaʕj=N||}׮]իW!J1ټy3VZpdddرcxb׮]õkp}ZAW/矑 mmm|ۇ'N ''cǎZȑ#1{lJUK.\#T\3V\t\zW^/R=dS\C7nòeX߾}YSvQ/&Ͽ~fnnn߾cLKK2g1VZZ:tЬEfL"b'vuVÉzl"ISwݜ%1e#݄L[5U=hہ*?~QKlY`Gu"у}g̙3ѣGٳgRzzzL.C&Hؙ3gj=:0OOOvuV\\o6n8pclΜ9ۛݽ{?yxx0mmmcĉחM<ϵ#ɘ^=/jåӧOY|,??Ʊ"""Zio~fА`&MR{u^ܸ೟O1v=fnnUW;w={*~ڲ]q_Lokk[z GGGv]שּׁ,fmm;=\.g'L^]}^LU>>*>|_|jo=֭[omhϨsϯ6n8ۉu ֢˚`ȑk+iKyy9ϟO?ppp)ccc?}A>fLf!l:`bL;i͊n[ Os\;n ~:xiAM餅EػնׇdffO>$ ߯q?H$5ɓ1g Acco;Fn_*} 00زeoϺZFXX؈[Z9sZ9s&Ν;7?k,ɓ'*N¯kxxx@"`ԩCꛊ%ug.>1\~YQϽW cY)d:;;v:u !666"<<ܪm8 s+$ihzOD!/,~Ds]Pm&~4Wcw5( Eee%ڠjQRR2S,!766"77wȺ;>þ˳ɓ'c׮]8{,]/x'pB444 ;; 8p6m2xϦ&g^cc.ǎë d2#//O&\YfTWtܴ0TVV"==*ǀӧƍt:L>}@gg.0"HZ6x|}vZm<c0u")) Vٳg!QXR&M4ZWKc.>1\~YѮg ip~c9$3.. j~~7n; ""˗/G]] SSSm68q}}}8~8oߎ4ؕYߩϛ]^r_RڔmحEš_CA\.˯_AC'7 }* 2=5ɢrKʕ+p@ÇN vBd*qF[_}t:Μ9͛7Yhlm{,QTT?pww7HT T:*$$$3t1HKPոqj5^z%DEEߣJ#Oad~Gvv6T*nܸF_|Ocwuu޽{uuuuNݍYYY+? 6oyf~i3< 6:o~cq,i=ɓ'k?%!1ci#׽cիþ1!G[Y]]-,X &M$Eyyuָ] z{{BݻW WWW$*** k2S?>A@-kkw~.? @0d[MOI!No,}r{Mw*&Mp&+D_E~f̘!n*fΜ)fΜ)DrrB!ELLpss">>^>}/DGGLjV9sWWW'E__߰6cy&JR# ݭFr+Ϸ OO!mϾ%!sDPPЈ׵i4n:+d2Dww5K.{U%7mMp<(y{^y%.ҥKo6n85RԠ.{n!f̘!RSSիW 8p@̟?_L:UJob˖-ڵkV"*&N(͛'"Ν+$A%7?S4Ǝ˒4G:>>W^iii[xzz?OC^c28d~M$d/ܐ-;T*qiL}gt'f?h_uuWs8y$TcѢEv~ii)233lS\.unr8LFKtLj_Ǝ!ΰ'rVvRRRͷ2ud2\zr$o)(gM0wض(++/&DcC'N2ɡ4 Cjkkci[p9{_1>܏p0e[ZZi{Έ1#11DDDS.RRn1f<&ƖhaT?*`̈yL-Yx5)CO4cٲeNe˖HDDD8$cQTTBk{aa! 100$"""$z:;;YfA&!,, |G˄rC&!88 8SMM boСCE@@9bCBBrS"S3 ['Oߏ͛7;""qnar -- 9995k˘?>GlJyyyx1o<|gHNN'obi Řgmm-sMB^jvz)bڵM;䦹[ 9rxGtGñcdžMMfNN222 ᡇG}2bϞ=ؽ{7bbb0m4,^JKK+i\[[777DDD6gkk+s͕KRH$ sppϟ7Ntt4Z[[EEE7o}̙ 77wHW8ɴ2e xL8gƋ/WrhkkCLLsK,0N+V@rr2pec6gww7|||sM#11IIIFΝ;?ؠ4 sAs311~~~(.. hllLx":{.]ѣGсLZVjlZZ /_fpl'Dv:u xGo|8wۇ#33hhh5kdQZZ_}m{zz~0xNa\"""$jNr7s޽{T*9ZFL>===_LƩx\*S.97WTy]]bccڊZ k׮E@@=jC.37픛w&?[`` 222e'Ň>F38Vӵk{{{ BCCQYY6hZ 55@SSo Ƴ>j8p6m2Ԅ榃fFF:;;Q[[kuXd w@DDDdƍn:|Wt8s 6oތ+VpHMMŶmp ؾ};^p\pYYY*66F__t: NBBGce1WT;nJJ*$$$07<7]]]w^lݺuHىb+رcE9FDDDc$%%.]“O> J\+WW_oʕ+qE_jɁB_|Y'K̘1Znj#99aaah4HOOǮ]P(0e,X.WMLLDFFkZʠP(0aTTT@KK˸;=ޑroI.\_WH$pssx ?~;߲qDDD4FIBB رJOKc}gĘY\.GwwS'OD]]S8>>-Bvv6Fiؖ"33<ȁZvIIIA7bidM&ݻOҜE}}=o{7QVV &I&L[[@DD4ƍtٖDFFڭ޾3b̈yL-˪T*T*( wF1%""y{J?3bcKDDDmM=Xlӵ{ٲe(((91N2Ƙ~^XXB p 8ɴD2쏇G˄rC&!884TSS(X+?t|}}R1IDAT#G}DEE09)bsK.E||Crr2<== P(L?kkkdo<-- B`PFSOk׮en$""OZ{ݻiӦaxPZZX1Nsmmmڼz7W.J!H088A?`;hmmen$"""N2͛xPXXmmm1xnɒ%iŊHNNFWW._b߿or$&&")) h4عs'Fan$"""N2 gҥK9R&hZiii///\|a$ q)GŇ~8fo˗ܹsطoߏLDGGk֬an$"""N2mNyL[>}:zzz@zŋ0Ns= rTUUټr|)迹V`ժUXv-pQ?r9s8ɴ\Yf+d!666"<SLx[l1$""""""&DDDDDDdбcX" (+:urŶmcgii3_Q%$$ndduͦ >2rt솨(_uW""&vѣy3o(B%7Ŕs RpED7&N[oo8DDDDdl*ŵ(.EX&L茷yhVO?=̣R ֡%L? ]cE-R%"""V\Rv?8sRRJ/^N݉cǦ[ĉB̞%%l (oek!+YmdS7UUJ;WuұfMfѣyش) wbWq=!?ZqLLd&FautOEm S7E]*5D׺MG6]\dƲec=۸1M4P[?];9zĎٯj*Y6XPPP͚Æu?LYR*댃'By:tU(+~~8cbe34NMGl1)R){wmɓ˫F] ~~Sa.JugܱFZӄ xqŋ6w^^5niqضm,l9͛#15tDD7`>eӒv-qlݚ|\RJ<< k[o툙3٤1ֵ֮-m^1d>x ~!g!?^^n~|ڿȎM'Ǖ6XtB'ZVi=.*ŋ/9:ǬH})oG ApݵOjj).~lq\E֭ض-˗!_AYYZ۷wnseMY]_Taa {L~J5MKoTUKRlfDUg$ ,Y2ODDԊZmds47Ծ5:**xݓhN`@t*._nX$ Q焒Zۗ>8* _GkF/+x#(.nSУ'T*ӧ{4gewOw V *믙?8?`wAŵX: !!X) | 9A/1kSkCsw-n^85E :=$(7$cuy$Y[RWK륰w aa0hzu>f:VNÒ%,_DDDdGfMΕRaCss熷2M/ep2hY!ܟĉB96lu?G??guW0 ɓDQɓ8x07诿.ԩ"(_|-pphh[o!CcҤх 2`A_X܆2[-C.[IpazL'.]Qڶ zja m8p W֍fY>^!J%شI= B!7xVo7Kj6zԠ}{gY3BSggr[C~ODDԊvmn?WLS'I{≞ưv Tlz:vtkφuk]Ν39>d/dXZԆ֥fuχ1m[zݵ{'r)ѥ[.֒6-_/=S70cFW_-&˥X(?vUhNƍ 4|\\f喎Z/69v@뱾Y 866EmhKfbϞ3S;|nZKZO]ќPeW&tIIw#.n*~Ғ6-_/uXeQe2^^ q7{BѱKjԡkW7FHGirr* 7%BsRSK[ԆKj9?w66p`krԴZPh:ӪmcKNzzQK6 ]wwj-ջj.{}LKF 4inZӣĚ5騫SPRO`ҨV sv'UUVSy>ZeeuFϥY} lۑHp܇ݦä,-^QQע6]n63wuBZOI{[-0RwZ߻~g """Olk'0%% HK+Àǔ}8C6gzO58鉽{X>JB!'bŰVJ%]sg4ҧדfVV9diEEz/mzy1'pd־h믋FV'HI)isg7ygֲ;.">UgO:z>elFz=EFj=޷uSRJѳ nu+wMGIoԶm>a&ԴiIͪU ^Q5y uW]cZ"%}wo_1b6L`w'zkZ/L:\:}ϯ I]\j}NZ\X ɚet}oS>a&TDm5*+9s7~%5SҥJ^3hM1~|gew7EiiJ $$W//;/!!SXX{ك;. ~k'L&#hGb7nA~~5jj8x f >fek_nvn~6{8#僑K3SPPŋcnꁷdW!6/vHi^3gv䓽yyew Gw\.i=<<w׋/әT8yᾕVӌ$u^O~8wݵKŬr9O>9޲eZ3 qm&{k_wټƼah~{'7.6Fvu `wh"11a쳡v;ixx;23˱fM]SĻFc@_?f>5gN_O);wᇻ<׭^} ١:﬩Q}+IW7#!7Ǒ#y| u# Cԩzt;/b 8qyyըV]`w 3g"$OҹtO;F]:fH[-[#% 9c`?̙3ٌ= Fu}"C.2 O[x=cӦ_c;V=zx0cF<= z=]ѣ':qq()QHۧ7 ǰa4mXE'e%""I&1U$'' "qF Q+kz8/#"""k`}K r^FKDDDDDDdlM""""""bIDDDDDDdIdx""""pdlM""""""bIDDDDDDdlM"""""""&DDDDDDdl1$""""""&DDDDDDd&1$""""""&DDDDDDDL6&1$""""""&DDDDDDDL6&1$""""""bIDDDDDDL6 DDbWeJKK777U3mӴ\PN{W|]$ޚGdfӢEpZ lH lBG }Qlڴ *O< mm۞_/m>@DDDM2QTTˢrɶ.d[""""jCfee%@i=@DDDrJ <h׮ +VhFkkk!Hd?㑕Gb􄟟ͱ+++!H다$L0ڵ3-[4[<#ܹ3r91{l&Xn٣_~%$ .^@wh\:`̙8{V2&HSN!""`9ZVl\L1nSΩ9mWoΝ 0yd$%%]הz6^w֬Yh߾=<<<kcލdN5>VRRXyiH$jڳgO>?V_[ަ tTֽ ӦM nbbpwwǏ*J!Jƍ"99Y!/x@|q bZFqqqTc ! B2LDFF;wrq).d2 ʕ+"((HtIرC{.]oooq9as6ϟ~~~"00PݻW#G] iw_9R,YD|zӒ2g[ce2^ڦ2Kϩ9m'B.0+JKK_%T**9th߾8pb[דcvm6ѷO}e7>`N{Zf"""AII()){5_6ӦMA"{OѳOhdS!֮]?YgU\xQ,33Saaa:޲ef˗5˷mۦGgJvhZυ  bݺuZۮ^Zsν&?.UVi-_f z!e2xwL>%medN,M69ݽ+k-_rN̩uyffprrÇ(l ~7^`^L)3M+'5O `fT*!JuYV?@,H$:gffjŋ 5!5<<\B#ݻ_d[npu0X%6-944眚vAAAt .z֗ʕ+-Caɒw,}і?%LDDDdS;ٔLcHNNFUuGEbƍ&]~['a 6l1x`8p@k"R+Vয়~¹sPRRB9F}4tlS?f8;; zuvvFMM:h~jvԷM}Y^WWGGG899i&1TcDz,ٶP*%ԜsrrBmm+)twwGEE d2}0w{Kϒ2Py>DDDDmYii) !!m"##111=g8;mqq1.\T,j=xb<ݻQ\\:G*ծ~$DMY^^V/$o5,,P̩,92KNLg}) xCjׄ-lٯntl !ڵ UUUP8WWWopzj/gW.]z[Yc[[˖}Z8Գ|ƯezMX5VGa/1b|رc>C:Ͳui%ִk.{ 46'OlذAky\\t邧~ &ν;4~Vo+klkztMÇk-߱cGy-oͲ,899aȐ!e:t}/Q6[[_Zc6cǎ GGG1o<ӧOرcB!Ə/?XTVVu։)SN: [ĉOM"Dttؽ{'NB.X`"6m$DllիH$Zh rrr "((Hki1[V-֒zY:A9Ԝ۷oJw"!!A۷~ \sꙙ)|||DHHHII_YoΜ9Xh(++ǎFnnnFh'W:t(++3A߶MoKsF ȦG",,L^AA =z"##C3Fۋ~X?P &gϞ&MI 4Hر}^|Y֨kzz~- ,GII-  rǀ>03۷MMcT˫޽weY+oPp5qDSxᅣؼs !- 6dzM|z RRJup.T`˖lDDxχ!0еU#7J*VaD|PmrLZBZuѹ5Z+l:zRPP| dgWر|i,Y2*˫ƥK}!R IDAT:Yt↗^h?8… Zu?xW<=F lUw0aBg̛= II%dqNwaӦѱK/>sZR bb0cFWFsᇧ^ ̙7~m /9+*黐Y qCaб $uqc֮MGE9s6n s5&uر!eqjT'vѐ?`̘NtxH̚YgT.l( 򃻻0p/VI-:*uS(^xjkU%KGO89Ю'vƍgOO@ii/?ah1c:?{Cggd aCCr[W^9w """""8r=zxj58Q{Ͳ:  Щ+gwߥ̙bTW+ѭUuk6~!II%==1aBg '[CՏ@^z=o^8v츀pO9p WS^3$J}k%+7Pkϋ8^]oǴizswwҥQx#:n_ 6;v\@VV9TpŨQxuyǾ}5}?blq I\ii"#O/lt;/?MP]ɓwZToku38}*@T/^|~JDDDDdfКlH6dzuHٳ׿␚ZZ}Y|e?%K!avviI8) >]o7Uggk5+R`e2 f olrΝ5uWu þ}tbHK+Z]~8;/asi]~-Ki}a`ȑZ< S:6ni?(ŁxSiO.ۃ͛ZDDDDte5ؾ^|2{f}+NiXOOdžAO"pd?> KFif<ِ '%OԉT*Fi8p`2,w= V xoy3fߧ"(y3ؼԷwI3S7wީN^/]Im!>>_9Dъ V>+!1n9rnI?nZW^9%K̙cЩf_|w@""""l 3'*NI)Ae:0@GJiӂ=57ѧ3Я:yZ6jNsgz#+7f,ׂ}peǎ1u"0jT'L3+ 6?N. TڼWktrgㄲ:<|_<\_Mٓc4nkkN^Wq@3RiI[ҏ+/W`ƌ^ó.ͦuBd/fa:XmǏ$ɓ4r={ʔ 'h FcŊDlؐ:s m`.:FSYmpV{@Os^=BgB!4Vۼ./O ݻrܸ692}/*HDDDDl/hl$]^ivqq MZ:a9?5 ה)AHM[EF6$@~LDDDDdiHJJβRߍ/{ l!LO/.)q>4bb!:zfR@ɓ~fM(XcJڵ'vyOp_4W0vv|i=[ܽj$:wvT*1z -4GO?i.n>kiDDDDDL6-}Mf_.]gٖ-57~zש~}$'`\ NbކYG{=th Ovv-;n4=wNhDDx[;ڼ݇\_~J׻^QQ-^~9iieD|YsFWo6eHӶ%QQ1Bf5?0 _P|YgzӏlZ@{r #RgMF~~547+W+/Wʕj|Yl~I$nƣ@\\>*+Q |] ^~9^k%$ -Wᡇѣy(/WB\RRvv„X./VVZ:U/Lh{u~LDDDDd/$*QU]QظqI60ŒhMp87 SI wN]oѢ7Sti^}:u ?[d<|{s))XKq=W mi*9 ;yr:tpƮ]g-ŵMZc¾4Ϭ2ޖ.]JKWQ&$$Mdd$ &&FYb,LS[ģ@.n$%C"wo/<`w͒%իӐQgg[;jMTw8qNسg"֭KǾ}VZ!.GH;c̮`iغ5((<;ƒvo(У'm-[?/j'ƤI]0iRYOL Bm ~99UӧwOT*3EKVC_|1 Nal>|.U2:sg7 #Ft}ZZoK1=xd^jԦћȪ~vddGsD>qklJٜ /Ŵi!2rRRJd4wLk;xM`وB!pTJKxq,RSKQUāxf̵,3ftLWIlTW+ ^nPr>NfbȀV/sV: *(E`hVT<>;c۱~zK.-{bbh-8qqt?_֭w>>ѡC' <‚6?-{ )bbئ"qdI+˻/࣏>謗tUo=~XU3^{N4O'\k. +~Gxyyy7LZOݦt|ndɑMFFFFo@d2b„Oꫯ]v<B&sYmfM{6ﻸ_xWp!9äm*vcddd&Ml2222 놔+ѹsW$'A׮Zٳ#GA``0ƍC6JuU䡨7t3||M*φ ":z(0oރˆs`'F׮ͮN%t^g = 0oC˻ >nޠvWnJ 99>>;v#|MX.7AA#0|tЬwyDG՜OCi wS 0q Amvۍ#L69hHӧnՑMC߻/TVVbYYo=xb'HI97|ݻ :zb_z~ kgǎXf~e-NN@nݰx`ޝm#58~8}9ҿ۱F|7mp?{Jڷ-z{_yTii) !!m"##111=g8psiѾOGHRxyy!*j>}qk+_Sѳgw8ut 6>1c&XW^zA:멏9ϟ]yspmh-W'{e2gddlÑM~h>A2eh0\PA"7W>ݻYK3AtcTY{?5W/1onqՄμ[Z?3[u&oXZXEbb'N_lv™3gM#yg}v!jkk5i~?0 źњ Ɖ'T*6iy㌌dD^s '2l}ѢdkHLLBee%~/;kt,]::TWWܹ4%]c/i7jQ]]} AT")) o-of/!qI}ZrܦqΜoeVTUU4wkFmm +Ԣ/ZzMo7FFFF&L6p2228DFk^z5m۶ tO ||:cǮ'eĈqa8|F݊_|=4BC{cѢc%1B[\r@Cg_bc5LkzuP>|]wM3<'n26:cuמ[=xpSN݌F|a2֫T/H0l(tŜ97^#Mnc~W0aT̘q^-~Mg_ZMcϞ=W୷EHHODD W_}Ue˖#47f̸Fg8Ǝo@Jg.7VhRHSReLeJyyA:e U7g*r2\%S Ĭ! JSRѲ[}nb6WguKPh`p7V`JWϹyT'pE%)'M*tyJ,SzkJCv M~`addddl#/ʠm'΄p橓eF} ݤx)O7cK0]2GWګ׽[\Fˤo_K vJo Ax_ e{e'B]A2<Eemn_/]6ui3d^W# T<x csT!p* )P2<)yJ VO8JM}ˍ퇨t$ضNwE*^R3aD3럨)Tڙ@_ ˜4TLVby%,CqM D0ޖmn_z;IP?Ъ\gN@U j'T$8g4p3>RJ%pj'F6]$Ar h'\0CԶ j FFFFF6S q@)i ai\eJyI&,_o;0:4ZmySʮMW3TeOAM)`gu?)2JTCrDV{1AP<&#####]Em*** sm@R@R_P]gaE)YХ2ڗ9iDZ%PhdJ'I 4ۖmn_:KA~OkJ*T D9Kbq0ѐL_BRRhgIkICԶ M~`addddlۉ@dJ$5m'&g++*ȡ4hJOW"xziW8-[lh/ö +()ǸC IDAT͖I߾ ǐg.s{^amzK~qws*Dl>-J,T[q{-i4H_ 9D\%*dbc ҕBכ;x [UHW%Fry)Nը*vuZ!yi?6鎩"99UU7^+*J/bddddlKq:*p!ܾ~be#^RtKpJ`y<يˑf{{_u/--$$$Mdd$ &&z&?02222{u{%oE'0Sb%E]B /3%RqzFv ]ty*ӵJ.0G 6,AF?/3%7w M~Cض9P&A&2g2Hy/dl "[e =d ~CF6~ah&M+l*wn G$=#<LDHò9&&&B8yM&{ JR}B,+lZ:uy#BV6spQύXsVkڼc+מmcھl~uJvR^ڗ+XD~UIq:0S8a䚳\\?qӚDDDD7tYSo6l2389s{gzkVo[}\pW&8~GcTlJ-BP _&Z~]뢵aMbFra#'*‴"ZT Ŷ=υ2eҷ/SӘzk<3/IwA_CQr)>ٽ]߲*<#*_E#}82,OEuiQXu: veYr<rB~*Wsm]Ru)\(AWOg|7!2nZ۟/nZkA_1!A|yהB ;"y5őMDzsg(T맆#( {ԗ^~8:DګJ9}q2urbF] Xvku~>=I߾L)OceB?"o" e #>&0Xv}˞۝{z"HL捅[@X1+]Չ}6;O+.r\['Cj|S:ktww@Sd?ߝepyS;e4)qI,#pT0ۑ0W&>wD"""t$ֹ6r ꈣ9O料\فqrus+Hpvbv/?kLeNy,9?ʰ(:nrdSy*9Z0Xv}ˎT?{wTItZҲ o"0*ҢQqpm7dGDA˾,e@[ &m4i4<}^r{{S{.&wom3GT9_O?xh1478VHR2kOJm~re@5J9v F mYڶ<G셉W޳9c?W_D""""&1 p (dW;]W.hnSCeRHp.u{b,kZ%!owbgqׇkPwJmzLN5TDJ W1UwۦfLTGrI2f|sk)е"1l6pb.Tٝ׵M,O(\Y֯;1w[)NZvDkgNTX_L:*+kWY 73THU+q\-`q3>#wJ10K:쵣Iq T}v$wds0V;0r\:)ru4ƉJ9Kf'_ 9\qHDDDD3f8yvZSvƵُڧ@%aq]$WK`h${c{E}"\p޾=_ysv)"W_E=KymD>zW::"^Yv6]2<:9[K1J9 ĵ H(u~o+óqʣx.$z&) VvQ\JJȞ$~]zz7Gp[j1Ú,mWmSzu ߴߴÔ{;#sh>W Be@ߖkdf=)uC;E$"""b31䊃jXt%0~™Hi[F+`stB9)j2SX{וADDDt2[!ii=.LUx!oӻ4ɓ'c{A!5PT Mh-Gmt죱yWt6rRs*]n8 BF&.v?&da=W5ŪN&|v ^/*;3G 6S{ """JI )$z=e@nፑU;6|6$DDDD8WJ:~y~)L6  7>`$ """"9I`o"+H6Cں/B;Ƴ {ğ}އL47(/!tY%Dzh֟qg}~oKm2TtuS7Ci\L7(G %財:;`Ď3_N%-<]@}~:o(;"8(d3qxC;"r_*;:KWNgkx9}kGm{.g`,tNP]^^ց=8 3A=)}P&~& fġdXk kC013p""""jy&{b#žW.H}]R Cͼ.Mmƚ_H]ߞ/SR Y)LJW5 v_t nL*`3p,Jz{ |Q+.EJȳNC~WiUxk:%a>)Dɰָ_VdXk{s) vԪi蒨)t܊aqʎGo#]%Y>ň_Z_ [ 5䯬`am_F{ʎbGDg8msCl3@-p]R_81l{9J(f42{$l@$OwקoNDDDDL4/lzy}2wU34xtI;z|7t |PouF<3R|GiYɰָ.MN VVRU!OW)pk1"/#izx]UsMKl鳽RVÅ, {4Á!p_k-=b %gg/AeLѷ L5衍㻛p2l\'DZxjQ`nRq%bIlmJF.íaSN TC@}~R-,}P`]-cZD[} 9Q݉zZ?x rZ.Äx.Ngà5$&ǯmZ"7#}_TV -y&1Ѽ!k r~Sfȃ >O:5TV]U`[B_!PYa-3;\8nuBB4 +.wfFFWj}/MJxųphFKC^qȦ6[\Z;]Om.MUU t+Ms>Ɔ@*e>jUO$aQY*.;]>Iq~}o98`r\f..5h޴N.|VZߙ|Un-$"""W\8Y @M J^"yW5ØJ9b$"""""&G6(8IDDDDDD1ǑM""""""K|dS022222222222J.d'<###########M&L8p2dD2L6y322222222222dD&M&L8/d'<###########M&L8p2l\R(ٔBG&############G6lrd#L69ȑM&############G6lrd#L6/6>dD&M&L8/d'<###########M&L8p2$""""""bIDDDDDDL6&M""""""bIDDDDDDMRmXZZ..E‘M""""""bIDDDDDDL6&M""""""bIDDDDDDL6lM""""""bIDDDDDDdlM"""""""&DDDDDDdl1$""""""&DDDDDDdl1$""""""&DDDDDDd&5H7q߿2W۶11;g][r+ 95S#?ݶTw*+ꥍ_F_ܳg< #Fd{;!)Iw7m*/M&;zp啩uY_wB}4ÓO^+L[-"lp6q)?= YLnǢgZ;"#d2[TLCf6F|AlzgڐB>]ѷoϟJ>V:',]JYx+Ѭ:&p Llr|Pݻur}$L w2Yl-gz׮<waÞ?4['x^A̞p}pDDDt%>s:5>ʬ(+bֳaHWqgP V`#kѭ[j? Ͽ++ذ MD7? ~7܀v>SO߄jgBAA *w'׆źi8~^}uϲasxAS1c+vgNW5kJ%?[sBS'/ɓ`,]:͛kbr #4իK?n%|sFB z=# v?5dxۼܧ-[qm<ߍ.F_I~l܆UnB=4Rwƽ,͉0xQTt~i M8{ֆ~sCTCj4 so߭XjI}϶x챭v"!A>ow݄=6.jGpϳP$y5)qR(d5vMW^ \NIIf݅-gðgx|0ddh ;=Go}h;scG.Ə7=n1سg< &uF@nxꩫ= (~ #GZ@.?C#55emgϳ/8 q~lθ瞎ǰCuE^^ѰgOg 1QkSOI#|N,Yr,~ݿ_t5T*9f8ǻ{ҘHMc,]ӧ]jP%DDDX4{6﹧#|sħ ֕A.[ qѣGZ, @1tIaS;پdN N~}m&^ŲigϳS*=!;wNiۡC\1KX} IDAT{{QRRU:1F#G_⊔z˼u'j8Dzn+гg:v82+ΞE 'A t횂$fMĔ&5jdGwطπy!/o ޽hX,$$ϻWVڛKdM4[,?vќۃe୷xMMJjz2p`֮-Ŗ-p~Y= ZmNDDD1ycTJJƍK/Ê7b۶q::&u]k>{h}3'}QVfEnŀ /o%:ڴQs62kL}I /F6e>^s&G_8teޏ!N`!sOfG ٣LԷor=ɧOWaݺ2OKhfk!&LX}ܟ׬>zsXB= ΂oE˖k߾_}usLϿ׬M~55.TTT{Fɚj<֯_sϿ~g12Ͻ'"#zX#̙BsղCbیW80ELCmZ޽O6uؼ}fm232EL6fUUT`Y**۵J~df{=o!XNTTT7ۣcGi+O pT%v5OڎU+R<*5ٳ!l6'>֬)Az:瞎? V: ͉M{=#bGz={yhQTV:PRR9s~ڵ oҤNQڏOډ_~1㩧ʁ->CC}=g͉o)ƛo+*2Fݗ? z~[+By f R'|f8\s} 5X{9 'i߱CUW$DDDDl-ڬߧ7.]҆;. kG{NWɓ z]F Uvb£AHqq%M{9W࣏ Ԏp߶G%J ?/X=`_}tKKݞ7&O䳬 >ςvO?<3lQm[[rK[@v7cƌ~7vl[xck<=3"4HM ݺz֬>x>xw :$ώ134wO}uY;gcU))e|st萄/¶mgq ݺ⮻:"77;>7.55.~ZѲeKBDDDԘD0mʱf=u} xDV ;$@ ? z&`p%=x_>Xl/P\ |HUUI6kf@Q G[FG߾&|K\.paX RS hӧ~u wY- HJc/HLNO]no8:qM*dgמRq*8h=¤I2@##À-ؿ`^&둚jر:>v;0mgUW)P\ #ZUx nwOCj;qb^xAUm\ > ˗'a' 2IUAcƘ 'T̘=X OCq:7g< 4:%1'?_n77]ɦ`ԩSLS`0]B Lo~DbB{챨ʼ&})=Έˬ{R2eK#+VH`ՎJa!??邶7}ӧD++KVoVS=W5bN`N&7݄8&tZ0}'Ob͚$0loκx*,Ybǽa8dz0~=z(Ii;׊8IDDDDqlٲ#+˷y͚IЙ3Nܚ7,/}w╞.ipJ{Ϟu_өE :/!̔^SVY-##UyDQ| Q7rO>ih]o7{x!5K,@Hјo5Lb].?Af CqDDDDt'?\G7iܗTʣhuD]L~chhg̔:wod}ipa#uS]ށ>H;$x@eZ ^Gqg 7 vD[oY^^^Te9mTT|;1!yb0m}PCi{ҽ?oM:g--uٿ˧G47LԷ|}q'q^W.0l 4bD f]J;S۲ee^JLDDDD#77)))ɓ'_~}ߟiiIQmysa?% kV(VJ֯- j=4_rN{w(mJTYYJǺ5z~0jTi 7 &MK?Ż/]\`Hj6nLFܷω%K{Z~YJ2;v/ZrDDDDty'a0 󑚚qy 6UU$9O?] 9v”1c$`\,Љ^!p,DǨ]0bCV `BV˗'_ў9spm*|q5ӟ;h\j&S6m 2Ąd$RN7&gO 0];NpaD:趯6mdZ3t1Pn{ݣkIt?%$l)Ö-ɸzƏ 5U[o`p%nMsJZ0p mX(3RMMdВ%Z̚^!-M1c,S͛ѻ"u-c x)R8`۶%c`%ӂd=F0Y3VbD3 (sRo$"""C6\qAXmVK.m:?}I%}/r{5a^'MA6R^\BNFɜ3Lo;ydl/(D&J 3/;{ t^Lm-+(pB.H\._u%Bqd"v /cd\A&’%ZӈlEf޼D\{'J3u'cl^.K{eؽ;*"""""@DDDDDDL6&1٤@d2}ȏn86_\:Ԍ8=L.ƾf6={Х]ѳ fszMjBx 7:`A"**մi9xiqӦY|-HJ#9YMK{z{ykȉqCFZ""""&i11v,[yٳ 8h4g\Vc;~;5zRo_~[5,אW_!/ς(.NEaa d2&L`޼VPٶкց%hf˙ZT3f~0sJ;!^vᩧ9R9s⑖&Cǎr|aڶc{DzLĘ1 yZ7:]fUtUf0zzh4zk·~ tyr9p cZjFG> G]Jha@Rxq$j^'d2=owzv{w !q/˯J,\Ѻ^|ъ#G3'4nu """ ٘[rE.]E':C@' (V EvΝ BЉ|g}@']r.w٫Q\Y#Fر!vܰt[%%.Ѣ^kǎ9EEK~Y:AWpioyyKdgDVzjԆum[HKӋ#GA]N n۷;^3fT @'Q{ε @'Ns\94"=QSw_]RN &$W @'&L0ݯz}9gT?*h;EzѺ^[gf[:D>_vtJ':u2xھf]crnЉÇ 3]o8꺑ԁ.}F1⟼<:;GtUth4ɦ^SL2eQ%3oYgЉܬXQYvS:1t)D-pdӝ}oHm>v}mOBDFFdС&O| H M$}nnlYm?I_9~۶9MvӧK[wI6-rRxﶟ|R4kHz^:sCEj^h4:q5&/|]W!(z6 =߮P DDDd&ɓJfitͺe?i9 (/w$Q ;Ƣ zUT֫T_.]:]za\_f}H;w\.iLΖ=y;11_ǎRN~_RSMR+ j,Ć vѡAWBx2ϺnѽJ)>FC( p'FN 3fhЧ"hY7:EQO$R^u(Z-/S& ^Gf w߱ho8X h_9~K=^',:}{6Џ[߾}[;QCD.$$߯Nt9|"%%0ydQGST~/@yso>{n{4 ^+=nڲNpAc@Sm%7}Ǣ;Vd>Rc'ڶ5 ijjx_j{Bpˌvhp_VRWƝ<ֽԸѣU5*s( ymWu&M!to9Ǹ2uگi(*J {p@DDDK'0 0(֮J!vv֭B҉k'θ^if'f1bI$&N@Fԉ2f!vp.] B >GǏ;Ez^kg9Źs.qm$>tR&7Ku;bs\"' uk*ѭAdi2j7ް*!ZgZI}< OC)_#.s5SңO^{VoGOM6sn0 V'ZЋ&fR;PWH\ _/=[?>gX)]RsEddHB˖z1ylڴti(t"9Y'F6}"3SJʫ#;߭K#gw;ZB%ցlK6ecnfE>t&?ZGK^jڄ5a^'MA6%.tdD ؑIa&S䷠M< RR뜺a\N.R׶޲'rˍ~EDDDM!8I~˗۱uk2\.` ƍSa-;&]Mm*ئh޼D\{'Z]*lj""""&\^}ב,"""K]@DDDDDDL6&]~8AP™Mq U֥)k6C` @| 6$#)?đMKشi9xiqӦY|-HJ#9YMK{TUhZl#F2c*{yy+d2}ЙεAyDDDdbG̚ekVcXXaǒ%vwj@߾ j,Ybwٛq}U,8PT@&a ͫh]݈+jGHW_iq,YR>N̞m {x*|.Mj/a'X>kR ̘,9SRMOvᩧ9R9s⑖&Cǎr|aڶc{D>f3ۉ=ZOwISTbU:e^š5I|SͦJ'Y3\=ڂd=4=5o}Gt]o\8رѧO2C)Q-Zǀ&,^89ٻ L۝2v5pi{C틚uNF0j))z4onwTl_J#HIѣ_?>:EMCcD **DP]+4YYTzdd0q;=lv;NaZZǨQfm7Fs(,t⦛Tή}k'Ǎ7PPD~ ի0x=RS ;ւ>v;0mgUW)P\ #ZUx n7iG!G81gNB:_h?"""b٤4ۂ35(-MÎ0Ə`GDedw[?Q\aJenzp۷'ĉTq3 g HCQQJ g߱jo qqR,/waJo 8u* 'xq ~ҳ ޫFiiMAf2<@^}q `Jb;,yʕvzk7aJ #6|{C?Nc^/'S1cc\ `?nIu߿YG?P'g`Cc5} T,_H:VӦY|W(t嗭eWg޲ @'֭ !<$ضQݛ6Ie ­w["XwO> Ez^:sCEj^h4:q5&ox@&L0 @'>طBa{oIC_"""h4F'ZgN]W)S`0 ̛nRё^㌸̺ ,lHߊ҈K~#.99rliO w߱ho( OF ׎޵o/O?mEYK94^};hz#FuIѷ_N`8#A>W1lԖӧ,Y:j%տӺzs 7:V&kj<5kPV°afQp֭z Kqq:5fY}% EDDDWI6-[Ҳpde6Y337ּTfyy賭?䧧bڇ7}Ǣp.3Z\qSV jx=VoBf .233}E Od=EE.O [8&=:߯-Z}։e]\>M#NyDQ| Q7rO>ih]o7{x!5Kc/lÌ"DDDD=GD9]f D4Od˔¯B{w, =z( ֯O“OjrI-݈Ej\]5wP ST>Z L@:΀){'G vt|Ay')QY?QSH6sss-ˋ3g|?VT|>z=iCe/;L۶rmc%X7u)1{vLDDDtYk,ƒ{bZbnhZ/T*ؾv{f|*a6 QP#FDbbTRKeB]J%xlǏ;Ez^kg9Źs.qmҬ~h :sevA:yTpʾmo,fmhٳЉo6S\b?\ZPXDf^n7څ"mzq+K!Z \'PO=%l~7wT ?B.׉+4B0XF\}QT:nY(:6QY)vѾA$$DA#u"|lDDDtF{I';Ř1fjNgV>LJBՉn0"ɑMJyFQQ2vBu8V*g?~UhF/J҇Pn{dS!|&z4 V'Z*=*jjbRPX !ıcNq=!eK<"w*gI$PAZT@W`ae-ʲm]]u/_ł%@T 顇BL2q$A5/f9> s#6o~ɶڤKDE$1Q'Çe~)$)/oUڵ3HTrlJPGpSU"""lF#̊n]{ݏֺ=>%ŬYV̟_duݧffoǏ'E ٢"ڶ5SHܙx'z""""kd 'OƎ]ZAC\D7-^R4h`-+NVDD˵?Q}TMlRp91eJ t:Ij.?vkر}{"\.W/F 'G$""""&t'plK]IHPb:TիM4`Ѣxg@X0lF%㽖FVkh1&&{w"U& """"""&DDDDDDdn< (HXz-p˙z>U*lْk;IDDDDu#DTN+XL:rǵjGb{'-*raڴR4on@tF f;#曍ѣaCCe=ڂF hHM5 ;ۂ<;(1٬ODRjmD套B_kڑccEKHdfFᇣcǺuk{u%oQl zT(v%ARa8 -UшkmX /`J-Μ nAN=Ndev̟._}KÆYtgqf\$ظ1Fc ֯O ;4#7ΎNDDDL65kll&sV̙1f@V~ڊAxXЦM{/-[F`F{Xe{ 1|Z-н{$΍Ոn=#SKr ġKH]Fb޼8\_RʎNDDDL6QZ\հ. nAb11zdfTJGr*[<ȑ$'Gn ]x4nl@B=zbE9RcSgƐ ej[.' ÄX= 9҂W6de٥ s MC)7qb 69~8CnOMyݶ. fFR%xQ_PRoVv z$%ѽ 3/ρ]n ӽZZС;:&E?Xv;0uj"QT>С̞C5~euS.?|Qx P/=Iy6~e۵S^@DDDd3 ?`֬;;a6 ׂ-[»nIzȂ?1EEIغ5VR֭yW/ubǎD<<@ >cHSERPPr}ĈfD:3c0a5W~tSqD2:ux4rNг |ckק`͚D^&9Z{ &P]uLnM5xR$$W 0R`?Cm*κ֩S$H x\믗cG#-w?);.Z>}׻v9*vr̟sB̘av֬R#33115 ƣx)FKDDD7X9f̘ ˲kTG/}L}H/_tIy8W-#>[[7uF%we1G ,Y̶x:)xQޟsz+-\~Ӟ5IWcΜXۗŋtߙzo6Z촁dsɒ%3f 0'OzFun?_+G|ݻ{ RRT? {P'-kE0Sn_y']֣gOSZ(ڈWLLz)Ͽ޿+|z%6._s`4rÇk~n{G =1`8 52Qou} IUfʇ+vNwxZvPbJ?ImD%}~ ALVaDdeEVNرa{:I@Ռ:[DDDDuZ 6:IF+d̍&)3XΞ]*fH~C$ ENZ'YYJfΝi 2d:AjeÇrK_ʪ= w)j;xCL{ӷ%"Be.L"_|aΝф?1̰aJeRZ*|ymf͔Wr?-wKҼ^nŢ暞n4측g0 Uֿ̱ {1.XD^xTL^O+|z^]gJmPb""G%2R'S&%%"}g֭ |GXez"juWfv(s(N/\GYYFGӫRvٲr|@DDDWezl8#̒htngc,.V4ЋV!CLRP 堻LD35U/K{<AUjY&-ZEIӦ fݡ`;rsmҥQtͲCҔİ<'2drƍߕ$e]H3r'N8e*ILltv`3d%"erFjuC̙cN*yr{Q*gH%޼T{e1E?)/ykg(v1fٽvYJ7[leUqȸqfIMKdN2xV:n_~iaÔ~lða}wfɦjĨ1r!Xˬhz+S 3ӄ}8~< -Z(kж:Ebī>d2ɓ'cG.B@]wb7Rɘֲ²|'""?Cխzlpd1/hX ֬cD\@^&ANCDDDd(0ߢEό-s,\φ!"""by_2޺Zw]HLTaDUi: IDAT6 7\#""""""&DDDDDDdn@ (H̎XzY[86l5j@l [$"!nQ&ՉS-((pb --A'N\qZeGf$$葘G&|Fe\6͛GzÆ~VTJg[63f"=݈(=Z4`ƌRDDDd^I^* DkɱǢѥK$23#ɱc:57(Cv={QT]R0n*ONthڵ6,X˗Srg 'V;1gNY\=Xpa9ϏٜEXF!~JDDDL6ǚ566]D瞳BfΌ,5+j9O[1hڴ{šelhsYa6O<@wܹqQ~`LR#4c̘֭a4 Qcy+WYf(JK64-HL#&FL>*[<ȑ$'Gn ]x4nl@B=zbEՉ}NTzپ1xCYwÉiz0!6VdFU/HmPVl7w:qbLSNńsryJm((pa03hȀ(ŋhÇjj?ނ˗;wN0mZ)Z0@#-̀__I YqmF$$葔G&,XPW./ρ]n ӽbZС;b?ĉ%hTٮTƏT] aSF-!EEI1:?iMagV=kUl=^Ou#DDDDbĨ1Ҷ]{i!rs8DDFkWl`Ed^|A"#ugt*]g.F .ٹ!+unbϞuIz4ʉN|%cǚW_`]C7w1C4mk;b2lhn݌Q`Rʦe@<\NU7t%.dD:3귮 `? qa ԧ׻dt2iSY3_7إeKQ})?b69eP:׿?`k89k}Vb%)h@s_ $5[;]ܿ>Pi_ڛFG+uXrV[I@QMm ޽K.]ַfR1uB[+3V+uk96x}WӦ ӬRٳ՟Һao;W^uɤtL6, +=XOu_5iP`eŋ)qq0l(jjR[ED1gN,Kp:Sk#o_rhT=s}kʄ|GszefFB) . `"KĴiѕƶW6gVlR^ge? %K0f$%%&OzFun:/ܧ&}W`qWүr޺Nt!:Z=M!'kk#^_w01U5}k6i˗{GCmIߩȑJ#;w:Ѳ¾q,hȀG }i*$USur'>\s;*ÆU}e(ĉJ_6.}j ֭ʪ+mH >[}'LDDDTۭO|g(_}eݻҼ^4%s$eٳKlw]w<39xošV$+Klٹ!DQf v6B4hV rS.]r+3O^Y3+3;eB7uo(iPc[DD kr/IFh›}m0m2)-Y\F6Kfl9C&}.$# :3X";v8[ RNe%cӧ]brK<~ZYe3Ts\dV,۶%= iiz}c6zY"#u;eRR"wvi qq:wU,VW|MgyvD 1IL2+ժ̤?3Q]F[2bYݍ~m d8%#CIDJJsˈu~tzn*|Q WIzQuҴ>xYw"'\tb($&dp4}s'2dIZ4nDL&7,x'MkwZ^\K$#CItr&۰v~QZh:(sXStf&FULhFT͛܇Npʄ IMU{&z<"+T\WҮAdgenGeU+Ǖ`Mw?~RIOWmV_J=3"""L6U#FCZfEVͽGk0^Vxo}X&IhB9eȅm);w&^I(x&)N<;w!6&jn2n$1m7-[+,w""q?QݨwɦH G6]Ǝ`;oOeh8DDDDL6K !zX(1~о}$.g1ټ~h/o].eyu%&{w"& """"""&DDDDDDdn< (H̎XzYKq-U*lْkBDDDDG6n SZPPĊZ,[NLjZʎHH#1Q޽M{0mZ)77 :Zt# 3cesGJqFѰʲw;1niiDEѪO=eEI '1٤ڨK/t b[]kGN=.]"FNٯ>%oQl zT(v%ARa8 -UшkmX /`J-Μ nAN=Ndev̟._}KÆYtNiB~+Wja0be5 ~&B֬a$sYV3gxϚZyzLtq駭4H^EJ mDвe6nU0'ÇkݻGb8(?DՈn=էRo>}M~h|˗MDDDL6Y5` PP$&Gf ?R+lyDp #GZl@LݺU3wKи zaŠUhJǎNOC7uom<6lpGbcHN6`H \amcUmv`ڴRhaFGZU }}UVl7w:UI YqmF$$葔G&,XP<vr5HO~[С;?ĉ%hTTƏsm# aSF-!EEI1:TW[72Ç^ ٳNi>}/5J,M"""by 8h<fܹܙYpln x! h%aDXJ[_s^L8zԉ;qd222"%6;v;-Q$I!ʺk+޺އNa^/KĩSɘ93&XFE֮a"4+غՁ^L8v,)غa6 f(Evo-x2LsRo_6TaڴRw/}Рs ,B_Nг |ckק`͚D^&9Ru#wZomʺ\g}hj,>Ęӣ"&&0fff}RKIQ''W"""cĨ1Ҷ]{i!K2eINN2eaɼye~?\dg+יk@'cǚY3Jڵ6ϲBDGo_SPo%pMLL=d@'+V]3g*|>PYׯm ZWdNO_hNYvSZ ݟ{?8*Ĺm]]}{e{.e˔:j5h@'?1c̒qIaʸqftpayeFڤY3ddd>oy֬}歷I\N~IF1Gvv4OϐKٜ2e ,YXd ?ըλ(  ޽Q~oZe{wGFFRPڈ{o_Chj}{֬Qo`KrZөuk+3V+fElN y2"״v4k{ge6lm[믿:G&IL ^Wjlߞtzl׎Z7f̘ ˲kTG/:}~}z/_tIy8W-#>[[7uF%Xwe1Gn-\ZEZݵUWD̙}xqe;5 """bY˖,Y1c )Isx뭷jTMk)^1%E }{ދlQ^ʠ:)7{:y҅h=z4o뮍x}ԫWב^ivp#OG wteKҠ믍ƍQ#&PݧT5k㾮ӗ;y S駕eoYV_Wxj9ysljE6= iiz9sU>=,:y2));nm8;*CQ+~2۷;$&F'-Z=}5?!]+6`2l&),tNVNGDDDj6zl8#̒htnoqP\$[ Eɐ!&)(pJFh(uꥠb_~i /hJ=}T^m=""+WڤE :iTr;xCL{SnMt1JTNu2|YwHZrзv0}zdd(wBN$VBۍDIF3*6*6oVno2jT[֌ذyN0"J4iɓ-RX>)/ykg(4hlJP={2nYRSd lŐKVQtr(7و%ȡC`-"[W^k|Oc7>k"@f 9qxZPN-*rm[#:uΝWm'""""6L?;yd߅ؘX5[NFJ:UeKceNDD# ]])‘MDU;ւ5kؾ=.Ы GkeM`-G~foܞcxvd""""by#' bu@b w'/չ61$""""""&DDDDDDd&1$""""""&DDDDDDDL6&1$""""""bIDDDDDDL6&M""""""bIDDDDDDL6lM""""""bIDDDDDDL6lM""""""bIDDDDDDdlQ/?gy=cF;Z -K*R7hx|z+>8f;֮=u6kӺu=#&&{}$SvuUv_eג]73m%χ`ݏ}uk'nKt -?[Dž e-${[Z#2RU;| |i>llVA 뮦4-4~i/cɒ#+Ɖf9ѨQ 226|&DEɼy}j˪T_ؽ[RZNĉm_ҼxB@vX u}I̚ **۷FbCF!l~/!!AnŏCQ~XY_Z$y Μ)EZ̜7y_‡ŋehJ? iV鶅f>z+mSwՔvyBvPFOCB6E8rD9mī{*!AoN>66ā-[c<8 #?k^=Zi>xm(/wz._F~e[w  )NQQ ~<8a[n0ذkeuKpɵCFLMkl(.bX?B[cص2ExTU6l8'3jަG9b /tʓl>l©S%h2ޯ͛= jvD3׭;3ntظ,6m:W_˸.Xի0{eF<6,_>7x;|؄ӷ!7w0:tH#Umvh<gΔbK(,Tj]{ С-ww*fP\l?GhS =-Sh+;6OhMo4 _bf4k9~睃~|v;}p}Xn~rmmKY7|شi8 Ƿߎԩ/aڴ-0/IO? ޏFVV#޽zޠ|Ӧѯ."/ostZ g.]zԓDcƳ~\'"""ZH6SNTyN%߿gDmګ7+=oV<˗ap>LoZ`00p`SL11d^6m]ط]M1cF4kx5ƍkin'&7gg6F~MpbxFz3zNE|wܑ9q2l;n4̙=mv?;W|}FmY׮>fyl)={_POJKL ,]߇%K#%Eٶ-[cŊk`{ܸ~{ly~oNHxX۶((?899wʶ[8MeƏw&qjCgڎo-6mgϽaTEETnp1 ޲ZyܱؼٻFNV'طnyد>}R}ʝ;xjXMڬukO*&DDDDjtͦ)C4{o枤cWegg~0{vg\t`[ѣ=_\_˜1uo(.[ߥPL+oQ 5kwС@Si}ojI|s 64i6,p˕+ $ξ}<g>s\|s[X,ݿKҥG|ʷ8_|qƍ m03ft?bftHfe3~Ѹ~Ԙ:}2?܃S\ fp օ^_^i""""$'N=#\ D{{FhP9;rT1uN$.<+Ojub˖x}r&u&s&9GqYj˄ m<͟ןAY۶]o(qTMy'TY0Vv1?5ڴQfox|>]⹥SOJ{-I۠Tia0KPTTCpr9=9{J >ߑBsE"n.\(lҥGdTGD*Cdoig+ShÍ}ĶX˝8v̌ށR -cK8u&m{V՘zJ7r jȺuqu;+[o_؊ٳ#?| zN=H{[wx| uOf5n\kOٔZ(]޴i,^}\1 5%~_v&pa]I&3(  CkVu me~4obvHOGTT4TnO?:ahDFFoúuwݎ:et뫼;lRR&Lhy{FD\ݺ5_oL~k3&5|xːg 7{i>ѣѤ/Z57z⭷zU;;P~XmM$ QՈQcСCYխ+rss*Tov :t zsjdj߶m0if˗dQ`2ɓ#bcbh#<7Nֱzu'!f=)""""2xp3de5Žw[4\ر"^~'Y:vl&'eֶ͛u2pީ8)XDDDDL69s0bė(+s/kSo}1`@zن!i GLg EDDDTFK7VxnSJl`C#DDDDDDdlM"""""""&DDDDDDdl1$""""""&DDDDDDd&1$""""""&DDDDDDDL6&1$""""""bIDDDDDDL6&1$""""""bIDDDDDDL6&M""""""bIDDDDDDMP6mr<Ϗw~} sFףr'nm ]/6n< 뮦`޼><5}r*\$嚌wr6A{p%AhCVj8ƒ o_ċ/DGGlz6 / }!&~`r A|64nF;e777ũS%ŢO4P_(}H6Wz嗧ռ\}Gtt >C<8G%߁Lv̜6 ͛a!j |o/oOPNKݶm$W `80gOW_݃cb֑1;fs)L;v?_V?>P_]MDDջjl^隝f;t p-IXv(6l8+@A.CL~+oRܓ8tȈ2'7ԩ <%رqns^yQe6$$hбc x& SqX\@F1غu${V,%j5<x6KQXhA|6œOvF1a`Q0u޽ްک&U6M}WX4._.GZZ,{ŵ²T.FAQ bG!.FFh,7ĔFƞb&D ŚD"KTĆǺ,m \cyvgϔs0s ꅱc<OWqdAqĉvzru;/mNK@&fMMѼ8ol}V@@M4j$儎k};ѹsJK[[U׹-[n# `іHHHCFNXʕpqQ ;?~T>6n8}ZLԬ逧O1gN[ԮMdIL|,~hZ.~-0MYʫpp!&Er9P={5=n$ϊa۶,ZB0c{J[O!8;KЮ~ÆsiO]e793b2 {/Y~MDD`\+22uӦ4JsdƎU݌.ԦNMq$'gaɒ$DEBaCE.MKOGL}:%9;3f4DE@BBomvzU{~~>6oS__JezrYSNnWUQu=?oq./toƗ_^Omm;ġ Cjj.oO=fMw״xޜcceNԩ F l֭7ѧO}10 0%9qr(7K\e kQ(Sqc5Ξ 1?ܫmӦhkRfM%}Qͬ?cl]+Ƶk[N:T^n+Vh0~|#G qh&vr«>=}g6-M޽ػ7 crGH$@ϞrlڔS QXȞ>,t Ԗűcjem""Ϧ\ߴG4 8s&;wF;΂/z-^lz{;c߾>HH޽ΟO__4(1^^SCRŋ,OO%֮!ش's|vAq;w޲*InnIHLQ"ƍL]k>*;wºu=8ѽľ>7ofa:c>育侳.[-s~5 =?\R)…א8}J >,Hxc:c块BߌsŊ㏡HJz 浃<<yM1>}mۺlS*׶*sWhzTHJJLj[G"Z4|Y{E7>3OuGhbtC# dyqzeĖW>8W 4wpB^+]|#O8׮՘,5+׫sjNm}`{wh֫'→銘5k =;=y$$B.eb]٣\ NͭDDM\-c4!jԐfGһz5COѸnn1r.. ̵vUddkt J=+uê|T̙mP* 51u~vfdܹT*1mu}di9Y].=ӧM78:Э'fhcmYΞ +k`FVb075rUqg $P*/…˗wܹ-~ndytX򦓹u1J,ԕUFF>|0Nx"de{ =`%#22wkVѪbj%K?y9.. lx?x L]>+| vEr3m37 0k#G*"AvXQe… U OrW< 6ODAѣ((>_,* 9d^s6_=%ߓ $%ֺ>M*\40S9Rn%w;v7?~=fe߭z [G5e2^@c6mzvCYsϧ\V-榷XXyz#3S$`ܳh{ 0|}݌gd8;+L쬿8$ρՃ8Y`IDATY Ų sۡX@ddbb#7_}u|ɢ74׿XkG}};/>A~ ĦEEQےӧl+rP^0@>fL6?:ءKS߿/OINH7Ӹ9n׮NiKRxyI\ n/6K=lc26=fu.XrҮlpnn~**E?;fy6\.[G5e2~VQCaپ=~x,k[r,Wy< voOAQ=@\|E㫯Pɾ-L]UYRRɐݎ?CZW^`׹z5Νg7\*\,\mRr 9Sd/&Ç7FDDk XV T}+]%NP*aC> OW} 1gN:u᧟Th4][bѱ?y"DŽĠnɰredgvpq6uMhԑMDD_YNEն`8E]ח_jPT$LKqr٬>5#lG5e2ž4OJ~a ^F\\: II٤c*cqtabIJe0vl39h!r),%1j;VGkPepH=%bs78$oDGUh6%fnktSFgV}۩S[Uww{;v2(YT*`sAT :uAرBk]TWWR(3usg[b:U֭.KnU&"pid}l=7Yne嗣ѦV {1lvHo뿗6=zሃ͚9۬cc8Qzk*[NlVyoDzF:t&-kxx xz*!JɚzbnzK:Un٣_1ѽi~dTþ5j˼) /*ρ9sll|oJ.֩hqG᫯0sf2s]@7H`аih#:W&0 ,YD\#=ԍ {wwOg8.^"sg=< Ԧ:ݾ`UuYgpj23 믷zL9c 'Ǐ? k`"6V[.'N<ܹgJ]OU̾l9YRwl*W^'~^0'OjwFʴEoANN!O#==ߢanzK:WO?}w !?詵} dpZeXunbΝ{Yx GՍVlIyΊO,G랞Jܻ gFjj.>7:.Az~$|۴fyu`„jgt^HL|>}͝_}Q]T$87wkm\!vMqř3'!(/j``mL2/{rMҼOK];aZtň/Y\OQ:wG*\OUT)'sIDU-s>jTSDG'ƍLܽzȗquJas0/,YH8􀀚Ti}2 ֭btn:hڜ9Je|I$CRRrPSf';8;Kf͚IQԪMDDWmO6kcnhvptM7|M'Q3,VD"2~NJ_⥗x26mA11m 6#s`u9xfX:wZ\F0~ؼ9ȤoQ &N|Maowu:b0/ZG(n ;w? PH!KQ gcdҔWFTA5^] ptr~} BC刉Qc뛠.XΝNWww rV- c>5>dט897ǕJN)لҺMDD%?ppeC[?lٲ3lJZ]#w*.K#x@_4j}`rrA'4D:b&"\FF󆅅!(hzH񆾟 ?y&ց6A" }GTE߁I{7]èQMQT$}sĨQMYP:РA d96M)bעTa,?L s朁 hAcABuܳDD $""""""IDDDDDD9DDDDDD`lM"""""""DDDDDD`l1$""""""DDDDDD`&1$""""""DDDDDDD 6&1$""""""bIDDDDDD 6&1$""""""bIDDDDDD 6&M""""""bIDDDDDD 6D"D"1v5t *CDDDDDD/hYbڵxV)&M$Х۷$.ϒܹs ' 0uTdgg~۶m޽;j5jԨN:!::R%&& OLb A^flhܸ1 drKsqtO͛̚5 K.MҥK;"%%gΜD"Ahh(VZe""""""r߱c %mիQ<ԩVZ;L R>.>1JݺuÑ#GL~/..Fڵ!Jq޽Jh9;w Œ3777u6Jyyy0qDq]vԩSAde:Us""""A%p Jr˞lj /^Ç1echٲ%6nhr񞕕UjFfh9㫯 I%Ç6k𥊖k{8~8cϐdOu<==~~~F:sMLYTݻcHJJիQTTd4Ќʾͣ]t GQQtkcz?mM[]@qZviiiFi rv}ll8^3  _?y$4hw}J3gm3ʰ64fF8@wu6]СCၤ$Z 5'o&4ᎉtfۥH>DDDDDT My 6BBBʘ?@ׯpm!++K5k@ӥ 5JuV!77WHHH7o.H$aݺubZ6ʝf  cǎ .uTv9y rдiS!>>^#ԩSGpqq.]T|j=zՊ+B݅d/.]jA vvv·~+dgg ǎ7n,8:: O:%wBrr!lݺUhٲ,O9ыӧ66}Z  ˖-|}}'''Iϟ/KMMÅF r\PB``m۶ ;k͛7 d2PN* 6cUIHH(2L[0feΣP(&˒R?СCAV *Jڵe˖Rץhy ͚5 &ΝI~Kn/f͚Jz-\xQXpVZmv='""""z͐2* 6-Н;wХK$''?*H`gg'%"""""F 3f͚RY=-H3ADDDD*XhFƢ#k[@DDDDaaa4r9pZ"""""ciTXglM""""""@"B. R!""""""erHgH$d8hd2-"2G{AjgT $""""""HRH$H$:׀L?Цc)E\k@֥$ NRIENDB`plprofiler-REL4_2_5/doc/images/tpcb-using-collect.png000066400000000000000000001605601465735455400226550ustar00rootroot00000000000000PNG  IHDR[S;1bKGD pHYs  tIME(RB* IDATxwXSw KTPAD-sZ{;jjگu׽" (CTQ&!dB&`yx>s='s-B]ADDDDDDTWVe'FGGeB$of=rss+کS*`S0$""""""&DDDDDD$d1$"""""*ڿ?L""""""2^ih$t&3$"""""J%&Vl:""""""*oĈzd0$""""""&DDDDDDd~xMf-sZ֬  ^^8th|͚8>~:9k2Qpvf{phDDTqh$[KKѨQc_kj6Ʈ9 Mg!ȕ/" :];W {7@ 5dG2R9#22 7!lYg89Ռ_kZVE*F9?Gnnhܸ{/G>Pjc6fDDZ<|8 ouRihuUy<~{DDƵk#+ƍLLp99b68w1u5q_JDdnr$SӯkBwbǎضrzhhM/UCԆ:TO[[KVb$'b߾lXxH_ܹ0@(>~,߾ "z֙H%ڵsŒ%-U۳'F4NMCM"48;[# .o\J߱cٯbDDO[.;i?֯Q>u+5:dd!8xq>m X ww; W[x1))Bv/~j,mN/)[J8uJ͛HKX;lY#F4aa>1И i(?|X`zЭ-Сˁ`};E[ڹbfZTeGe.UY^u1e I KGj89Y{7qMagPJ:y2sYHOZ0th*+2dbNLƩSBJ dƍ!8^ו.B׮4Tg^o@n:#44 ۶EXX:EptTl#`ئ`c*c]:..6U?UyRUj+cKjmfPRJWY)(P<|M[s))BYYb̛Sd!)о~+||ͮ}r1keܾBrr! q#l+Q#Ձt҆[|ƪU!Jq#/O?ӏ0n\S,]YȷS= U[n~*̙prvr%Wc͚䓎0ϠΛwvWKϟO)8v1~A_ʫChhν}(++7of?kz`xy9Ty;+Ғ%ױqcN&СD,_/q˖ *234DffQT cs̫5"ZIHWy\]k(]uQmFHg}!-/3/xR㇭&7of⥗N|jw0~i̚0e9dd~lÉ*MRo[[z R,[vC-(o{XmCԦ_0j _z X*#V;7QK0;p ?e}}-4Z-i3D$Js=,X*S{cLM]Bs+qocΟy؟FDTTˑ-[UwV5:.(`Ŋpvƌm0p7<=!Jq鑏$1  ܹC=*?gͺݻ+V647*g_Fvv={B˖u!oPb+noT I,!)wǯ&$Rwv[G@xx//@k[OUf{VtyOƮGuwܹ\VVܻ b<|s=-Z8kӪU ڣW@ddVW3mAH˔ ԩѼ3!ʑ.)de)#ծ[n;ffo#O?Ν=_-[/˰eK<-ڟ~RMb rT*GHH*.;突*G51cL'"2$H;wqjtOҼF4u(T;;K-[UNwrV_ƍL_skW1zxaÏ)ܼYщɈR> rUi]ðaǕu3gEÆnv6fsկz|ڹyݣ'l鋡C!9ۡCI2}:1Զ_\ R_;vS>Mֽ{I$rl+h-%vr*iؼ/b %XS3T/_Ϊ۰q:0x CT")"ԫgm*/Čm}JSU^{`e c~*}p ooC_Y?g)kDD5QNmo֭wcĈjٻ];Wkcaڴ*_H+?:믷P-A{FK/q{s [[K̜FX.IRZun]O|AjoTI$~)+BBRO=˳PW^eTUYYb?%S ͕ed֭lDDd*JrsڵF%iIL,:ݻyF{XsS׈j|JU(?]y'N<%(HuS$>*ӎ}roT^vx|@q.S~1lXc6nCZH:eB*T2Gʴ޽Tbjdfcu.gh̜y F@`^Zk0--9ݺEXkB]?5ܔڗ1ɬڵs /ޒPqN##2$'b˖xwZe`ukG^U{?w1&M:S[ǦMq1B) V\n:bp/ ׮eҔNMOG((PT{9;[|92>Uڱ5kn@Ǐ|?U}*/"i=(,L|E@%z{j:%qD2!J NC6BzkϞsE(**YXX5..$""" Ř9I@Jb b\3.LU JN.ĉgpb*B)qHƏ?14>ǜߣڟ澔X-B]g^T~̪o8uCT !Oj5W??'U*2_BB>F:r[} ºu=UT16rr1xQȑA0i͒%0aTe'qfQާ)9ݘc~q~>ƌ9rݙ> voP>Hhyƌ zxq&MjV{= D6ΝJܹ3V~KN.+t={Ґ~bF`ݺh2_H`U1̫1c)DDU|~a`` `ԩr-v<4`׮:o^֠AZ?l&G_:0jSjUwysgӧǶmwM^")VFN:5cFD\|?}/|}}ՐAH7]Ծfjkk/섾}u;UJ0IyMk Ԯ,[n//lҧB=w̧aΜ1yk3u?5Xm4DDe \'3\Ǐ(((56t@׮0rڵ3Q6u¿qHnDZ"qD0n>hvʕ0zPƮwq 6ضv츇 66xxءKL691c| Ix uj2c~눽{ĉd=k2֭k-O;u몏Yv nn6Ć gOvマlKnΝ=0i?t0JrSlYt(FmӦ5G>PVADV\p@?'֭l<~,T֯N+x{A۶;kBAm]ܺb" .F_ܓ&G?7V[=})QUUtY"2SIIM{ņ!""x,Ry\KDD5O%""~!w"14矪ݓGDDՊG2j;wrqC嵘 3DEeck`„fl<""VV쓑QY.|ͻBfNl<""Vƨݻyط/W|!h&Mѱ iz5 m>@DDDDd$SbD8s1Μymqco8:oUOH0R9>(?P{PdT$fш}O?t /xn]X _|lY.+[cժVS :c͚xe=wnhM"㣏.C, -ꈖ-:b,_~,;ٗ /xAx?:hv&))B|512Ղ[Bcq#SǎG2ѱ^oN~?ETT6D")5s”)p[@Ek{7woD")n]z?G!22 2AA7~JDDDDdIfdИM,M2mm-c˖x#Onec0;>AAhfezW%&""""2Rмb *mڸj]B,1R$Goʎ,K{,+Æ5o=i!!zVzzde⩈U:ᅬT&Z#F4ٳC_>XTĔZc"""""&MU_Fo#w橽.::[z~!SCp~IMJFT( ^z}mjGʒJؾСMÆc: IGu+[@4jTzB{|n< :le]~LDDDD$Tw;M;pC<%vu=1198>EXZm!Μ)Eukݻ{S'E} du ;];Ws^[j/+KO?<^}VR>'(sf[okS:䎾}mQ3.A$~D"/ܪQc"""""&:#ytiSxeG,; ƍ%HM_nȑ$128x0ouaa(,H ӫ׼jsDdŊ`)eٲ%BCӐ/AAWfBn]4)|E%1`D~z_~\2䈱u+uwŢE(;PH`[*G⢣sL^+@q!Gc=<|XXl1""n]4?rw кZc"""""s o ={TftYCR)+>~,:uڇl1lq˵̙m7757wn;᫯cӦ8/^?'O&cΜP`ΜvxjbK:O`\gUP119Ëbi+Wv(?Y>1V޻ v8yr~V>-…a8vAwVfE*CU]~LU}Q핛8k2<<N+aog_3F?يQA F,⭷Zq:XDGgC ukZ E: [޽ OŎwqc!'G \GGk:"8ƍkM&g{8p"#Q|Mk-ZC8v졲|rnnhÆ5ưaF11 bVGh/ pX2D" <=M^]Y8x0."9Ű-5=ѷoCnaIU]~LDDDDUH2Q"GRm#wOq̙j=ډGi0֑L 6W0j EllsgϖB},i@/{$p#cD"GDDrs1B(…|m5q9رM_MH$x/O""""""s;?pa{g !!7ofbjLꏁ:M{zmDR̘qI\t􋰶oDDDDD$,yxa߾ظ1Ǐ?Ľ{yepsEn?5r͞nnؼ `cco:bIDDDDDL2M89Yc֘>Ykڴ69{3QMaDDDDDD$dL"""""""&DDDDDD$dL"""""""&DDDDDD$d1$""""""&DDDDDD$d1$""""""&DDDDDD$I&1$"""""ê6V*++[+ $""""""&DDDDDD$d1$""""""&DDDDDD$Ӭ)R-O2`2dDI&LFFFFFFFFFFF&TLvtFFFFFFFFFFF&L2FXkbtt v_fZ ;̓f@]˛ps#c v"=I/ jjO??Ĩ7+ lŋ?/kaÆ"22I]S;غ/0_4M;bOj9/###d2LFFF(<<1d`]LLXZZM?th=Kh߾-3\4}QSS/GESiWbRMؾ<$G2k f5ґ۷?5jއhڴOF~/ ŋ!`vL&{rj}dd!++ ݺ[}ʳknw?M{ iiidw5All{4mڢ( ڵ = ӦT1/o||yV]j.-I0tͭ>evyҦ扌Jz8q C&M =GL&C׮=ɓ"&&Vy>*r7#0x0x{z φ //_:t}СCg:tHth޼55AEoyu_T'NbРh>>~x3FFFd#̲###ѼysԶ3gNχW?ԩf/_?SƬ3ï(9M1=Ktضmg;"#ѬY3̟1Μ9B!Ο?l >B$>}6>d|G+h@ݻP(Ĝ9Ç7b˖r[XXС}O1;UΗwwJÐzGEEgagO>9zx^*bֿ?wy'O?ϟ?ݚ4i *…q~,BB ~vD"t p-X <==zQQ70d`RgW}ۧ"G2u/9ngbٳ`mmmp?cddgE\(KP S+*nn RY8</^ŋ?ŨQ#3"3KDPPrŋxR9Rzk^&LGFN[C};>|&N2߄ SGЩS9 СCgܽaa5#)E[ ^C ,;Ν|ݥKЭ[7Mؽ{o} `?JeW[fZ#<#77w:˩UyT}`'O2KKBѭ[p7֯yTnn. <<N+aogڛ?~=pqqAPPGw۷w5._~)""-Tk׶_ӿ{^xax]O>Oc}[jT]M^s'}Ry]n]ys$&&òn+]ڼy VЭ[0z1lllt3NJk[KERVz˩U(-K &穖&E9ŗXC _Ke` .CF|} &O~ Mۻrz:i@Wl{We-_Wmmm%l_$Q9] ~ұc;wWq5̙3+W^^Ϟ=|?#rXJ¦Կ/lmm`iii##cmA,jo ##c㫯e#X%}};Ze7Ѿ};BTTIo`Y4l9ׯ߄%Ÿ]o_&q#GDDנJRn ӦM¢E z~Kwj:DZZ/__ __ܸqAFFFdr"|bDDD˫Jnx#aZxx]WRn8pBbqѓT1222dD0vj|:ܼ 77/4hm\[V;.]Vy{c޼xs.ŋз”CXj-ʕ0PY+Wɴt)cƌćwߝofڵnwSq+ŲtyǏPy~aF=q2oot: <6lɓ_W_}.WaȐ;vWO4V3zGMWX~%.Ю]g_A cѧ xyaҤ[_?gXd9Za؉8w gddd4XZ9 L8^Ɵnڵ?}Ȉ9Wȣ8,d2222(XZg͚W/LoܸD"!?###d>#xM&######ѢmCAAcĈalFFF&L2h222227FC8{?ֻƮ]жmg4o}_ѣFFF&ψZ|L""27y&R;=l&"zY Xx$G2hx$dQ%/avADTI;_$Sdߙ`=,ÎAvT l""2~T_;&PGK~e^5}]tG@sʯ{ʭe/߾r.S BaeʁEiŶ|ARY啟o9Dҽ(1T%?lS]U6|<,w2TL[:dmiHBlA,д _E2ZOneK~ $/t}!WL2kܛxjJ똮LO-D Ky7bvXy ,o u%RK X~$>'d L~y{sp;8snJSCerY}.ޯGSP|M4~*?LeHy> Oq eV'zg)`#e4e-UOʳ&A{i3\\kKl]ϓSݓDȫyV}}c@Pz*\Kl~@[LFzfLvtFFFFS~~H@R?yw`^[ηn~``m=J60H|B}ޥ-`bʤiYK7Wq.qˤrG/^セ@ ˮi^}ZBn'ho3Mu UKQ40a(8ѽT__0P/Gxw(֣m:Dlr߂4M׵"&L4$:>@ Js}[h\ƀ%0ʤmY)OyQ#}]Bա3Y{=&g6bkW߲Js*GSٵ9W>FakiJ0pmXGy糀9M:V4tT|uP/fkqyZMF&Y7ѨQ3\m .Ry~ -[&}2M3*A|m[(jW&m˪HykKT,#Xs >@]+ Q-ͫoY:;Y*ݒIKݥ.C _{xi({>K9n`sI@wWKՒɫ麖CTLFd7###c5~ |}Ck:ޮn˟\7 42]/hrV鲟\7IӲG#kbp-4>VQS4wΓߍR$,PmO(cv"y_-۾_[Z/\vmus.]Qoe?XG>P(xE>O5s!Hi{zEk9D>DY!o ={ԊJeeł+}1 yͱLςъkC}GS "DD5!;FWb*.75Sʵpk25mWEɭ4+0qk o4V>cLW\ID>k2]4ɔ=ƊrV#pyU#-;)/ֶ,i+~Pg /*ƎaS!M{˫'c+V72=K=l Nک?G2d/*F?Y  FFFg sG2dVHf(X7,i z^ADDDD.^4DU IDAT0ŅԸnw`+ e^ĩUZoA<$ٝ2ClD՘"K2Pxc1TX%oإ;x$j}2+v$S.M>51zȂ\g* 8ij"w|+&m'Tś rj6stN;+>JF~[ R@9h2> >0ta.7 \4N'"""Y^l_;{=X-0&\UIsQd2,`Zm/wVyR'BT*R͐vPf^n(mbh;!BpAF:fpm}]{ E"z {(\S7&Lhk ADDDTߏTsiQK z̲o CcXYGUyj CEWpv+ȱ/D^qpk ܝ-`o+6Hq$꺺psRL )vRyux ⓥDDDd4V*˨|If-O0mKY8/0h~.Laܗy𙐅擳nVӲpl&f㟳EZˮiڝR Y8%t\%7KY>" [O,}18*ίorֺctߝ2f\)7%Z&CG+5%KEh B!q[soHDDD|<51u_{iVK$*O,>N֞ &L#% _D l^\з5>]q:7m_zZLwr 1WͪSm(uA\d{yXOW`LeHyV/M,qk VWv1}kpC!^m?\05lн=>:8 K'خ&!.MJ_Ld; y\*ZG;qU)C;".xs>P2}ly-Seocƪ4z9sb[F3^DKڣT\+e_wc9\=8I5h2ɬTc4pCX =]%;>29gk 5Cʥ4ɴ,Է TN{k+tjQz$3y nV?+ҙ upNaoczǍspDdRM &*%IdrXV尷՝tIer0q2[!z%R9l _ :H4MZ`đ1ii~R.]H_CK,EH7RFuz˷%*@ S_mKl`H5m B8,F*âߍ9쬫#퐛xL4$8G2Eb9 i=ۖrqv2 Dr[^t߂C3+; BayGQLږU誗Wސ(sne:AӴ--DrvP-, *!z7YݭUv: PT\+k\eXvrrW{k|]96/BVVZͻ4@_N.~X(ר)wAFNE22z&MKG̯koBdH/KtmXCkIfMULƭR|Z5kh]!lk֯وMW8WK4?ްKh9%>bȂ\tyyzˤiYʣzZbLw;EzMMaAB8 Be4mm^Ʊb,}CXY "{,V]ssUKlB9lNu5'Uv[Wwp#^6ee1fss0-G[T lR׊~mr6VwxM&i'Ey,VNkcDX[ @"kY$z-D| 6L\'+gh"@. %({ʺ o*.XI~QYF1YHjVzZe2Ƕ07hib|EK ""g*9ǭ? ǭͺ6;<*{Zf J !4) Q] kUm,DE"H0DH'3ɔ2dLRޯܜ{9ށ̹ܫ: 6++HL,( N~y1sLG&m$S4/$Q'WMö`$ lcw_JDDD=XǑ@)" 7b1z:ߦL֥k{έB.@aS4 ""C@ɜi`mo$SDvbfzހ۶!8OyVQ˕t-d¸<QeiUVQ@ U^D$n8U>1O8\"Ҫ!tBDDDD ~=Mt#e+bߒ{"""""z@lkt2X9 ،1$"""""lQ  jI'O4}c; =%oK}qr} xڑ=dCﯗװ\xo8q۬8| =8dBØ|W!hO&"""sF1ɬqJ\oC^Yg՝"i"އv۶=JKǞеKر+v-Sg'v[֛gpPO;gbZvDDDD &M'Oy h'%ja0:#F㗼v$l6'BRq ,|C`nHJT"R#ǔI(,l!I ipXiuzLw75If3=ߐvAL; qd%ٗݗpw7:= qk3 +uVٹ,SUh 6o3['_۪>T >N玟Ĥ; >ఴZ8[lÝk^ڶA8U vx}IC:t }{ilxR\?,Vm;+0(ϵ$""""&L2 C)ہEtmqm4^']r+ uڞ1#cߋkwŸilij*WCcꭓmWSEUH̤$%J|'ϿR ػ)cGٗk㹗ƾݰ ]zŽ8#wOOsԹoxƣ:9#⛕ɫ@&q"Q!2RfFކ/)mZt3[akeݞZ[oA_uk[udvB!#d0'o5 ̹+ {YܾCkDE} ɌC.G@!I 8X`s[2 NtǮ <>]WezL1 敢l'X7C@.*&SC۪YY:\.(gj ;Գ2فSPQDTvF-C]~ɫ@r%ztUr6?՗0\%""""j yEKđNX,N/!^cZ2Z+p❅>{`-,'zߗj42\(7XUW}b娬]Y%}kX_-َJ޾uCb;a49s9?lANo;r**,G̮f7 L2yBF8^ pT~ w+J%ó/AQ;qGOTŋNNFFFFFFFFFFF$(022222222222^HddddddddddddH&Ld22222222222r$I&G29$#doR9$3FFFFFFFFFFF>'I&LFFFFFFFFFFF&L2h222222222222$d2222222222222dD&L""""""bIDDDDDD$dL""""""bIDDDDDDب~]wц~葀￿;v\} п+<5#gO2clΟ""Bnq-p흡Pږ@? <ך5ghQ ݻ'` Gpq.XB?d"+UOy|kמa9ѣS=Ъ:,@Ll~ `^-**9ӧ2Yx[[=^K/ܯ7ovܯ.?.,J;/yY;K}]w3xٜeZm%֯?<sn}Waڴ 8q»OO.s֝cк&,0_Z_t cq/@h%zNĞ=Z:\{m{m{mg2k=:4haK{|;oOڵ7b^4Rsŵ̟Ձ{xQP07O9 -0l!7:7D>C^|1?ފ+o@۶Q{[VVe*+Rba8tV^}#Iy|APyϗ#o)%L s޽'_{;U~UWEFb߾[#svO Oawy-{d5ѽ`X޳? ߓ|Oݑ;݂y@/,9q)!C}F G= c}+G& iوI&]3Цr :Z.]bk@kٲ&t+ @N1Ph. < P\lҥMU5*ӧwF@ xk K`c̘PȈ?9* -iTh;}}e!ٳ⮻ &FH&G| h%6O=%UV:s~=tH$5T*9{7"G.Oa9? ĨQxh6 JL:uɎCgcy+gK #[Q\l ]vوI&T_c;ys ** rtž}^]=y]|6j?xܩS;{]# H,ѕhF2e2G{aΜ lTcרO?߃W_bbT{˒M] IDAT툋S՛xkF@9rg)*JN,t?Yd]O_pJFRgB_?*iIc!uUXa0Mk3ɤ~}B9 I;v'V +=FedlPؿ_+11t"""j䍭B4) 2V/L]ᇳaُt}9tz|_͗d;V_Jx>JJ,k 7y־}ŅzP_X.K'_|ڷ\.{{Ϟƍ%1udeU+5~sj2?%K,d1.[_\uUCw~T7ߜ$>< O?ʉJXSpg h;]_|QÆUd1PP`@1fj==j;riTV:p O=v:e:st>Vo}kr_SP>5}}>[a2ٰdq,^,%vrkv`å:]^_}WS]YrU*WODDDt%32j|rV%'x>@s]Zm%f\r,]:kߍ?/wu)**ǜ9>/s-}4^]|[XhkMHTKKZy]׬9^k6R^臙3ӽ5Rϳ{'|gѵ`1j<:GNoxc}wGvĉ0vl;>h/<`q;Kq?_GG+>̟zQBb4D3gbg^>"5g⟤$5>z|E!8wz}4:um1kVWO\C`s0BzAHKʕ7/Nb8zԈr"n)S:y]vx9\}u,b߇}V+m4W x? oBDDDX5IDDDDDDKF2J"""""" &DDDDDD$dL"""""""&DDDDDD$dL"""""""&DDDDDD$d1$""""""&DDDDDD$d1$""""""&DDDDDD$I&1$""""""&DDDDDD$I&1$""""""&DDDDDDDL2I&5 JvAd2@ĀnSl n"2ls,H&Ŝ9f8lY .̙cnZ†#L!.NC+[He kZCZƙvm'N8q}FV>dd6B$m$m\kLV!'džPo_ɱaj[φf DQQ!0uUUvz2`ժ*|Q4718{V`x3rrFYcÂjaC,JKƍ3cɒZRbIDrL⛑d+a <J%#@&Dcǜx) ƌQbH$&Х FC9֯U,0wߍ*({QB"N'Q5J(O>0V91o^@DDDD$3PeZQPf萕eİ&_ra'&N4#!AFkoӟm:qhFX 2bٲtعff!oۛkG~7ިBZZԩcǪ@ncWXHMC!9Yi8t`[gǠAFDFꐐĉf8C:>60gkyϞ ϣ*;v ?ر#KG=Zi"]8w6jwѽ;O"""""&S_o0y=Aqq"v튃$p-flbj2pfjc8X,6nCq;w_б^>hDTD@o{׬1cj!5j׽Nvb`#6n/b%bX!F;Vw2 &tq8}:<]w!NoqF6<7V ezH3Q5Ϝرcu|P-M6KUw+*+_:""""jYI^ٳLٳgC4>5~;%$#pn jx qq2de)Dzm.R$:u#)I^Z |a%mo 7J 6LJ~$W(-?0r11A J$L&uyu+l6W"1`R?k଑uߢ珋+5HL#2RaLl]Su1ֆSQ>سg,Dv ;sZX9k,,^؝X.^_B7z'=Ks͚8*%Q۷?j45`@(TǎrX͍ tY)JMZm[juz#T &$f{ Mtc4J7Θa}qt֯EI#Gz'W 'dž3"0{vr2zr{+Of}y5 DDDD+V,MkJJ~-{\IK6ϟfW•$ k@O{/\p^_SMW.MIX_RR*뒜Z*/=q=Æ)Qp:'U֓^/pM&|A%_E!B$`Hwa'u&N'V!%E(\""""jIաMIZ~<ץZ\3pmS& ^v{ tho`WWPgMI:_?$u p~=.GJndǂQx(wZ8nYoN'>mjDž +""""FdN<ֲ쐶oYFY+!LL<26KK5Gӡkp񧽁۟F+]tkVbg;kq5?YYmގ+A5PPȑFdEޫٶՂi0gfJ9ˊǡ^2LDDDDdŋcɈ̜9oVHGM]yޏ( U'(۶=CزEzuI7m֯:V0xu}%Ł7}޶mJԮ+iymMǫ. ՝, X_32R|yGxeeϟ3Z[0p`U<@N}ϪRr٥u]}q5 GDDDD_zFhQdgg B+ 2 l\{];PbNfhWI<=( !ۥm*Z1pMI]"#C/T*ؽ׺^:II:ѩ^=N1eIZp"-M/ضMCg߁*oiu,^{"xzYqSDv:uMBMEJN=묳_6o \+zЋ|0XJ\sAT0&BoBlf;ETVك*{fT^ߗ;Bъucg_~Ysd)e0:Llmxq ^݊m4ȈT9Vŀգ;Ga>:@ܰȡ&DUWɰ}{^[n1#!A[o5c(%vsVc!FoÿUKUUw`H:L`F> l~A] v;пKvwF* R_0twaF\GЪ V`43Ҩrl,o$"""O),V ˗7]f}p{8pxo/%EENлvq""""Fh4̙33/H4xN{KJ6 9 m$Zʗ/DDDDt4$SDd^!fʕ62ĈITɉỌI&Q`-uי0m?f1lZ倍qq2wQ 'gL""""""bIDDDDDDL2[4L#Bj dݦ@8&DDk6n疋cDFcԸ޻DDDD$֭v,Y2>3nj-ҥ18|؁9sM]+V0b :0t_}e ^7T6:1ɼH sZH˾})hZftVِcѷYY jذzѾVdg1xEE ϏL&ԩf,ZTtYOyy̛g> DDDDL2+?UT-sYTĉf$$пmB'màAF,[Vwt2;w:4@l{t8{Vb{@zT:ߗQg}hT*Qbbta43ʂl> %]rطW 3>wWU%]zNgB|[qpAu4t\rswUHKS'9ƎU!/ρ\{}^XHMǴif:hnCB'QP ߞ?̙Z޳EEzz% w`P`@DDDL.M. Vgل,vѵ^(ZksNmkkT )vLi[]sNѦNdeɓQVfV|ie@io|NmڵR6n"1Q'wԻ~ hb(ع.t:xrhŌkhfQZN1}YZ1o%> d}zZ1rF>Z!:pr~ZoXjRy>/*r֭u];ظ&L&!v찋Ν"6V+|ٽ.T*HO׻۾~Morn}?.ǎ9 tbEu?H _9~Cb՗?lV Z-Y⽿K͙SrRҽl|RYo֐$bn<$tBъk5z}hY!wo llu """&M.ɜ k&&VS( p8߮СFiGDJmӧn3\t='_9~.MW9#=%EVc}ٳu_R*뒜QCi0n 11aJvN',AݵˁW_G4_Qo)Hh'!N1)2ׇly-|oS& ^v{ tho 1ݏH"}'ࡇ*+Æ a%ԧn3BP_F~]YWPgMI:_?aäΚU.];7:R6:5$sɵegg~XVpAf}fi(b}:t{.7}I^}#PWOxq%ObлQQ5Ck}6C]/~ F+]'L]\;kӫ޾/>UwQٺ~\{;="A=xЁ#G_Y)HdxbL<3g⭷ i?X~>= PͽI۶ ~gm"v}뮓[iS~ Z+);ĉ'گ~4Ʈ]tǟTQoҧ*yI_VOfzrڶVRR2%5ؗvqu?Gro^wڏ5*[etQ ˗{o8W_Z R"""5G&P(b ذ&˅ػ.ڵ J+vc i&yyv1zQDGWOFԊmLBezR?FקN9DRNtG:DiSL"=bBku&-ٶMCg߁7:Eǎz~[%,!vB&~\>O@.I׿B/&D۶G8S{)&oi#X~G;EJ[GMEJkvݚmڼ&ri›|&kTk1*@!&M2 B+*˅ض&:w֋(˳]QeCqF=H2r L">^'j0 ֮1DМ11Zq FQP;J*Dyp?⢠6V:_v#m+`7U}{P*"5Up{w G:>X.:vXzc6P4$Iъ6mt{ʅ(ěoZEtԎ@ԟmwzr6mSr͵3qnd,s᪫tbLs{}i*ѷADDhE\VoEJWVwnWV `Mb^{He/UjIf]I,=#SX ˗/o#{` IDAT ѾtilQ]qa\f4̙33/H(SԜ%iiCCeyy<ϗ򊈈׬L!9Iv+WڰcGN`#&MR!''CDDD$Z@FOJ-Z3a43 3S?f1l|Z+L/8cG5Crv1$""""""&DDDDDD|q?2kͲMqKU֥)k2Ça@d [!6?H&Q35g,[Kcps昛|VabcuaP#R٢"'V됖fq&]kk>/`Lwf^ByDDD$B'DbFmε6^+}V!'džPo_ɱaj[=oaEv+QTxd2LjƢEUAݳǁ^ X }D|M Ξ?ތ:듗y:nCLjdVb'X=gR <ƽ4P* 1'zʂ1c??2t"…QAmA}9 L&w1~ 11 ^0 5&q&tN_71l**{Zң͈A!+ˈ7_ra'&N4#!AFkoӟm:qhFX 2bٲtعff!`o_TUI ((pb8uhZo/Dž YPy9YУ:0`}TRGJCLӦQV&<~ եcӧ#5UJd=M3!*׵;лjjƙ|e$34JKrswUHK~w$ر*9kok]uv dDd zLhFAAhCy\ 3GgOA;V`XdG>K?Quֹwo2 !""$=#SK(ESg hBĺu6a6 ]t VSsk}Ě5U`p]"3S-z|}SiYYqC9m~ZYojn+@_}#GΝva4 ЊSMr*W,d!Ǝ5 @+^t]45JڷNVdgJ2X{˗W @+nwR"jU{٩SVkƀ?^H{CM2X>%%NhERRWrCuruCh~g ._啄Zױ>,2aRX͙cjunMZx'cVl(%CJI/k知m ZZW϶yYZl|RRVm'DBNh4ZqF/S-[S/>ػBm$ZJ(.5k/^ ^Xx1򗿄om(}YrCoUM ~zLǎrX͍ th? Or U_ڹt>%% 9LD[}}b(uYNѣuO?yL~=fW_-}gU[79FrtYjjVڶ3ueý׽UHh{ 3Oӧ~},JJ9oekr8@N 3fD`ϝkC|Q4|Q-"\b_ݴVܿ}a.[K<YT\daC^ zNLlY u*Ѷ\c[BPHY@C˙3RoFu d2Â;?~܉{/\p^o6m^eYWWٚI|\ Ci0n4AϰaJvN',A t |PWcѢh9 v9V<+?QcM2}!N)2W 2mdn; ޽8t(6'4p:eҥUM.5Omk_=ٳP*GgUBfT_umL3ڦH'p~Ç|{Pe]ub`#6mc(^TYѥsF"""ƜdN<ֲ쐶oޟhʜ^>=?{~Ym.9X^놋? tho8#7/OpH6fz-.vgOj &S*`ɒ*zk{t\#2̚%_iqR\<3!,81rf@nnȰ8rĉ(u90+dŘoi^?$nlrV#L֬\c*Uh3Nd VU mMtQQZgwRY{pO]Z6u*ѷADDhE\VoEJh[e/lz!=&;$]V&kY_s>5 P"""d32j|&?2?欥$=fV%x͐D7+ˈ(,GҥEENлv] q3A;sLG&)o5'hIZZ{I:Ԟ&/Kc/iVIɤfnq`֬rh{#&fʕ62ĈITɉagLj _\*RǎUM0`Ѣh\w Ӧ |q4O\""""b6w%ۘ%w%6ɰwo#"""J. """"""&DDDDDD$/N@fpY)\f̻ n"2l$"""K#D6sQPe1X4;0gɷk F0!6V85⫯l!-*r{+Юjiigڵ˞8}UkW4ZY67׎IhZJd=͵%"""&́aA;У:.j 996<}*FN W!oaEv+QTxd2LjƢEUAݳǁ^ X }D|M Ξ?ތ90pkذ`A4DlRqXp& _ !ky7arOt"""db``~X|9{W'5K 2bNe{$/mYB9ؿ߁'⑖&}u]p5 iǎ9ѳ#F(~}5:0~Ç+8ьφcr͵czR`~3#GyV`}GUd&uhBD]Zؕ`_YuյصE%,E@P@zP2=m2-I>+/f9>3sewpsg37Wȑx@j o_Zݳlj͸65gg'""2;adeDTdT,(P(k6"'Dž!C3 2Ҁ43^೬. jEBtT@:v̅GݺFеʭc^'T*:SSMA̺C7ض(<֬qkW3 HH0bP+rr\OsХinƇ M)7n\>48f;nOMyޢ 99. lA|utI_hڄX ٌ>+[gf;w:qZO M1h΀N׬Lf.L᷼M0:I-;hӦEb _N;fL8OD~RsOT7oWU0OODDDTZJ&).7:CKX^:v4ɚ5vZEqH˖F KfS /UO:o$VdrIMUܸ^{};璺u fǝQ,ϋ+d]躃7w1V/-Z%+!fڵv$ju ; .|%YˌAiNrJ:IN6O?bٺ!͚%6V/9C꿱zi.@/G[*oHeBX4,^/o;R߻ڞV *lL?p?nLaUL0e˓@/-[Zv&z\jvtݷ!^,DDDD1L!Kri*U$`0ĉ%!!Aĉ`0T*"_|Q,^-!z5˿Ό z5p9ujzYYvS""M҂7uoU'CJx>wnq D^fٳ.D&6 c)gB%I!euKZ 니PLv8x):^ nwJmlެ$C=zTܿ+{uZ e nDF*ɚ-JR?{vq_.ȰIÆII1޽36VflJzfi]n$so„ JdNM8{V9c%t7jd8l(^.^tUIL;x3 $&MΝW8ʶ@5o;sUeo.\WX_j=O逞=593.s!e4 ǂ?.G`Μr'HIɔظ{ dAjlDF<؂5kx"""~?"Kd%"s'JD\]g03fc@ vUou&%?_',@l ? 1v{b3ض/Yڵ ذ!= x"mk…j{u$eu[޳|NGHer[736lp`֬h|a'Y-O\ ݧŷ`0N-3΍AZZ"#m0{v /zDDDts$Ç/,==Ru^t;= =˓|Yy^rĭ"7uWEe%wY1GJS?7O=u\ڵ Ct4p钄?-瞙yWpJ{V&G3wo΍ b~P#_Lw`{fڊ®7z?#B߾fX8LQfl>[1cX*yeKeݳ8zDDDts$ܿm„ x+UuX^9;Lr=0ݲkܸQyDm6lu tf:I &`] {w/׳zun:o){zEޑ;>M-ڵ2cOM2wʽZ$> z$WϮתeN/%')))vQH~Rgz)]?+

ql6 & +{'"I;a)dL[kTjYvj5!""SLDd2& evl ݌aôXD!"""bIX2֚oΜm1Vm6fώa1ɼql oou:exmũkW&ƛL""""""bIDDDDDD5' @0,{#ΔZSfwL7r\nzQX((6nCl$"""L"r&Y:,\ÁNLdZ̎>},5 .΀=k{ʞ:HN6""€&ML<؂իWݞW_-Je(sdY0uj41!<܀ƍ: '1ɼ$Vu+Šv,YbODÐG%v\in?_WEHO[7 NJΝPT=ڊ9sl!W'ڶ5a >,yyXTgCXdvbƌ2_s{bb̚s IDAT %s0wn1S"""bI/&J!4੧"=˟y&ڍ> / ~; *4oƧFqc5֮TW aO B:wÇFCDa,60qb>W[KذiSFb`-""5xHlҥvvV"""b庫ڵqa+ 4 -͌?*4. jEBtT@:v̅GݺFе@Je@VSgj)xYw%Ӛ5tjFT F jENڠHޖ-Mسljv̈8w ]2yf|a1\.oRТ 99. lA|utIBZ-p~::cX_ɓ ШZIIFw8tȿ[k@|;g~23عӉ֢IKӦj Evc. Wψ1cؿy>UabI~۴ éS˘Ŵi4?YI\!g8rĉߎ.7{׽FH*ETIn"rs8DcGYcUdli0df=R\MjML&lT΍s.[ ii&9~)yy.5"^>Jl;xH{ӎjҢQb6]kNLV@56pII,R }T$""S(^~*/eg@/3f+6V/}=1j lyrreKcvpFCe}n(^rs]~Ϟu QI&)tIn"-Zud 8q$$$8q J%2|R/Kz%$ˬYufd˨Q̩S+le'N8%"B/zN҂7uoU'CJҶx>wnq f[aazy-ĪW/DGY_{ ѾӦ%wZU)ϙ3IIW=[?hQ'tt:gO fΌ<\aHe}{㏋3'`v'zO=N*+jK\~KE\H%T}pD*UuToQ;2x]wU[1ImLip?_',@l ? 1v{bۣ;5s+߿ﶶkaC,{..;Ehք mP_u$;v-Sys#nЭ680kV4>0ړMx|Yỷʼn'Ç/,==Ru^p;==-9fEuJUqc{J &.+&Uz*L)3ΝCvaFbQ}iSuPmV}h0cFܹ1p:_,j}=.SϻJ _;%;z[ޟ[ZZؕǝLYqo_3VAff&O(3}8xЅ買=봲 eN{K #""dΛ7ÇG||<`„ x+Uu牭_u=rN{eKD@B`o]'Oa@n栓` vU`;wW=WW}+6魋yG]L)СJ7#۷;ѸcgVԩcđ#iHmdaC%ƒ4dJ;k}o0ےn)_F=۱Me/^oFD 6šKO6$"'G}Aeߖ+zpGDDDէ&= FX^v5ɏ?%?_d.$'DKVwŒ㕙(M+E$;!we;aF.]:-ҪQڊ'( ԪeMrS._v+3I~iQy'5ٲEن` d@;ؘ~.j2Ν-+VI&jC襲m`m?H D-*a,Ұ2Π&DL)k.II1JT^&"YY6TE1C=XX"ӧ++2%SeWTj[bŷ09%II-L6mRny]41JRoޒ1UGp6"aaz"ٲ.͚%:Z/َ>U4Z.fTf.,TfF🝙:&IN^$"B/;djYssZ e@8%%EI把Ds뇜upznm)|eU.IFhҠ!xYw#% ~I!C,oC ej*ݎwfRAy$_f+m$M +w[)S%%EIccrf۩͜Y$:DӋN6mL2cF|nؠܦJ'2C* WCǏ;eXԫ 2aleTX7 U++IOȮ]˪TWW*&~Ҥo65/x>gDDDDՕdZJ¢Bt7Ȭ%jͶk -͌{8v,)ƞ:B&kQDDDD8{'L읈B$7ͶkƍMeg;Vs^?? """z5*IH&cQV,_n֭qp6L%Ktl""""&D'my̙޽-3 HM 1l""""&7-A-㭮uWGL8vcC݄l""""""bIDDDDDDL2?fӒeoęRk쮿ec?X^(,@T 7!6э#D7Iqbb.'&Mq-[fG>g@f|ReOra$'a@&& lե=ceK"# ]Xn]=ڊ$# hԄ/D~>'1ɤ$V(u+b[Xaǒ%v D#"""&t-_nc#CHW^)F{ƣvm&O.zGVRKկ}>Bnf_}c}ݻqp jEn?ls.W,O>3 ϜQbLIn)_Y2fi|RKLTW'G"""ETIn"[1 2qDIHH2qD1 !'-bn)U:J>5˿Ό z5rM/^Vy8ᔈe(P tʾ 顇ebsP[] (` [oP4\e:Eˀ٣Yl(vңG}ǔ?{.TwҤc}TA-`HqY23!-2{vqeL&ddؤaCe^oy/Kt^~K&)tIn"-Zu19qD̛7F0o<Tw߭-1b<߽;т行RK#{+V(#*;{G8RR(*JDffl캫"އQ^Y\i.ml]Nrjf͔/"7W&kJ %kxٳ\RѰRieY}n}(ׯ/?GfVUKUmH*rsuTʷ)ו0٠ӮjXX[oUѰۛ1mZak/]rǽn]_OMJRx/-Ku#͕J|2+̙p+ /Qp=|q1<s@Uf$dJwp iifgO ֯Eݺ*u))F|U1,T """bY]*{y0J\T.Go뮊x;&w7)M1[{Vk6ZWxv°<6lsEymۚp>e?OT>JJR:BǎY'ٹRYCP7lp`֬h|a'Y-O\ ݧŷ`0(G[PT>K|xe镪#ؼ5; """bYE͛Ç#>^s„ x+Uue11Qu@{@Y|7*Kޒ"{+6x:y҅u3o뮊x}ށԽ{׉^mPCLoG owqc#ǂ믊FN#&N#GjK%S *NK[Ґ!+}Q><)}T֨n?q/#ÿX=ز/ 7#"BMХKXĒ%v5n{(eu ̙cCxx۫ݰe{&ή]Mvٵ!jqxei bv]w%&;áԩKNEdvjeV2YEOpJZi(9e2k姟U8i&Fe Ⱥ7w1]jnm;b6ZeMՆ>siYm}KRRoIaHVCn(*^>8~Pf(@/c3g\bL^ ^L{e{w:^|8%IIIN6ȦMvZi41JRAΞuյDD HX^>HElKfFKv#=d˲uC"#ҨWұ}"ۧYNp^Ύj56ܿ)kxDDsg^\%ɪU :^4KNSRRH$?_^=䔮_Qr!"rsN/+GDdR4jdF/ 7uo0iPbȰ$<\/qqz2"9$)ImEqqZV[c2%_RR;6V/wielAP5sft`N/:^ڴ1Ɍbb6%W3C*?2lpzSƎJzJԯo r[HDX7 U+V-[d.GeU*'|ؽ!G[^=%! nҥIbc:(#шF%R]:uDFF ?2f&{&wǎţQ#S\h„v°}{5ЉOf9N0Y;MMjIA|L&c4nl*,; m}(WVLDd2&*ǨQV,_n֭qp6L%Ktl""""bIO2֜91ۂ1cl̞ÎLDDDDL2oz]rTSa׮8~QR I&1$""""""&DDDDDDDL2I&1$""""""&DDDDDDDL2I&1$""""""bIDDDDDDL2I&1$""""""bIDDDDDDL2I&L""""""bIDDDDDDL2I&L""""""bIDDDDDDL2JTE%K/m<: |ui|ѣ~jN…7'p"@rr4 e;{qb!Ԩ];ָP6| <|`ĈNRm{hj@Es?|m8 x.5)!"""*Cd._~ʕ`l6&Nӧgcn=V8tȌ>;#`\g/ܹ8b'Ν+!wdCV_ߌNbl@1xNF OɓVv"""H2/aC8r|̓?vaP^]F`ݺ!h߾I+{oomtxؿ$vqCu锛ÿv9oh aatDQ/:UR'NlYbřkȹsX(`ܸ4t: 6X-ڶM Ny|=6OBk IDATvÞBGG{j5ka6޽гgqa߾q[\ď?gG!"""*ʩax`رb^YV<!;k*Tcn6ѱ֭u%{ tT=[''NX1kV6nƭcĈxf SUi,N,ZtVC&X,vtZ4kj[ 6VƙWy#33Ǐ[PTD:HIaFx[$|yZaY K5+vQP@f7~[R=o9h*+W]o=gWcaVPc?xЄO?=[/ҥ"jѩSm"-v`T՗.=>:g ЬO=4b#tM0p`2-6{۰yEŐ 铄X-k;ÇSf[&Gbch2n0|6nI2pxO#̼hTcmFqӳnw!;;yX4fTVS?dqrц;sg?s{$΃M;R"7[^ҥ'}K%e9zԂ;xFʳf9<6({8|؄;FjI22|4nW׆ 7jj+WSOm닱v9[wgWrC~QK}w Ӧm,1'6cѢ~ذ!3gvSlFFiPǩ`;!99g`8qZ3JDDDt3TbiȕcA;kwʬІ [iӶk \?<-Se:]*Ojn20}z6طo$/ =}tyy󟷢؉h fGbʻѡr9UKQ}UEy\nz?HO IJNPn+0~7Abm7^AwVB~;HkZCNرc8,JmJhq,3f-Ǿ#S3[Ϲ}ܼy,v~ǩSM4k{1 ҸbȦk4.{~ kذ&ЊM=tW_Ϟ|]_\wI,y XeڬY3O$DDDD>B&T؁^4(ٓlXqLV</{f; ӦK/mddăr]&MJL(<=MK^W4y^oU+5$αc{\S.jn-ݺðaMЮ]bqHEn52ΞB٠AzJBf8#==ųh Boӫr>~f۱A(6h]k*k3߾k4"""",7._~vy%IF):7_;_ftgfز{Ȱj6{ey]o8Nm)Sn{%طψ9s!=}-yЊZHY֭:|ۻd;JF`a'i-oઈd^V#|NxU5_iZI^_3ڔ4yt>1TZ2ۨQ'pwԡQ-w7JOrQXvtZ2gF5^RO?&† 嗋Wߏן+zK]P(oRRLF||8L&~96 FHcb40~gψr T*%F2oH̶o_48|v*[H?vd;SBv7;6z2{wJm1lXb l6̯oـq_ :[CJ{ &MOUWcذ&_.zUU>P63[DDDDBy3UV}%u\k+_+ هtI:q^OcW:}3v!3#Xu[Uef:իϢȉ͛/w8U*n̙sNm_*2fL34oƺjj6Μ糰zzkv&UP@vvF^}'Nʇ!+㽧fw KEJ&9xyJ2Vʜ}M*#|s3̩>n\ ˗FqGZ Y((p kPauLEp-&""""}( ˝EV[\y-[V,cBL Ӧ@<ѣ^Rr8pCVU'o$ti0oS"'xbs2KGt`ܿ|r>؇>8ztt닌 gƤI8v̂ bzk<>ߤ+yE뷢Tz"+T_ǎ=1x|ї^W_o)UןoF]_G7o MOWW D슧 ͅS)kY9~X*fIK""""}*[.aҤVh$jjq=st}޽ALݺ}q& t&@ }7Ç7ARR4ѯ_,\x'&LhQ==< kGxwq&1X| ^}#tph4*Ԫ;- ~;StUVNB{kڵ#BLm$Oj+HE|Wqwp|;ATT5ԩ62r3|x ZjȐAj~;Æ5AJ4ҥ}{^ !6[OܐPh*Eҩ#222"Tc#v|E*vs:=z|9tѢ~iEUo?~岀EQF0Cq„ މ(ٔtxTO>ɩu|)O٦MsSDDDD1`@CtRYYy6]a4Th4jde]oY3jd;6o$]V ri2kpЯ_~dlƌ.PT/U\DKok>WuTc2Gx6FdKt Tn07þ{!wVXkDDW;lt6 z%!22Flr& uF"-~-7tyo#2n@M~m"&ɬi֭;SK[^<^ڵKd{Qܹ=ر1wa=[}M#a<Ξ-ٳi,[6 D\עEƫ |e'>PnMDT&LP-J]y n?p檱Qǵw5IAs:: fh/ )XC5<7_%7wEyf;zj+l6w=wWry_z}1=PFum |e'>PS]MDt+/(Hծ)b xX1k֜ç 'KЦM"L }/U_FI|1 nv*]i@+?,\x |sp7ĉ-ժ'%t۶25zNƁF̛w۷_ƹspuDk׺xTWzPVkM.xFw給Mu֭.n3g/p~OV]ar#)) C*)#$k'ixʺT!dÈ)N8 |eWE}6lp?)/8q Z-Pi?FC+]̎.\@Z/A4e^ >^0qW/ 23c=Bf 5Uߙ3Ww޷nDwDj 5K2{mll~C&w@Y~eO_ڋ~8ŋBxxo`!323/\8Jns~//NGVe|DGWͮ}=;fҥ'cG~ bb_OuʴSUmt5.OVN>lƌce|qj'aX8^zɿOge]|>]u'6?-wߝªUgA׮uCYG(yׂdw^ {z2۶v dyl6Ǝ =Μ);ݰA9<\HfR ۶mtIDGk퐑q |SRE}`<|Q-c_[qϊ1yrv`F VyBE~e/_^VLYRih ;v8p+_{qARn٢t of &pWb>^7ܙoZV~>{ jρXjj<֮a4(gR+:]맟r=NIIQ?vE!99h+:ڤЉłwbXN57Ǐ[0*i|y_=_}:'>Ab}j;Uf닾˖-;I"mmۆapYsC]D{C704 Sb߾ؽ{}'yݕYG(u\`5:.;._u=Y z̦MuسgUv萒&'`^~ЦRt萁Gو*v K6l+>2lOY*ZI0/ס(5+ʈeL^ b`0$`׮8ty ʕvOš5XMͧcb͕x`.,Z`͕hÝd$m""& l͗TWl66 HMY6{1DqEq?~~4ĶfQb"ƶMh|0o&ƨ}Pkҵ+jtbBXXewfw49{sܹgg`2;d24}gّXTTTbjĎ>'OF5CKZf7޼ EEdo` ~2TKQVƢv!Z[f}9ix}{wLLz֭'͚*-O?˗ԴbN))FtCCQq , lΝ9]re߄BѣVlf1&8"wtHAj{ ,rsR 2xx6R64oc~Է _dMUIa3=He,OxpX ^}WMS=ԶGהF 8ieiP>:z?@@e<]_ h --p;|ɓOpTAq훤t(-T6m2ҥ Ufq3޼9ritܻ'-s]>ymMw__<0]܀K ޝ#.cn]L;cU6A1OA|eLMa|P&v''=1k0itJ`P˕lHȥ2a&3<ʺMT^ tϰdj-mQG5!IgRZtmepL. ΟCtw 8vlUU,.\PT4= ^(9͖ј`RF[Rcg2^~*4e&.^̠Ĩڷ y 2 E2!j|nbij0q.Y '&H PfhHߒp01FjE.u vI|DOiL1ܵ 46@aʕb-mYQ@yyD?ӼrFFBIAHHqO^o<1ϸN3W\^ PL Profile for pgbench_pl run #1

PL Profile for pgbench_pl run #1

PL/pgSQL Call Graph

PL Profile for pgbench_pl run #1 Reset Zoom public.tpcb_upd_tellers() oid=66239 (2,912 samples, 0.30%) public.tpcb_upd_branches() oid=66240 (1,085 samples, 0.11%) public.tpcb_upd_accounts() oid=66238 (979,064 samples, 99.21%) public.tpcb_upd_accounts() oid=66238 all (986,899 samples, 100%) public.tpcb() oid=66237 (986,899 samples, 100.00%) public.tpcb() oid=66237 public.tpcb_fetch_abalance() oid=66446 (481,666 samples, 48.81%) public.tpcb_fetch_abalance() oid=66446 public.tpcb_ins_history() oid=66241 (943 samples, 0.10%)

List of functions detailed below

All 6 functions (by self_time)

Function public.tpcb_upd_accounts() oid=66238 (show)

self_time = 497,398 µs
total_time = 979,064 µs

public.tpcb_upd_accounts  (par_aid integer,
 par_delta integer)
    RETURNS integer

Function public.tpcb_fetch_abalance() oid=66446 (show)

self_time = 481,666 µs
total_time = 481,666 µs

public.tpcb_fetch_abalance  (par_aid integer)
    RETURNS integer

Function public.tpcb_upd_tellers() oid=66239 (show)

self_time = 2,912 µs
total_time = 2,912 µs

public.tpcb_upd_tellers  (par_tid integer,
 par_delta integer)
    RETURNS void

Function public.tpcb() oid=66237 (show)

self_time = 2,895 µs
total_time = 986,899 µs

public.tpcb  (par_aid integer,
 par_bid integer,
 par_tid integer,
 par_delta integer)
    RETURNS integer

Function public.tpcb_upd_branches() oid=66240 (show)

self_time = 1,085 µs
total_time = 1,085 µs

public.tpcb_upd_branches  (par_bid integer,
 par_delta integer)
    RETURNS void

Function public.tpcb_ins_history() oid=66241 (show)

self_time = 943 µs
total_time = 943 µs

public.tpcb_ins_history  (par_aid integer,
 par_tid integer,
 par_bid integer,
 par_delta integer)
    RETURNS void
plprofiler-REL4_2_5/doc/plprofiler_cmd_ref.md000066400000000000000000000240031465735455400213520ustar00rootroot00000000000000plprofiler command reference ============================ ``` usage: plprofiler COMMAND [OPTIONS] plprofiler is a command line tool to control the plprofiler extension for PostgreSQL. The input of this utility are the call and execution statistics, the plprofiler extension collects. The final output is an HTML report of the statistics gathered. There are several ways to collect the data, save the data permanently and even transport it from a production system to a lab system for offline analysis. Use plprofiler COMMAND --help for detailed information about one of the commands below. GENERAL OPTIONS: All commands implement the following command line options to specify the target database: -h, --host=HOST The host name of the database server. -p, --port=PORT The PostgreSQL port number. -U, --user=USER The PostgreSQL user name to connect as. -d, --dbname=DB The PostgreSQL database name or the DSN. plprofiler currently uses psycopg2 to connect to the target database. Since that is based on libpq, all the above parameters can also be specified in this option with the usual conninfo string or URI formats. --help Print the command specific help information and exit. TERMS: The following terms are used in the text below and the help output of individual commands: local-data By default the plprofiler extension collects run-time data in per-backend hashtables (in-memory). This data is only accessible in the current session and is lost when the session ends or the hash tables are explicitly reset. shared-data The plprofiler extension can copy the local-data into shared hashtables, to make the statistics available to other sessions. See the "monitor" command for details. This data still relies on the local database's system catalog to resolve Oid values into object definitions. saved-dataset The local-data as well as the shared-data can be turned into a named, saved dataset. These sets can be exported and imported onto other machines. The saved datasets are independent of the system catalog, so a report can be generated again later, even even on a different system. COMMANDS: run Runs one or more SQL statements with the plprofiler extension enabled and creates a saved-dataset and/or an HTML report from the local-data. monitor Monitors a running application for a requested time and creates a saved-dataset and/or an HTML report from the resulting shared-data. reset Deletes the data from shared hash tables. save Saves the current shared-data as a saved-dataset. list Lists the available saved-datasets. edit Edits the metadata of one saved-dataset. The metadata is used in the generation of the HTML reports. report Generates an HTML report from either a saved-dataset or the shared-data. delete Deletes a saved-dataset. export Exports one or all saved-datasets into a JSON file. import Imports the saved-datasets from a JSON file, created with the export command. ``` Command run ----------- ``` usage: plprofiler run [OPTIONS] Runs one or more SQL commands (hopefully invoking one or more PL/pgSQL functions and/or triggers), then turns the local-data into an HTML report and/or a saved-dataset. OPTIONS: --name=NAME The name of the data set to use in the HTML report or saved-dataset. --title=TITLE Ditto. --desc=DESC Ditto. -c, --command=CMD The SQL string to execute. Can be multiple SQL commands, separated by semicolon. -f, --file=FILE Read SQL commands to execute from FILE. --save Create a saved-dataset. --force Overwrite an existing saved-dataset of the same NAME. --output=FILE Save an HTML report in FILE. --top=N Include up to N function detail descriptions in the report (default=10). ``` Command monitor --------------- ``` usage: plprofiler monitor [OPTIONS] Turns profile data capturing and periodic saving on for either all database backends, or a single one (specified by PID), waits for a specified amount of time, then turns it back off. If during that time the application (or specific backend) is executing queries, that invoke PL/pgSQL functions, profile statistics will be collected into shared-data at the specified interval as well as every transaction end (commit or rollback). The resulting saved-data can be used with the "save" and "report" commands and cleared with "reset". NOTES: The change in configuration options will become visible to running backends when they go through the PostgreSQL TCOP loop. That is, when they receive the next "client" command, like a query or prepared statement execution request. They will not start/stop collecting data while they are in the middle of a long-running query. REQUIREMENTS: This command uses PostgreSQL features, that are only available in version 9.4 and higher. The plprofiler extension must be loaded via the configuration option "shared_preload_libraries" in the postgresql.conf file. OPTIONS: --pid=PID The PID of the backend, to monitor. If not given, the entire PostgreSQL instance will be suspect to monitoring. --interval=SEC Interval in seconds at which the monitored backend(s) will copy the local-data to shared-data and then reset their local-data. --duration=SEC Duration of the monitoring run in seconds. ``` Command reset ------------- ``` usage: plprofiler reset Deletes all data from the shared hashtables. This affects all databases in the cluster. This does NOT destroy any of the saved-datasets. ``` Command save ------------ ``` usage: plprofiler save [OPTIONS] The save command is used to create a saved-dataset from shared-data. Saved datasets are independent from the system catalog, since all their Oid based information has been resolved into textual object descriptions. Their reports can be recreated later or even on another system (after transport via export/import). OPTIONS: --name=NAME The name of the saved-dataset. Must be unique. --title=TITLE The title used by the report command in the tag of the generated HTML output. --desc=DESC An HTML formatted paragraph (or more) that describes the profiling report. --force Overwite an existing saved-dataset with the same NAME. NOTES: If the options for TITLE and DESC are not specified on the command line, the save command will launch an editor, allowing to edit the default report configuration. This metadata can later be changed with the "edit" command. ``` Command list ------------ ``` usage: plprofiler list Lists the available saved-datasets together with their TITLE. ``` Command edit ------------ ``` usage: plprofiler edit [OPTIONS] Launches an editor with the metadata of the specified saved-dataset. This allows to change not only the metadata itself, but also the NAME of the saved-dataaset. OPTIONS: --name=NAME The name of the saved-dataset to edit. ``` Command report -------------- ``` usage: plprofiler report [OPTIONS] Create an HTML report from either shared-data or a saved-dataset. OPTIONS: --from-shared Use the shared-data rather than a saved-dataset. --name=NAME The name of the saved-dataset to load or the NAME to use with --from-shared. --title=TITLE Override the TITLE found in the saved-dataset's metadata, or the TITLE to use with --from-shared. --desc=DESC Override the DESC found in the saved-dataset's metadata, or the DESC to use with --from-shared. --output=FILE Destination for the HTML report (default=stdout). --top=N Include up to N function detail descriptions in the report (default=10). ``` Command delete -------------- ``` usage: plprofiler delete [OPTIONS] Delete the named saved-dataset. OPTIONS: --name=NAME The name of the saved-dataset to delete. ``` Command export -------------- ``` usage: plprofiler export [OPTIONS] Export the shared-data or one or more saved-datasets as a JSON document. OPTIONS: --all Export all saved-datasets. --from-shared Export the shared-data instead of a saved-dataset. --name=NAME The NAME of the dataset to save. --title=TITLE The TITLE of the dataset in the export. --desc=DESC The DESC of the dataset in the export. --edit Launches the config editor for each dataset, included in the export. --output=FILE Save the JSON export data in FILE (default=stdout). ``` Command import -------------- ``` usage: plprofiler import [OPTIONS] Imports one or more datasets from an export file. OPTIONS: -f, --file=FILE Read the profile data from FILE. This should be the output of a previous "export" command. --edit Edit each dataset's metadata before storing it as a saved-dataset. --force Overwrite any existing saved-datasets with the same NAMEs, as they appear in the input file (or after editing). ``` �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������plprofiler-REL4_2_5/doc/tpcb-problem-fixed.html�����������������������������������������������������0000664�0000000�0000000�00000074472�14657354554�0021563�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<html> <head> <title>PL Profiler Report for tpcb-problem-fixed

PL Profiler Report for tpcb-problem-fixed

PL/pgSQL Call Graph

PL Profiler Report for tpcb-problem-fixed Reset Zoom public.tpcb_upd_branches() oid=66240 (81,747,512 samples, 28.58%) public.tpcb_upd_branches() oi.. public.tpcb_fetch_abalance() oid=66446 (1,248,733 samples, 0.44%) public.tpcb_upd_tellers() oid=66239 (195,487,118 samples, 68.35%) public.tpcb_upd_tellers() oid=66239 public.tpcb_upd_accounts() oid=66238 (4,335,474 samples, 1.52%) all (286,011,539 samples, 100%) public.tpcb() oid=66237 (286,011,539 samples, 100.00%) public.tpcb() oid=66237 public.tpcb_ins_history() oid=66241 (1,150,113 samples, 0.40%)

List of functions detailed below

All 6 functions (by self_time)

Function public.tpcb_upd_tellers() oid=66239 (show)

self_time = 195,487,118 µs
total_time = 195,487,118 µs

public.tpcb_upd_tellers  (par_tid integer,
 par_delta integer)
    RETURNS void

Function public.tpcb_upd_branches() oid=66240 (show)

self_time = 81,747,512 µs
total_time = 81,747,512 µs

public.tpcb_upd_branches  (par_bid integer,
 par_delta integer)
    RETURNS void

Function public.tpcb() oid=66237 (show)

self_time = 3,291,322 µs
total_time = 286,005,315 µs

public.tpcb  (par_aid integer,
 par_bid integer,
 par_tid integer,
 par_delta integer)
    RETURNS integer

Function public.tpcb_upd_accounts() oid=66238 (show)

self_time = 3,086,741 µs
total_time = 4,335,474 µs

public.tpcb_upd_accounts  (par_aid integer,
 par_delta integer)
    RETURNS integer

Function public.tpcb_fetch_abalance() oid=66446 (show)

self_time = 1,248,733 µs
total_time = 1,248,733 µs

public.tpcb_fetch_abalance  (par_aid integer)
    RETURNS integer

Function public.tpcb_ins_history() oid=66241 (show)

self_time = 1,150,113 µs
total_time = 1,150,113 µs

public.tpcb_ins_history  (par_aid integer,
 par_tid integer,
 par_bid integer,
 par_delta integer)
    RETURNS void
plprofiler-REL4_2_5/doc/tpcb-test1.html000066400000000000000000000742161465735455400200620ustar00rootroot00000000000000 PL Profiler Report for current

PL Profiler Report for current

Example 1 for plprofiler documentation.

PL/pgSQL Call Graph

PL Profiler Report for current Reset Zoom public.tpcb() oid=66237 (1,073,876 samples, 100.00%) public.tpcb() oid=66237 public.tpcb_fetch_abalance() oid=66446 (490,112 samples, 45.64%) public.tpcb_fetch_abalance() oid=66446 all (1,073,876 samples, 100%) public.tpcb_upd_branches() oid=66240 (1,055 samples, 0.10%) public.tpcb_upd_accounts() oid=66238 (1,066,077 samples, 99.27%) public.tpcb_upd_accounts() oid=66238 public.tpcb_ins_history() oid=66241 (1,034 samples, 0.10%) public.tpcb_upd_tellers() oid=66239 (2,878 samples, 0.27%)

List of functions detailed below

All 6 functions (by self_time)

Function public.tpcb_upd_accounts() oid=66238 (show)

self_time = 575,965 µs
total_time = 1,066,077 µs

public.tpcb_upd_accounts  (par_aid integer,
 par_delta integer)
    RETURNS integer

Function public.tpcb_fetch_abalance() oid=66446 (show)

self_time = 490,112 µs
total_time = 490,112 µs

public.tpcb_fetch_abalance  (par_aid integer)
    RETURNS integer

Function public.tpcb_upd_tellers() oid=66239 (show)

self_time = 2,878 µs
total_time = 2,878 µs

public.tpcb_upd_tellers  (par_tid integer,
 par_delta integer)
    RETURNS void

Function public.tpcb() oid=66237 (show)

self_time = 2,832 µs
total_time = 1,073,876 µs

public.tpcb  (par_aid integer,
 par_bid integer,
 par_tid integer,
 par_delta integer)
    RETURNS integer

Function public.tpcb_upd_branches() oid=66240 (show)

self_time = 1,055 µs
total_time = 1,055 µs

public.tpcb_upd_branches  (par_bid integer,
 par_delta integer)
    RETURNS void

Function public.tpcb_ins_history() oid=66241 (show)

self_time = 1,034 µs
total_time = 1,034 µs

public.tpcb_ins_history  (par_aid integer,
 par_tid integer,
 par_bid integer,
 par_delta integer)
    RETURNS void
plprofiler-REL4_2_5/doc/tpcb-using-collect.html000066400000000000000000000746501465735455400215740ustar00rootroot00000000000000 PL Profiler Report for tpcb-using-collect

PL Profiler Report for tpcb-using-collect

PL/pgSQL Call Graph

PL Profiler Report for tpcb-using-collect Reset Zoom public.tpcb_upd_tellers() oid=66239 (175,842,662 samples, 2.60%) public.tpcb_upd_branches() oid=66240 (393,465,416 samples, 5.81%) publ.. public.tpcb_fetch_abalance() oid=66446 (3,310,825,465 samples, 48.87%) public.tpcb_fetch_abalance() oid=66446 public.tpcb_ins_history() oid=66241 (3,655,074 samples, 0.05%) all (6,774,775,742 samples, 100%) public.tpcb_upd_accounts() oid=66238 (6,181,197,914 samples, 91.24%) public.tpcb_upd_accounts() oid=66238 public.tpcb() oid=66237 (6,774,775,742 samples, 100.00%) public.tpcb() oid=66237

List of functions detailed below

All 6 functions (by self_time)

Function public.tpcb_fetch_abalance() oid=66446 (show)

self_time = 3,310,825,465 µs
total_time = 3,310,825,465 µs

public.tpcb_fetch_abalance  (par_aid integer)
    RETURNS integer

Function public.tpcb_upd_accounts() oid=66238 (show)

self_time = 2,870,372,449 µs
total_time = 6,181,197,914 µs

public.tpcb_upd_accounts  (par_aid integer,
 par_delta integer)
    RETURNS integer

Function public.tpcb_upd_branches() oid=66240 (show)

self_time = 393,465,416 µs
total_time = 393,465,416 µs

public.tpcb_upd_branches  (par_bid integer,
 par_delta integer)
    RETURNS void

Function public.tpcb_upd_tellers() oid=66239 (show)

self_time = 175,842,662 µs
total_time = 175,842,662 µs

public.tpcb_upd_tellers  (par_tid integer,
 par_delta integer)
    RETURNS void

Function public.tpcb() oid=66237 (show)

self_time = 20,614,676 µs
total_time = 6,774,775,742 µs

public.tpcb  (par_aid integer,
 par_bid integer,
 par_tid integer,
 par_delta integer)
    RETURNS integer

Function public.tpcb_ins_history() oid=66241 (show)

self_time = 3,655,074 µs
total_time = 3,655,074 µs

public.tpcb_ins_history  (par_aid integer,
 par_tid integer,
 par_bid integer,
 par_delta integer)
    RETURNS void
plprofiler-REL4_2_5/examples/000077500000000000000000000000001465735455400162455ustar00rootroot00000000000000plprofiler-REL4_2_5/examples/pgbench_default.profile000066400000000000000000000011221465735455400227350ustar00rootroot00000000000000\set nbranches :scale \set ntellers 10 * :scale \set naccounts 100000 * :scale \setrandom aid 1 :naccounts \setrandom bid 1 :nbranches \setrandom tid 1 :ntellers \setrandom delta -5000 5000 BEGIN; UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid; SELECT abalance FROM pgbench_accounts WHERE aid = :aid; UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid; UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid; INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP); END; plprofiler-REL4_2_5/examples/pgbench_pl.collect.profile000066400000000000000000000005131465735455400233530ustar00rootroot00000000000000\set nbranches :scale \set ntellers 10 * :scale \set naccounts 100000 * :scale \setrandom aid 1 :naccounts \setrandom bid 1 :nbranches \setrandom tid 1 :ntellers \setrandom delta -5000 5000 SELECT pl_profiler_enable(true); SELECT tpcb(:aid, :bid, :tid, :delta); SELECT pl_profiler_collect_data(); SELECT pl_profiler_enable(false); plprofiler-REL4_2_5/examples/pgbench_pl.interval.profile000066400000000000000000000004541465735455400235560ustar00rootroot00000000000000\set nbranches :scale \set ntellers 10 * :scale \set naccounts 100000 * :scale \setrandom aid 1 :naccounts \setrandom bid 1 :nbranches \setrandom tid 1 :ntellers \setrandom delta -5000 5000 SET plprofiler.enabled TO true; SET plprofiler.collect_interval TO 10; SELECT tpcb(:aid, :bid, :tid, :delta); plprofiler-REL4_2_5/examples/pgbench_pl.profile000066400000000000000000000003451465735455400217320ustar00rootroot00000000000000\set nbranches :scale \set ntellers 10 * :scale \set naccounts 100000 * :scale \setrandom aid 1 :naccounts \setrandom bid 1 :nbranches \setrandom tid 1 :ntellers \setrandom delta -5000 5000 SELECT tpcb(:aid, :bid, :tid, :delta); plprofiler-REL4_2_5/examples/pgbench_pl.sql000066400000000000000000000034751465735455400211000ustar00rootroot00000000000000-- ---------------------------------------------------------------------- -- pgbench_pl.sql -- -- PL/pgSQL functions implementing the pgbench transaction. -- ---------------------------------------------------------------------- CREATE OR REPLACE FUNCTION tpcb(par_aid integer, par_bid integer, par_tid integer, par_delta integer) RETURNS integer AS $$ DECLARE var_abalance integer; BEGIN var_abalance = tpcb_upd_accounts(par_aid, par_delta); PERFORM tpcb_upd_tellers(par_tid, par_delta); PERFORM tpcb_upd_branches(par_bid, par_delta); PERFORM tpcb_ins_history(par_aid, par_tid, par_bid, par_delta); RETURN var_abalance; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION tpcb_upd_accounts(par_aid integer, par_delta integer) RETURNS integer AS $$ BEGIN UPDATE pgbench_accounts SET abalance = abalance + par_delta WHERE aid = par_aid; RETURN tpcb_fetch_abalance(par_aid); END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION tpcb_fetch_abalance(par_aid integer) RETURNS integer AS $$ DECLARE var_abalance integer; BEGIN RETURN abalance FROM pgbench_accounts WHERE aid = par_aid; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION tpcb_upd_tellers(par_tid integer, par_delta integer) RETURNS void AS $$ BEGIN UPDATE pgbench_tellers SET tbalance = tbalance + par_delta WHERE tid = par_tid; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION tpcb_upd_branches(par_bid integer, par_delta integer) RETURNS void AS $$ BEGIN UPDATE pgbench_branches SET bbalance = bbalance + par_delta WHERE bid = par_bid; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION tpcb_ins_history(par_aid integer, par_tid integer, par_bid integer, par_delta integer) RETURNS void AS $$ BEGIN INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (par_tid, par_bid, par_aid, par_delta, CURRENT_TIMESTAMP); END; $$ LANGUAGE plpgsql; plprofiler-REL4_2_5/examples/prepdb.sh000077500000000000000000000034401465735455400200610ustar00rootroot00000000000000#!/bin/sh PGDATABASE=pgbench_plprofiler export PGDATABASE # ---- # Initialize the pgbench schema itself. # ---- pgbench -i -s 10 -F90 # ---- # Create the stored procedures implementing the TPC-B transaction. # ---- psql = 150000 static void profiler_shmem_request(void); #endif static void init_hash_tables(void); static char *find_source(Oid oid, HeapTuple *tup, char **funcName); static int count_source_lines(const char *src); static uint32 line_hash_fn(const void *key, Size keysize); static int line_match_fn(const void *key1, const void *key2, Size keysize); static uint32 callgraph_hash_fn(const void *key, Size keysize); static int callgraph_match_fn(const void *key1, const void *key2, Size keysize); static void callgraph_push(Oid func_oid); static void callgraph_pop_one(void); static void callgraph_pop(Oid func_oid); static void callgraph_check(Oid func_oid); static void callgraph_collect(uint64 us_elapsed, uint64 us_self, uint64 us_children); static int32 profiler_collect_data(void); static void profiler_xact_callback(XactEvent event, void *arg); /********************************************************************** * Local variables **********************************************************************/ static MemoryContext profiler_mcxt = NULL; static HTAB *functions_hash = NULL; static HTAB *callgraph_hash = NULL; static profilerSharedState *profiler_shared_state = NULL; static HTAB *functions_shared = NULL; static HTAB *callgraph_shared = NULL; static bool profiler_first_call_in_xact = true; static bool profiler_active = false; static bool profiler_enabled_local = false; static int profiler_max_functions = PL_MIN_FUNCTIONS; static int profiler_max_lines = PL_MIN_LINES; static int profiler_max_callgraph = PL_MIN_CALLGRAPH; static callGraphKey graph_stack; static instr_time graph_stack_entry[PL_MAX_STACK_DEPTH]; static uint64 graph_stack_child_time[PL_MAX_STACK_DEPTH]; static int graph_stack_pt = 0; static time_t last_collect_time = 0; static bool have_new_local_data = false; static PLpgSQL_plugin *prev_plpgsql_plugin = NULL; static PLpgSQL_plugin *prev_pltsql_plugin = NULL; static shmem_startup_hook_type prev_shmem_startup_hook = NULL; #if PG_VERSION_NUM >= 150000 static shmem_request_hook_type prev_shmem_request_hook = NULL; #endif static PLpgSQL_plugin plugin_funcs = { profiler_func_init, profiler_func_beg, profiler_func_end, profiler_stmt_beg, profiler_stmt_end, NULL, NULL }; /********************************************************************** * Extension (de)initialization functions. **********************************************************************/ void _PG_init(void) { PLpgSQL_plugin **plugin_ptr; /* Link us into the PL/pgSQL executor. */ plugin_ptr = (PLpgSQL_plugin **)find_rendezvous_variable("PLpgSQL_plugin"); prev_plpgsql_plugin = *plugin_ptr; *plugin_ptr = &plugin_funcs; /* Link us into the PL/TSQL executor. */ plugin_ptr = (PLpgSQL_plugin **)find_rendezvous_variable("PLTSQL_plugin"); prev_pltsql_plugin = *plugin_ptr; *plugin_ptr = &plugin_funcs; /* Initialize local hash tables. */ init_hash_tables(); if (process_shared_preload_libraries_in_progress) { /* * When loaded via shared_preload_libraries, we have to * also hook into the shmem_startup call chain and register * a callback at transaction end. */ prev_shmem_startup_hook = shmem_startup_hook; shmem_startup_hook = profiler_shmem_startup; #if PG_VERSION_NUM >= 150000 prev_shmem_request_hook = shmem_request_hook; shmem_request_hook = profiler_shmem_request; #endif RegisterXactCallback(profiler_xact_callback, NULL); /* * Additional config options only available if running via * shared_preload_libraries. These all affect the amount of * shared memory used by the extension, so they only make * sense as PGC_POSTMASTER. */ DefineCustomIntVariable("plprofiler.max_functions", "Maximum number of functions that can be " "tracked in shared memory when using " "plprofiler.collect_in_shmem", NULL, &profiler_max_functions, PL_MIN_FUNCTIONS, PL_MIN_FUNCTIONS, INT_MAX, PGC_POSTMASTER, 0, NULL, NULL, NULL); DefineCustomIntVariable("plprofiler.max_lines", "Maximum number of source lines that can be " "tracked in shared memory when using " "plprofiler.collect_in_shmem", NULL, &profiler_max_lines, PL_MIN_LINES, PL_MIN_LINES, INT_MAX, PGC_POSTMASTER, 0, NULL, NULL, NULL); DefineCustomIntVariable("plprofiler.max_callgraphs", "Maximum number of call graphs that can be " "tracked in shared memory when using " "plprofiler.collect_in_shmem", NULL, &profiler_max_callgraph, PL_MIN_CALLGRAPH, PL_MIN_CALLGRAPH, INT_MAX, PGC_POSTMASTER, 0, NULL, NULL, NULL); /* Request the additionl shared memory and LWLock needed. */ #if PG_VERSION_NUM < 150000 RequestAddinShmemSpace(profiler_shmem_size()); RequestNamedLWLockTranche("plprofiler", 1); #endif } } void _PG_fini(void) { PLpgSQL_plugin **plugin_ptr; plugin_ptr = (PLpgSQL_plugin **)find_rendezvous_variable("PLpgSQL_plugin"); *plugin_ptr = prev_plpgsql_plugin; prev_plpgsql_plugin = NULL; plugin_ptr = (PLpgSQL_plugin **)find_rendezvous_variable("PLTSQL_plugin"); *plugin_ptr = prev_pltsql_plugin; prev_pltsql_plugin = NULL; MemoryContextDelete(profiler_mcxt); profiler_mcxt = NULL; functions_hash = NULL; callgraph_hash = NULL; if (prev_shmem_startup_hook != NULL) { shmem_startup_hook = prev_shmem_startup_hook; prev_shmem_startup_hook = NULL; UnregisterXactCallback(profiler_xact_callback, NULL); } } /* ------------------------------------------------------------------- * profiler_shmem_size() * * Calculate the amount of shared memory the profiler needs to * keep functions, callgraphs and line statistics globally. * ------------------------------------------------------------------- */ static Size profiler_shmem_size(void) { Size num_bytes; num_bytes = offsetof(profilerSharedState, line_info); num_bytes = add_size(num_bytes, sizeof(linestatsLineInfo) * profiler_max_lines); num_bytes = add_size(num_bytes, hash_estimate_size(profiler_max_functions, sizeof(linestatsEntry))); num_bytes = add_size(num_bytes, hash_estimate_size(profiler_max_callgraph, sizeof(callGraphEntry))); return num_bytes; } /********************************************************************** * Hook functions **********************************************************************/ /* ------------------------------------------------------------------- * profiler_func_init() * * This hook function is called by the PL/pgSQL interpreter when a * new function is about to start. Specifically, this instrumentation * hook is called after the stack frame has been created, but before * values are assigned to the local variables. * * 'estate' points to the stack frame for this function, 'func' * points to the definition of the function * * We use this hook to load the source code for the function that's * being invoked and to set up our context structures * ------------------------------------------------------------------- */ static void profiler_func_init(PLpgSQL_execstate *estate, PLpgSQL_function *func ) { profilerInfo *profiler_info; linestatsHashKey linestats_key; linestatsEntry *linestats_entry; bool linestats_found; /* * On first call within a transaction we determine if the profiler * is active or not. This means that starting/stopping to collect * data will only happen on a transaction boundary. */ if (profiler_first_call_in_xact) { profiler_first_call_in_xact = false; if (profiler_shared_state != NULL) { profiler_active = ( profiler_shared_state->profiler_enabled_global || profiler_shared_state->profiler_enabled_pid == MyProcPid || profiler_enabled_local); } else { profiler_active = profiler_enabled_local; } } if (!profiler_active) { /* * The profiler can be enabled/disabled via changing postgresql.conf * and reload (SIGHUP). The change becomes visible in backends the * next time, the TCOP loop is ready for a new client query. This * allows to enable the profiler for some time, have it save the * stats in the permanent tables, then turn it off again. At that * moment, we want to release all profiler resources. */ if (functions_hash != NULL) init_hash_tables(); return; } /* * Anonymous code blocks do not have function source code * that we can lookup in pg_proc. For now we ignore them. */ if (func->fn_oid == InvalidOid) return; /* Tell collect_data() that new information has arrived locally. */ have_new_local_data = true; /* * Search for this function in our line stats hash table. Create the * entry if it does not exist yet. */ linestats_key.db_oid = MyDatabaseId; linestats_key.fn_oid = func->fn_oid; linestats_entry = (linestatsEntry *)hash_search(functions_hash, &linestats_key, HASH_ENTER, &linestats_found); if (linestats_entry == NULL) elog(ERROR, "plprofiler out of memory"); if (!linestats_found) { /* New function, initialize entry. */ MemoryContext old_context; HeapTuple proc_tuple; char *proc_src; char *func_name; proc_src = find_source( func->fn_oid, &proc_tuple, &func_name ); linestats_entry->line_count = count_source_lines(proc_src) + 1; old_context = MemoryContextSwitchTo(profiler_mcxt); linestats_entry->line_info = palloc0(linestats_entry->line_count * sizeof(linestatsLineInfo)); MemoryContextSwitchTo(old_context); ReleaseSysCache(proc_tuple); } /* * The PL/pgSQL interpreter provides a void pointer (in each stack frame) * that's reserved for plugins. We allocate a profilerInfo structure and * record it's address in that pointer so we can keep some per-invocation * information. */ profiler_info = (profilerInfo *)palloc(sizeof(profilerInfo )); profiler_info->fn_oid = func->fn_oid; profiler_info->line_count = linestats_entry->line_count; profiler_info->line_info = palloc0(profiler_info->line_count * sizeof(profilerLineInfo)); estate->plugin_info = profiler_info; } /* ------------------------------------------------------------------- * profiler_func_beg() * * This hook function is called by the PL/pgSQL interpreter when a * new function is starting. Specifically, this instrumentation * hook is called after values have been assigned to all local * variables (and all function parameters). * * 'estate' points to the stack frame for this function, 'func' * points to the definition of the function * ------------------------------------------------------------------- */ static void profiler_func_beg(PLpgSQL_execstate *estate, PLpgSQL_function *func) { if (!profiler_active) return; /* Ignore anonymous code block. */ if (estate->plugin_info == NULL) return; /* * Push this function Oid onto the stack, remember the entry time and * set the time spent in children to zero. */ callgraph_push(func->fn_oid); } /* ------------------------------------------------------------------- * profiler_func_end() * * This hook function is called by the PL/pgSQL interpreter when a * function runs to completion. * ------------------------------------------------------------------- */ static void profiler_func_end(PLpgSQL_execstate *estate, PLpgSQL_function *func) { profilerInfo *profiler_info; linestatsHashKey key; linestatsEntry *entry; int i; if (!profiler_active) return; /* Ignore anonymous code block. */ if (estate->plugin_info == NULL) return; /* Tell collect_data() that new information has arrived locally. */ have_new_local_data = true; /* Find the linestats hash table entry for this function. */ profiler_info = (profilerInfo *) estate->plugin_info; key.db_oid = MyDatabaseId; key.fn_oid = func->fn_oid; entry = (linestatsEntry *)hash_search(functions_hash, &key, HASH_FIND, NULL); if (!entry) { elog(DEBUG1, "plprofiler: local linestats entry for fn_oid %u " "not found", func->fn_oid); return; } /* Loop through each line of source code and update the stats */ for(i = 1; i < profiler_info->line_count; i++) { entry->line_info[i].exec_count += profiler_info->line_info[i].exec_count; entry->line_info[i].us_total += profiler_info->line_info[i].us_total; if (profiler_info->line_info[i].us_max > entry->line_info[i].us_max) entry->line_info[i].us_max = profiler_info->line_info[i].us_max; } /* * Pop the call stack. This also does the time accounting * for call graphs. */ callgraph_pop(func->fn_oid); /* * Finally if a plprofiler.collect_interval is configured, save and reset * the stats if the interval has elapsed. */ if (profiler_shared_state != NULL && (profiler_shared_state->profiler_enabled_global || MyProcPid == profiler_shared_state->profiler_enabled_pid) && profiler_shared_state->profiler_collect_interval > 0) { time_t now = time(NULL); if (now >= last_collect_time + profiler_shared_state->profiler_collect_interval) { profiler_collect_data(); last_collect_time = now; } } } /* ------------------------------------------------------------------- * profiler_stmt_beg() * * This hook function is called by the PL/pgSQL interpreter just before * executing a statement (stmt). * * Prior to executing each statement, we record the current time and * the current values of all of the performance counters. * ------------------------------------------------------------------- */ static void profiler_stmt_beg(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt) { profilerLineInfo *line_info; profilerInfo *profiler_info; if (!profiler_active) return; /* Ignore anonymous code block. */ if (estate->plugin_info == NULL) return; /* Set the start time of the statement */ profiler_info = (profilerInfo *)estate->plugin_info; if (stmt->lineno < profiler_info->line_count) { line_info = profiler_info->line_info + stmt->lineno; INSTR_TIME_SET_CURRENT(line_info->start_time); } /* Check the call graph stack. */ callgraph_check(profiler_info->fn_oid); } /* ------------------------------------------------------------------- * profiler_stmt_end() * * This hook function is called by the PL/pgSQL interpreter just after * it executes a statement (stmt). * * We use this hook to 'delta' the before and after performance counters * and record the differences in the profilerStmtInfo structure associated * with this statement. * ------------------------------------------------------------------- */ static void profiler_stmt_end(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt) { profilerLineInfo *line_info; profilerInfo *profiler_info; instr_time end_time; uint64 elapsed; if (!profiler_active) return; /* Ignore anonymous code block. */ if (estate->plugin_info == NULL) return; profiler_info = (profilerInfo *)estate->plugin_info; /* * Ignore out of bounds line numbers. Someone is apparently * profiling while executing DDL ... not much use in that. */ if (stmt->lineno >= profiler_info->line_count) return; /* Tell collect_data() that new information has arrived locally. */ have_new_local_data = true; line_info = profiler_info->line_info + stmt->lineno; INSTR_TIME_SET_CURRENT(end_time); INSTR_TIME_SUBTRACT(end_time, line_info->start_time); elapsed = INSTR_TIME_GET_MICROSEC(end_time); if (elapsed > line_info->us_max) line_info->us_max = elapsed; line_info->us_total += elapsed; line_info->exec_count++; } /********************************************************************** * Helper functions **********************************************************************/ /* ------------------------------------------------------------------- * init_hash_tables() * * Initialize hash table * ------------------------------------------------------------------- */ static void init_hash_tables(void) { HASHCTL hash_ctl; /* Create the memory context for our data */ if (profiler_mcxt != NULL) { if (profiler_mcxt->isReset) return; MemoryContextReset(profiler_mcxt); } else { profiler_mcxt = AllocSetContextCreate(TopMemoryContext, "PL/pgSQL profiler", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); } /* Create the hash table for line stats */ MemSet(&hash_ctl, 0, sizeof(hash_ctl)); hash_ctl.keysize = sizeof(linestatsHashKey); hash_ctl.entrysize = sizeof(linestatsEntry); hash_ctl.hash = line_hash_fn; hash_ctl.match = line_match_fn; hash_ctl.hcxt = profiler_mcxt; functions_hash = hash_create("Function Lines", 10000, &hash_ctl, HASH_ELEM | HASH_FUNCTION | HASH_COMPARE); /* Create the hash table for call stats */ MemSet(&hash_ctl, 0, sizeof(hash_ctl)); hash_ctl.keysize = sizeof(callGraphKey); hash_ctl.entrysize = sizeof(callGraphEntry); hash_ctl.hash = callgraph_hash_fn; hash_ctl.match = callgraph_match_fn; hash_ctl.hcxt = profiler_mcxt; callgraph_hash = hash_create("Function Call Graphs", 1000, &hash_ctl, HASH_ELEM | HASH_FUNCTION | HASH_COMPARE); } #if PG_VERSION_NUM >= 150000 static void profiler_shmem_request(void) { if (prev_shmem_request_hook) prev_shmem_request_hook(); RequestAddinShmemSpace(profiler_shmem_size()); RequestNamedLWLockTranche("plprofiler", 1); } #endif static void profiler_shmem_startup(void) { bool found; profilerSharedState *plpss; Size plpss_size = 0; HASHCTL hash_ctl; if (prev_shmem_startup_hook) prev_shmem_startup_hook(); /* Reset in case of restart inside of the postmaster. */ profiler_shared_state = NULL; functions_shared = NULL; callgraph_shared = NULL; LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); /* Create or attach to the shared state */ plpss_size = add_size(plpss_size, offsetof(profilerSharedState, line_info)); plpss_size = add_size(plpss_size, sizeof(linestatsLineInfo) * profiler_max_lines); profiler_shared_state = ShmemInitStruct("plprofiler state", plpss_size, &found); plpss = profiler_shared_state; if (!found) { memset(plpss, 0, offsetof(profilerSharedState, line_info) + sizeof(linestatsLineInfo) * profiler_max_lines); plpss->lock = &(GetNamedLWLockTranche("plprofiler"))->lock; } /* (Re)Initialize local hash tables. */ init_hash_tables(); /* Create or attache to the shared functions hash table */ memset(&hash_ctl, 0, sizeof(hash_ctl)); hash_ctl.keysize = sizeof(linestatsHashKey); hash_ctl.entrysize = sizeof(linestatsEntry); hash_ctl.hash = line_hash_fn; hash_ctl.match = line_match_fn; functions_shared = ShmemInitHash("plprofiler functions", profiler_max_functions, profiler_max_functions, &hash_ctl, HASH_ELEM | HASH_FUNCTION | HASH_COMPARE); /* Create or attache to the shared callgraph hash table */ memset(&hash_ctl, 0, sizeof(hash_ctl)); hash_ctl.keysize = sizeof(callGraphKey); hash_ctl.entrysize = sizeof(callGraphEntry); hash_ctl.hash = callgraph_hash_fn; hash_ctl.match = callgraph_match_fn; callgraph_shared = ShmemInitHash("plprofiler callgraph", profiler_max_callgraph, profiler_max_callgraph, &hash_ctl, HASH_ELEM | HASH_FUNCTION | HASH_COMPARE); LWLockRelease(AddinShmemInitLock); } /* ------------------------------------------------------------------- * find_source() * * This function locates and returns a pointer to a null-terminated string * that contains the source code for the given function. * * In addition to returning a pointer to the requested source code, this * function sets *tup to point to a HeapTuple (that you must release when * you are finished with it) and sets *funcName to point to the name of * the given function. * ------------------------------------------------------------------- */ static char * find_source(Oid oid, HeapTuple *tup, char **funcName) { bool isNull; *tup = SearchSysCache(PROCOID, ObjectIdGetDatum(oid), 0, 0, 0); if(!HeapTupleIsValid(*tup)) elog(ERROR, "plprofiler: cache lookup for function %u failed", oid); if (funcName != NULL) *funcName = NameStr(((Form_pg_proc)GETSTRUCT(*tup))->proname); return DatumGetCString(DirectFunctionCall1(textout, SysCacheGetAttr(PROCOID, *tup, Anum_pg_proc_prosrc, &isNull))); } /* ------------------------------------------------------------------- * count_source_lines() * * This function scans through the source code for a given function * and counts the number of lines of code present in the string. * ------------------------------------------------------------------- */ static int count_source_lines(const char *src) { int line_count = 1; const char *cp = src; while(cp != NULL) { line_count++; cp = strchr(cp, '\n'); if (cp) cp++; } return line_count; } static uint32 line_hash_fn(const void *key, Size keysize) { const linestatsHashKey *k = (const linestatsHashKey *) key; return hash_uint32((uint32) k->fn_oid) ^ hash_uint32((uint32) k->db_oid); } static int line_match_fn(const void *key1, const void *key2, Size keysize) { const linestatsHashKey *k1 = (const linestatsHashKey *)key1; const linestatsHashKey *k2 = (const linestatsHashKey *)key2; if (k1->fn_oid == k2->fn_oid && k1->db_oid == k2->db_oid) return 0; else return 1; } static uint32 callgraph_hash_fn(const void *key, Size keysize) { return hash_any(key, keysize); } static int callgraph_match_fn(const void *key1, const void *key2, Size keysize) { callGraphKey *stack1 = (callGraphKey *)key1; callGraphKey *stack2 = (callGraphKey *)key2; int i; if (stack1->db_oid != stack2->db_oid) return 1; for (i = 0; i < PL_MAX_STACK_DEPTH && stack1->stack[i] != InvalidOid; i++) if (stack1->stack[i] != stack2->stack[i]) return 1; return 0; } static void callgraph_push(Oid func_oid) { /* * We only track function Oids in the call stack up to PL_MAX_STACK_DEPTH. * Beyond that we just count the current stack depth. */ if (graph_stack_pt < PL_MAX_STACK_DEPTH) { /* * Push this function Oid onto the stack, remember the entry time and * set the time spent in children to zero. */ graph_stack.stack[graph_stack_pt] = func_oid; INSTR_TIME_SET_CURRENT(graph_stack_entry[graph_stack_pt]); graph_stack_child_time[graph_stack_pt] = 0; } graph_stack_pt++; } static void callgraph_pop_one(void) { instr_time now; uint64 us_elapsed; uint64 us_self; linestatsHashKey key; linestatsEntry *entry; /* Check for call stack underrun. */ if (graph_stack_pt <= 0) { elog(DEBUG1, "plprofiler: call graph stack underrun"); return; } /* Remove one level from the call stack. */ graph_stack_pt--; /* Calculate the time spent in this function and record it. */ INSTR_TIME_SET_CURRENT(now); INSTR_TIME_SUBTRACT(now, graph_stack_entry[graph_stack_pt]); us_elapsed = INSTR_TIME_GET_MICROSEC(now); us_self = us_elapsed - graph_stack_child_time[graph_stack_pt]; callgraph_collect(us_elapsed, us_self, graph_stack_child_time[graph_stack_pt]); /* If we have a caller, add our own time to the time of its children. */ if (graph_stack_pt > 0) graph_stack_child_time[graph_stack_pt - 1] += us_elapsed; /* * We also collect per function global counts in the pseudo line number * zero. The line stats are cumulative (for example a FOR ... LOOP * statement has the entire execution time of all statements in its * block), so this can't be derived from the actual per line data. */ key.fn_oid = graph_stack.stack[graph_stack_pt]; key.db_oid = MyDatabaseId; entry = (linestatsEntry *)hash_search(functions_hash, &key, HASH_FIND, NULL); if (entry) { entry->line_info[0].exec_count += 1; entry->line_info[0].us_total += us_elapsed; if (us_elapsed > entry->line_info[0].us_max) entry->line_info[0].us_max = us_elapsed; } else { elog(DEBUG1, "plprofiler: local linestats entry for fn_oid %u " "not found", graph_stack.stack[graph_stack_pt]); } /* Zap the oid from the call stack. */ graph_stack.stack[graph_stack_pt] = InvalidOid; } static void callgraph_pop(Oid func_oid) { callgraph_check(func_oid); callgraph_pop_one(); } static void callgraph_check(Oid func_oid) { /* * Unwind the call stack until our own func_oid appears on the top. * * In case of an exception, the pl executor does not call the * func_end callback, so we record now as the end of the function * calls, that were left on the stack. */ while (graph_stack_pt > 0 && graph_stack.stack[graph_stack_pt - 1] != func_oid) { elog(DEBUG1, "plprofiler: unwinding excess call graph stack entry for %u in %u", graph_stack.stack[graph_stack_pt - 1], func_oid); callgraph_pop_one(); } } static void callgraph_collect(uint64 us_elapsed, uint64 us_self, uint64 us_children) { callGraphEntry *entry; bool found; graph_stack.db_oid = MyDatabaseId; entry = (callGraphEntry *)hash_search(callgraph_hash, &graph_stack, HASH_ENTER, &found); if (!found) { entry->callCount = 1; entry->totalTime = us_elapsed; entry->childTime = us_children; entry->selfTime = us_self; } else { entry->callCount++; entry->totalTime = entry->totalTime + us_elapsed; entry->childTime = entry->childTime + us_children; entry->selfTime = entry->selfTime + us_self; } } static int32 profiler_collect_data(void) { HASH_SEQ_STATUS hash_seq; callGraphEntry *cge1; callGraphEntry *cge2; linestatsEntry *lse1; linestatsEntry *lse2; profilerSharedState *plpss = profiler_shared_state; bool have_exclusive_lock = false; bool found; int i; /* * Return without doing anything if the plprofiler extension * was not loaded via shared_preload_libraries. We don't have * any shared memory state in that case. */ if (plpss == NULL) return -1; /* * Don't waste any time here if there was no new data recorded * since the last collect_data() call. */ if (!have_new_local_data) return 0; have_new_local_data = false; /* * Acquire a shared lock on the shared hash tables. We escalate * to an exclusive lock later in case we need to add a new entry. */ LWLockAcquire(plpss->lock, LW_SHARED); /* Collect the callgraph data into shared memory. */ hash_seq_init(&hash_seq, callgraph_hash); while ((cge1 = hash_seq_search(&hash_seq)) != NULL) { cge2 = hash_search(callgraph_shared, &(cge1->key), HASH_FIND, NULL); if (cge2 == NULL) { /* * This callgraph is not yet known in shared memory. * Need to escalate the lock to exclusive. */ if (!have_exclusive_lock) { LWLockRelease(plpss->lock); LWLockAcquire(plpss->lock, LW_EXCLUSIVE); have_exclusive_lock = true; } cge2 = hash_search(callgraph_shared, &(cge1->key), HASH_ENTER, &found); if (cge2 == NULL) { /* * This means that we are out of shared memory for the * callgraph_shared hash table. Nothing we can do * here but complain. */ if (!plpss->callgraph_overflow) { elog(LOG, "plprofiler: entry limit reached for " "shared memory call graph data"); plpss->callgraph_overflow = true; } break; } /* * Since we released the lock above for lock escalation to * exclusive, it is possible that someone else in the meantime * created the entry for this call graph. */ if (!found) { /* * We created a new entry for this call graph in the * shared hash table. Initialize it. */ /* memcpy(&(cge2->key), &(cge1->key), sizeof(callGraphKey)); */ SpinLockInit(&(cge2->mutex)); cge2->callCount = 0; cge2->totalTime = 0; cge2->childTime = 0; cge2->selfTime = 0; } } /* * At this point we have the local entry in cge1 and the shared * entry in cge2. Since we may still only hold a shared lock on * the shared state, use a spinlock on the shared entry while * adding the counters. Then reset our local counters to zero. */ SpinLockAcquire(&(cge2->mutex)); cge2->callCount += cge1->callCount; cge2->totalTime += cge1->totalTime; cge2->childTime += cge1->childTime; cge2->selfTime += cge1->selfTime ; SpinLockRelease(&(cge2->mutex)); cge1->callCount = 0; cge1->totalTime = 0; cge1->childTime = 0; cge1->selfTime = 0; } /* Collect the linestats data into shared memory. */ hash_seq_init(&hash_seq, functions_hash); while ((lse1 = hash_seq_search(&hash_seq)) != NULL) { lse2 = hash_search(functions_shared, &(lse1->key), HASH_FIND, NULL); if (lse2 == NULL) { /* * This function is not yet known in shared memory. * Need to escalate the lock to exclusive. */ if (!have_exclusive_lock) { LWLockRelease(plpss->lock); LWLockAcquire(plpss->lock, LW_EXCLUSIVE); have_exclusive_lock = true; } lse2 = hash_search(functions_shared, &(lse1->key), HASH_ENTER, &found); if (lse2 == NULL) { /* * This means that we are out of shared memory for the * functions_shared hash table. Nothing we can do * here but complain. */ if (!plpss->functions_overflow) { elog(LOG, "plprofiler: entry limit reached for " "shared memory functions data"); plpss->functions_overflow = true; } break; } if (memcmp(&(lse2->key), &(lse1->key), sizeof(linestatsHashKey)) != 0) { elog(FATAL, "key of new hash entry doesn't match"); } /* * Since we released the lock above for lock escalation to * exclusive, it is possible that someone else in the meantime * created the entry for this function. */ if (!found) { /* * We created a new entry for this function in the * shared hash table. Initialize it. We also need to * allocate the per line counters here. If we run out * of per line counters in the shared state, we don't * keep count for any lines of this function at all. */ SpinLockInit(&(lse2->mutex)); if (lse1->line_count <= profiler_max_lines - plpss->lines_used) { lse2->line_count = lse1->line_count; lse2->line_info = &(plpss->line_info[plpss->lines_used]); plpss->lines_used += lse1->line_count; memset(lse2->line_info, 0, sizeof(linestatsLineInfo) * lse1->line_count); } else { if (!plpss->lines_overflow) { elog(LOG, "plprofiler: entry limit reached for " "shared memory per source line data"); plpss->lines_overflow = true; } lse2->line_count = 0; lse2->line_info = NULL; } } } /* * At this point we have the local entry in lse1 and the shared * entry in lse2. Since we may still only hold a shared lock on * the shared state, use a spinlock on the shared entry while * adding the counters. */ SpinLockAcquire(&(lse2->mutex)); for (i = 0; i < lse1->line_count && i < lse2->line_count; i++) { if (lse1->line_info[i].us_max > lse2->line_info[i].us_max) lse2->line_info[i].us_max = lse1->line_info[i].us_max; lse2->line_info[i].us_total += lse1->line_info[i].us_total; lse2->line_info[i].exec_count += lse1->line_info[i].exec_count; } SpinLockRelease(&(lse2->mutex)); memset(lse1->line_info, 0, sizeof(linestatsLineInfo) * lse1->line_count); } /* All done, release the lock. */ LWLockRelease(plpss->lock); return 0; } static void profiler_xact_callback(XactEvent event, void *arg) { Assert(profiler_shared_state != NULL); /* Collect the statistics if needed. */ if (profiler_active && profiler_shared_state->profiler_collect_interval > 0) { switch (event) { case XACT_EVENT_COMMIT: case XACT_EVENT_ABORT: case XACT_EVENT_PARALLEL_COMMIT: case XACT_EVENT_PARALLEL_ABORT: profiler_collect_data(); break; default: break; } } /* Tell func_init that we need to evaluate the new active state. */ profiler_first_call_in_xact = true; /* We can also unwind the callstack here in case of abort. */ callgraph_check(InvalidOid); } /********************************************************************** * SQL callable functions **********************************************************************/ /* ------------------------------------------------------------------- * pl_profiler_get_stack(stack oid[]) * * Converts a stack in Oid[] format into a text[]. * ------------------------------------------------------------------- */ Datum pl_profiler_get_stack(PG_FUNCTION_ARGS) { ArrayType *stack_in = PG_GETARG_ARRAYTYPE_P(0); Datum *stack_oid; bool *nulls; int nelems; int i; Datum *funcdefs; char funcdef_buf[100 + NAMEDATALEN * 2]; /* Take the array apart */ deconstruct_array(stack_in, OIDOID, sizeof(Oid), true, 'i', &stack_oid, &nulls, &nelems); /* Allocate the Datum array for the individual function signatures. */ funcdefs = palloc(sizeof(Datum) * nelems); /* * Turn each of the function Oids, that are in the array, into * a text that is "schema.funcname(funcargs) oid=funcoid". */ for (i = 0; i < nelems; i++) { char *funcname; char *nspname; funcname = get_func_name(DatumGetObjectId(stack_oid[i])); if (funcname != NULL) { nspname = get_namespace_name(get_func_namespace(DatumGetObjectId(stack_oid[i]))); if (nspname == NULL) nspname = pstrdup(""); } else { nspname = pstrdup(""); funcname = pstrdup(""); } snprintf(funcdef_buf, sizeof(funcdef_buf), "%s.%s() oid=%u", nspname, funcname, DatumGetObjectId(stack_oid[i])); pfree(nspname); pfree(funcname); funcdefs[i] = PointerGetDatum(cstring_to_text(funcdef_buf)); } /* Return the texts as a text[]. */ PG_RETURN_ARRAYTYPE_P(construct_array(funcdefs, nelems, TEXTOID, -1, false, 'i')); } /* ------------------------------------------------------------------- * pl_profiler_linestats_local() * * Returns the content of the local line stats hash table * as a set of rows. * ------------------------------------------------------------------- */ Datum pl_profiler_linestats_local(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; HASH_SEQ_STATUS hash_seq; linestatsEntry *entry; /* check to see if caller supports us returning a tuplestore */ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context " "that cannot accept a set"))); if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not " "allowed in this context"))); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); tupstore = tuplestore_begin_heap(true, false, work_mem); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; MemoryContextSwitchTo(oldcontext); if (functions_hash != NULL) { hash_seq_init(&hash_seq, functions_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { int64 lno; for (lno = 0; lno < entry->line_count; lno++) { Datum values[PL_PROFILE_COLS]; bool nulls[PL_PROFILE_COLS]; int i = 0; /* Include this entry in the result. */ MemSet(values, 0, sizeof(values)); MemSet(nulls, 0, sizeof(nulls)); values[i++] = ObjectIdGetDatum(entry->key.fn_oid); values[i++] = Int64GetDatumFast(lno); values[i++] = Int64GetDatumFast(entry->line_info[lno].exec_count); values[i++] = Int64GetDatumFast(entry->line_info[lno].us_total); values[i++] = Int64GetDatumFast(entry->line_info[lno].us_max); Assert(i == PL_PROFILE_COLS); tuplestore_putvalues(tupstore, tupdesc, values, nulls); } } } PG_RETURN_VOID(); } /* ------------------------------------------------------------------- * pl_profiler_linestats_shared() * * Returns the content of the shared line stats hash table * as a set of rows. * ------------------------------------------------------------------- */ Datum pl_profiler_linestats_shared(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; HASH_SEQ_STATUS hash_seq; linestatsEntry *entry; profilerSharedState *plpss = profiler_shared_state; /* check to see if caller supports us returning a tuplestore */ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context " "that cannot accept a set"))); if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not " "allowed in this context"))); /* Check that plprofiler was loaded via shared_preload_libraries */ if (plpss == NULL) elog(ERROR, "plprofiler was not loaded via shared_preload_libraries"); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); tupstore = tuplestore_begin_heap(true, false, work_mem); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; MemoryContextSwitchTo(oldcontext); /* Place a shared lock on the shared memory data. */ LWLockAcquire(plpss->lock, LW_SHARED); hash_seq_init(&hash_seq, functions_shared); while ((entry = hash_seq_search(&hash_seq)) != NULL) { int64 lno; if (entry->key.db_oid != MyDatabaseId) continue; /* Guard agains concurrent updates of the counters. */ SpinLockAcquire(&(entry->mutex)); for (lno = 0; lno <= entry->line_count; lno++) { Datum values[PL_PROFILE_COLS]; bool nulls[PL_PROFILE_COLS]; int i = 0; /* Include this entry in the result. */ MemSet(values, 0, sizeof(values)); MemSet(nulls, 0, sizeof(nulls)); values[i++] = ObjectIdGetDatum(entry->key.fn_oid); values[i++] = Int64GetDatumFast(lno); values[i++] = Int64GetDatumFast(entry->line_info[lno].exec_count); values[i++] = Int64GetDatumFast(entry->line_info[lno].us_total); values[i++] = Int64GetDatumFast(entry->line_info[lno].us_max); Assert(i == PL_PROFILE_COLS); tuplestore_putvalues(tupstore, tupdesc, values, nulls); } /* Done with the counter access. */ SpinLockRelease(&(entry->mutex)); } /* Release the shared lock on the shared memory data. */ LWLockRelease(plpss->lock); PG_RETURN_VOID(); } /* ------------------------------------------------------------------- * pl_profiler_callgraph_local() * * Returns the content of the local call graph hash table * as a set of rows. * ------------------------------------------------------------------- */ Datum pl_profiler_callgraph_local(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; HASH_SEQ_STATUS hash_seq; callGraphEntry *entry; /* Check to see if caller supports us returning a tuplestore */ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context " "that cannot accept a set"))); if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not " "allowed in this context"))); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); tupstore = tuplestore_begin_heap(true, false, work_mem); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; MemoryContextSwitchTo(oldcontext); if (callgraph_hash != NULL) { hash_seq_init(&hash_seq, callgraph_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { Datum values[PL_CALLGRAPH_COLS]; bool nulls[PL_CALLGRAPH_COLS]; Datum funcdefs[PL_MAX_STACK_DEPTH]; int i = 0; int j = 0; MemSet(values, 0, sizeof(values)); MemSet(nulls, 0, sizeof(nulls)); for (i = 0; i < PL_MAX_STACK_DEPTH && entry->key.stack[i] != InvalidOid; i++) funcdefs[i] = ObjectIdGetDatum(entry->key.stack[i]); values[j++] = PointerGetDatum(construct_array(funcdefs, i, OIDOID, sizeof(Oid), true, 'i')); values[j++] = Int64GetDatumFast(entry->callCount); values[j++] = UInt64GetDatum(entry->totalTime); values[j++] = UInt64GetDatum(entry->childTime); values[j++] = UInt64GetDatum(entry->selfTime); Assert(j == PL_CALLGRAPH_COLS); tuplestore_putvalues(tupstore, tupdesc, values, nulls); } } PG_RETURN_VOID(); } /* ------------------------------------------------------------------- * pl_profiler_callgraph_shared() * * Returns the content of the shared call graph hash table * as a set of rows. * ------------------------------------------------------------------- */ Datum pl_profiler_callgraph_shared(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; HASH_SEQ_STATUS hash_seq; callGraphEntry *entry; profilerSharedState *plpss = profiler_shared_state; /* Check to see if caller supports us returning a tuplestore */ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context " "that cannot accept a set"))); if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not " "allowed in this context"))); /* Check that plprofiler was loaded via shared_preload_libraries */ if (plpss == NULL) elog(ERROR, "plprofiler was not loaded via shared_preload_libraries"); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); tupstore = tuplestore_begin_heap(true, false, work_mem); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; MemoryContextSwitchTo(oldcontext); /* Place a shared lock on the shared memory data. */ LWLockAcquire(plpss->lock, LW_SHARED); hash_seq_init(&hash_seq, callgraph_shared); while ((entry = hash_seq_search(&hash_seq)) != NULL) { Datum values[PL_CALLGRAPH_COLS]; bool nulls[PL_CALLGRAPH_COLS]; Datum funcdefs[PL_MAX_STACK_DEPTH]; int i = 0; int j = 0; /* Only entries of the local database are visible. */ if (entry->key.db_oid != MyDatabaseId) continue; MemSet(values, 0, sizeof(values)); MemSet(nulls, 0, sizeof(nulls)); for (i = 0; i < PL_MAX_STACK_DEPTH && entry->key.stack[i] != InvalidOid; i++) funcdefs[i] = ObjectIdGetDatum(entry->key.stack[i]); values[j++] = PointerGetDatum(construct_array(funcdefs, i, OIDOID, sizeof(Oid), true, 'i')); /* Guard agains concurrent updates of the counters. */ SpinLockAcquire(&(entry->mutex)); values[j++] = Int64GetDatumFast(entry->callCount); values[j++] = UInt64GetDatum(entry->totalTime); values[j++] = UInt64GetDatum(entry->childTime); values[j++] = UInt64GetDatum(entry->selfTime); /* Done with the counter access. */ SpinLockRelease(&(entry->mutex)); Assert(j == PL_CALLGRAPH_COLS); tuplestore_putvalues(tupstore, tupdesc, values, nulls); } /* Release the shared lock on the shared memory data. */ LWLockRelease(plpss->lock); PG_RETURN_VOID(); } /* ------------------------------------------------------------------- * pl_profiler_func_oids_local() * * Returns an array of all function Oids that we have * linestat information for in the local hash table. * ------------------------------------------------------------------- */ Datum pl_profiler_func_oids_local(PG_FUNCTION_ARGS) { int i = 0; Datum *result; HASH_SEQ_STATUS hash_seq; linestatsEntry *entry; /* First pass to count the number of Oids, we will return. */ if (functions_hash != NULL) { hash_seq_init(&hash_seq, functions_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) i++; } /* Allocate Oid array for result. */ if (i == 0) { result = palloc(sizeof(Datum)); } else { result = palloc(sizeof(Datum) * i); } if (result == NULL) elog(ERROR, "out of memory in pl_profiler_func_oids_local()"); /* Second pass to collect the Oids. */ if (functions_hash != NULL) { i = 0; hash_seq_init(&hash_seq, functions_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) result[i++] = ObjectIdGetDatum(entry->key.fn_oid); } /* Build and return the actual array. */ PG_RETURN_ARRAYTYPE_P(construct_array(result, i, OIDOID, sizeof(Oid), true, 'i')); } /* ------------------------------------------------------------------- * pl_profiler_func_oids_shared() * * Returns an array of all function Oids that we have * linestat information for in the shared hash table. * ------------------------------------------------------------------- */ Datum pl_profiler_func_oids_shared(PG_FUNCTION_ARGS) { int i = 0; Datum *result; HASH_SEQ_STATUS hash_seq; linestatsEntry *entry; profilerSharedState *plpss = profiler_shared_state; /* Check that plprofiler was loaded via shared_preload_libraries */ if (plpss == NULL) elog(ERROR, "plprofiler was not loaded via shared_preload_libraries"); /* Place a shared lock on the shared memory data. */ LWLockAcquire(plpss->lock, LW_SHARED); /* First pass to count the number of Oids, we will return. */ hash_seq_init(&hash_seq, functions_shared); while ((entry = hash_seq_search(&hash_seq)) != NULL) { if (entry->key.db_oid == MyDatabaseId) i++; } /* Allocate Oid array for result. */ if (i == 0) { result = palloc(sizeof(Datum)); } else { result = palloc(sizeof(Datum) * i); } if (result == NULL) elog(ERROR, "out of memory in pl_profiler_func_oids_shared()"); /* Second pass to collect the Oids. */ i = 0; hash_seq_init(&hash_seq, functions_shared); while ((entry = hash_seq_search(&hash_seq)) != NULL) { if (entry->key.db_oid == MyDatabaseId) result[i++] = ObjectIdGetDatum(entry->key.fn_oid); } /* Release the shared lock on the shared memory data. */ LWLockRelease(plpss->lock); /* Build and return the actual array. */ PG_RETURN_ARRAYTYPE_P(construct_array(result, i, OIDOID, sizeof(Oid), true, 'i')); } /* ------------------------------------------------------------------- * pl_profiler_funcs_source(func_oids oid[]) * * Return the source code of a number of functions specified by * an input array of Oids. * ------------------------------------------------------------------- */ Datum pl_profiler_funcs_source(PG_FUNCTION_ARGS) { ArrayType *func_oids_in = PG_GETARG_ARRAYTYPE_P(0); Datum *func_oids; bool *nulls; int nelems; ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; int fidx; /* check to see if caller supports us returning a tuplestore */ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context " "that cannot accept a set"))); if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not " "allowed in this context"))); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); tupstore = tuplestore_begin_heap(true, false, work_mem); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; MemoryContextSwitchTo(oldcontext); /* Take the input array apart */ deconstruct_array(func_oids_in, OIDOID, sizeof(Oid), true, 'i', &func_oids, &nulls, &nelems); /* * Turn each of the function Oids, that are in the array, into * a text that is "schema.funcname(funcargs) oid=funcoid". */ for (fidx = 0; fidx < nelems; fidx++) { HeapTuple procTuple; char *procSrc; char *funcName; Datum values[PL_FUNCS_SRC_COLS]; bool nulls[PL_FUNCS_SRC_COLS]; int i = 0; char *cp; char *linestart; int64 line_number = 0; /* Create the line-0 entry. */ MemSet(values, 0, sizeof(values)); MemSet(nulls, 0, sizeof(nulls)); values[i++] = func_oids[fidx]; values[i++] = Int64GetDatumFast(line_number); values[i++] = PointerGetDatum(cstring_to_text("-- Line 0")); Assert(i == PL_FUNCS_SRC_COLS); tuplestore_putvalues(tupstore, tupdesc, values, nulls); line_number++; /* Find the source code and split it. */ procSrc = find_source(func_oids[fidx], &procTuple, &funcName); if (procSrc == NULL) { ReleaseSysCache(procTuple); continue; } /* * The returned procStr is palloc'd, so it is safe to scribble * around in it. */ cp = procSrc; linestart = procSrc; while (cp != NULL) { cp = strchr(cp, '\n'); if (cp != NULL) *cp++ = '\0'; i = 0; values[i++] = func_oids[fidx]; values[i++] = Int64GetDatumFast(line_number); values[i++] = PointerGetDatum(cstring_to_text(linestart)); Assert(i == PL_FUNCS_SRC_COLS); tuplestore_putvalues(tupstore, tupdesc, values, nulls); linestart = cp; line_number++; } ReleaseSysCache(procTuple); pfree(procSrc); } PG_RETURN_VOID(); } /* ------------------------------------------------------------------- * pl_profiler_reset_local() * * Drop all data collected in the local hash tables. * ------------------------------------------------------------------- */ Datum pl_profiler_reset_local(PG_FUNCTION_ARGS) { init_hash_tables(); PG_RETURN_VOID(); } /* ------------------------------------------------------------------- * pl_profiler_reset_shared() * * Drop all data collected in the shared hash tables and the * shared state. * ------------------------------------------------------------------- */ Datum pl_profiler_reset_shared(PG_FUNCTION_ARGS) { HASH_SEQ_STATUS hash_seq; callGraphEntry *lsent; linestatsEntry *cgent; profilerSharedState *plpss = profiler_shared_state; /* Check that plprofiler was loaded via shared_preload_libraries */ if (plpss == NULL) elog(ERROR, "plprofiler was not loaded via shared_preload_libraries"); LWLockAcquire(plpss->lock, LW_EXCLUSIVE); /* Reset the shared state. */ plpss->callgraph_overflow = false; plpss->functions_overflow = false; plpss->lines_overflow = false; plpss->lines_used = 0; /* Delete all entries from the callgraph hash table. */ hash_seq_init(&hash_seq, callgraph_shared); while ((cgent = hash_seq_search(&hash_seq)) != NULL) { hash_search(callgraph_shared, &(cgent->key), HASH_REMOVE, NULL); } /* Delete all entries from the functions hash table. */ hash_seq_init(&hash_seq, functions_shared); while ((lsent = hash_seq_search(&hash_seq)) != NULL) { hash_search(functions_shared, &(lsent->key), HASH_REMOVE, NULL); } LWLockRelease(plpss->lock); PG_RETURN_VOID(); } /* ------------------------------------------------------------------- * pl_profiler_set_enabled_global() * * Turn global profiling on or off. * ------------------------------------------------------------------- */ Datum pl_profiler_set_enabled_global(PG_FUNCTION_ARGS) { if (PG_ARGISNULL(0)) PG_RETURN_NULL(); if (profiler_shared_state == NULL) elog(ERROR, "plprofiler not loaded via shared_preload_libraries"); else profiler_shared_state->profiler_enabled_global = PG_GETARG_BOOL(0); PG_RETURN_BOOL(profiler_shared_state->profiler_enabled_global); } /* ------------------------------------------------------------------- * pl_profiler_get_enabled_global() * * Report global profiling state. * ------------------------------------------------------------------- */ Datum pl_profiler_get_enabled_global(PG_FUNCTION_ARGS) { if (profiler_shared_state == NULL) elog(ERROR, "plprofiler not loaded via shared_preload_libraries"); PG_RETURN_BOOL(profiler_shared_state->profiler_enabled_global); } /* ------------------------------------------------------------------- * pl_profiler_set_enabled_local() * * Turn local profiling on or off. * ------------------------------------------------------------------- */ Datum pl_profiler_set_enabled_local(PG_FUNCTION_ARGS) { if (PG_ARGISNULL(0)) PG_RETURN_NULL(); profiler_enabled_local = PG_GETARG_BOOL(0); PG_RETURN_BOOL(profiler_enabled_local); } /* ------------------------------------------------------------------- * pl_profiler_get_enabled_local() * * Report local profiling state. * ------------------------------------------------------------------- */ Datum pl_profiler_get_enabled_local(PG_FUNCTION_ARGS) { PG_RETURN_BOOL(profiler_enabled_local); } /* ------------------------------------------------------------------- * pl_profiler_set_enabled_pid() * * Turn pid profiling on or off. * ------------------------------------------------------------------- */ Datum pl_profiler_set_enabled_pid(PG_FUNCTION_ARGS) { if (PG_ARGISNULL(0)) PG_RETURN_NULL(); if (profiler_shared_state == NULL) elog(ERROR, "plprofiler not loaded via shared_preload_libraries"); else profiler_shared_state->profiler_enabled_pid = PG_GETARG_INT32(0); PG_RETURN_INT32(profiler_shared_state->profiler_enabled_pid); } /* ------------------------------------------------------------------- * pl_profiler_get_enabled_pid() * * Report pid profiling state. * ------------------------------------------------------------------- */ Datum pl_profiler_get_enabled_pid(PG_FUNCTION_ARGS) { if (profiler_shared_state == NULL) elog(ERROR, "plprofiler not loaded via shared_preload_libraries"); PG_RETURN_INT32(profiler_shared_state->profiler_enabled_pid); } /* ------------------------------------------------------------------- * pl_profiler_set_collect_interval() * * Turn pid profiling on or off. * ------------------------------------------------------------------- */ Datum pl_profiler_set_collect_interval(PG_FUNCTION_ARGS) { if (PG_ARGISNULL(0)) PG_RETURN_NULL(); if (profiler_shared_state == NULL) PG_RETURN_INT32(-1); else profiler_shared_state->profiler_collect_interval = PG_GETARG_INT32(0); PG_RETURN_INT32(profiler_shared_state->profiler_collect_interval); } /* ------------------------------------------------------------------- * pl_profiler_get_collect_interval() * * Report pid profiling state. * ------------------------------------------------------------------- */ Datum pl_profiler_get_collect_interval(PG_FUNCTION_ARGS) { if (profiler_shared_state == NULL) elog(ERROR, "plprofiler not loaded via shared_preload_libraries"); PG_RETURN_INT32(profiler_shared_state->profiler_collect_interval); } /* ------------------------------------------------------------------- * pl_profiler_collect_data() * * SQL level callable function to collect profiling data from the * local tables into the shared hash tables. * ------------------------------------------------------------------- */ Datum pl_profiler_collect_data(PG_FUNCTION_ARGS) { PG_RETURN_INT32(profiler_collect_data()); } /* ------------------------------------------------------------------- * pl_profiler_callgraph_overflow() * * Return the flag callgraph_overflow from the shared state. * ------------------------------------------------------------------- */ Datum pl_profiler_callgraph_overflow(PG_FUNCTION_ARGS) { profilerSharedState *plpss = profiler_shared_state; /* Check that plprofiler was loaded via shared_preload_libraries */ if (plpss == NULL) elog(ERROR, "plprofiler was not loaded via shared_preload_libraries"); PG_RETURN_BOOL(plpss->callgraph_overflow); } /* ------------------------------------------------------------------- * pl_profiler_functions_overflow() * * Return the flag functions_overflow from the shared state. * ------------------------------------------------------------------- */ Datum pl_profiler_functions_overflow(PG_FUNCTION_ARGS) { profilerSharedState *plpss = profiler_shared_state; /* Check that plprofiler was loaded via shared_preload_libraries */ if (plpss == NULL) elog(ERROR, "plprofiler was not loaded via shared_preload_libraries"); PG_RETURN_BOOL(plpss->functions_overflow); } /* ------------------------------------------------------------------- * pl_profiler_lines_overflow() * * Return the flag lines_overflow from the shared state. * ------------------------------------------------------------------- */ Datum pl_profiler_lines_overflow(PG_FUNCTION_ARGS) { profilerSharedState *plpss = profiler_shared_state; /* Check that plprofiler was loaded via shared_preload_libraries */ if (plpss == NULL) elog(ERROR, "plprofiler was not loaded via shared_preload_libraries"); PG_RETURN_BOOL(plpss->lines_overflow); } plprofiler-REL4_2_5/plprofiler.conf.sample000066400000000000000000000007651465735455400207440ustar00rootroot00000000000000# ---- # Settings for plprofiler # ---- #shared_preload_libraries = 'plprofiler' # Add this to be able to use # the .enabled and .enable_pid # options. #plprofiler.max_functions = 2000 # The number of functions that # can be tracked in shared memory. #plprofiler.max_lines = 200000 # The number of source code lines # that can be tracked. #plprofiler.max_callgraphs = 20000 # The number of different call # graphs that can be tracked. plprofiler-REL4_2_5/plprofiler.control000066400000000000000000000002671465735455400202140ustar00rootroot00000000000000# plprofiler extension control file comment = 'server-side support for profiling PL/pgSQL functions' default_version = '4.2' module_pathname = '$libdir/plprofiler' relocatable = true plprofiler-REL4_2_5/plprofiler.h000066400000000000000000000155041465735455400167630ustar00rootroot00000000000000/* ---------------------------------------------------------------------- * * plprofiler.h * * Declarations for profiling plugin for PL/pgSQL instrumentation * * Copyright (c) 2022-2024, pgEdge * Copyright (c) 2014-2022, OSCG-Partners * Copyright (c) 2008-2014, PostgreSQL Global Development Group * Copyright 2006,2007 - EnterpriseDB, Inc. * * Major Change History: * 2012 - Removed from PostgreSQL plDebugger Extension * 2015 - Resurrected as standalone plProfiler by OpenSCG * 2016 - Rewritten as v2 to use shared hash tables, have lower overhead * - v3 Major performance improvements, flame graph UI * * ---------------------------------------------------------------------- */ #ifndef PLPROFILER_H #define PLPROFILER_H #include #include #include #include "access/hash.h" #include "access/htup.h" #include "access/htup_details.h" #include "access/sysattr.h" #include "catalog/indexing.h" #include "catalog/namespace.h" #include "catalog/pg_extension.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "commands/extension.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "pgstat.h" #include "plpgsql.h" #include "storage/ipc.h" #include "storage/spin.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/palloc.h" #include "utils/syscache.h" PG_MODULE_MAGIC; #define PL_PROFILE_COLS 5 #define PL_CALLGRAPH_COLS 5 #define PL_FUNCS_SRC_COLS 3 #define PL_MAX_STACK_DEPTH 200 #define PL_MIN_FUNCTIONS 2000 #define PL_MIN_CALLGRAPH 20000 #define PL_MIN_LINES 200000 #define PL_DBG_PRINT_STACK(_d, _s) do { \ int _i; \ printf("stack %s: db=%d bt=", _d, _s.db_oid); \ for (_i = 0; _i < PL_MAX_STACK_DEPTH && _s.stack[_i] != InvalidOid; _i++) { \ printf("%d,", _s.stack[_i]); \ } \ printf("\n"); \ } while(0); /********************************************************************** * Type and structure definitions **********************************************************************/ /* ---- * profilerLineInfo * * Per source code line stats kept in the profilerInfo below, which * is the data we put into the plugin_info of the executor state. * ---- */ typedef struct { int64 us_max; /* Slowest iteration of this stmt */ int64 us_total; /* Total time spent executing this stmt */ int64 exec_count; /* Number of times we executed this stmt */ instr_time start_time; /* Start time for this statement */ } profilerLineInfo; /* ---- * profilerInfo * * The information we keep in the estate->plugin_info. * ---- */ typedef struct { Oid fn_oid; /* The functions OID */ int line_count; /* Number of lines in this function */ profilerLineInfo *line_info; /* Performance counters for each line */ } profilerInfo; /* ---- * linestatsHashKey * * Hash key for the linestats hash tables (both local and shared). * ---- */ typedef struct { Oid db_oid; /* The OID of the database */ Oid fn_oid; /* The OID of the function */ } linestatsHashKey; /* ---- * linestatsLineInfo * * Per source code line statistics kept in the linestats hash table. * ---- */ typedef struct { int64 us_max; /* Maximum execution time of statement */ int64 us_total; /* Total sum of statement exec time */ int64 exec_count; /* Count of statement executions */ } linestatsLineInfo; /* ---- * linestatsEntry * * Per function data kept in the linestats hash table. * ---- */ typedef struct { linestatsHashKey key; /* hash key of entry */ slock_t mutex; /* Spin lock for updating counters */ int line_count; /* Number of lines in this function */ linestatsLineInfo *line_info; /* Performance counters for each line */ } linestatsEntry; typedef struct callGraphKey { Oid db_oid; Oid stack[PL_MAX_STACK_DEPTH]; } callGraphKey; typedef struct callGraphEntry { callGraphKey key; slock_t mutex; PgStat_Counter callCount; uint64 totalTime; uint64 childTime; uint64 selfTime; } callGraphEntry; typedef struct { LWLockId lock; bool profiler_enabled_global; int profiler_enabled_pid; int profiler_collect_interval; bool callgraph_overflow; bool functions_overflow; bool lines_overflow; int lines_used; linestatsLineInfo line_info[1]; } profilerSharedState; /********************************************************************** * Exported function prototypes **********************************************************************/ void _PG_init(void); void _PG_fini(void); Datum pl_profiler_get_stack(PG_FUNCTION_ARGS); Datum pl_profiler_linestats_local(PG_FUNCTION_ARGS); Datum pl_profiler_linestats_shared(PG_FUNCTION_ARGS); Datum pl_profiler_callgraph_local(PG_FUNCTION_ARGS); Datum pl_profiler_callgraph_shared(PG_FUNCTION_ARGS); Datum pl_profiler_func_oids_local(PG_FUNCTION_ARGS); Datum pl_profiler_func_oids_shared(PG_FUNCTION_ARGS); Datum pl_profiler_funcs_source(PG_FUNCTION_ARGS); Datum pl_profiler_reset_local(PG_FUNCTION_ARGS); Datum pl_profiler_reset_shared(PG_FUNCTION_ARGS); Datum pl_profiler_set_enabled_global(PG_FUNCTION_ARGS); Datum pl_profiler_get_enabled_global(PG_FUNCTION_ARGS); Datum pl_profiler_set_enabled_local(PG_FUNCTION_ARGS); Datum pl_profiler_get_enabled_local(PG_FUNCTION_ARGS); Datum pl_profiler_set_enabled_pid(PG_FUNCTION_ARGS); Datum pl_profiler_get_enabled_pid(PG_FUNCTION_ARGS); Datum pl_profiler_set_collect_interval(PG_FUNCTION_ARGS); Datum pl_profiler_get_collect_interval(PG_FUNCTION_ARGS); Datum pl_profiler_collect_data(PG_FUNCTION_ARGS); Datum pl_profiler_callgraph_overflow(PG_FUNCTION_ARGS); Datum pl_profiler_functions_overflow(PG_FUNCTION_ARGS); Datum pl_profiler_lines_overflow(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(pl_profiler_get_stack); PG_FUNCTION_INFO_V1(pl_profiler_linestats_local); PG_FUNCTION_INFO_V1(pl_profiler_linestats_shared); PG_FUNCTION_INFO_V1(pl_profiler_callgraph_local); PG_FUNCTION_INFO_V1(pl_profiler_callgraph_shared); PG_FUNCTION_INFO_V1(pl_profiler_func_oids_local); PG_FUNCTION_INFO_V1(pl_profiler_func_oids_shared); PG_FUNCTION_INFO_V1(pl_profiler_funcs_source); PG_FUNCTION_INFO_V1(pl_profiler_reset_local); PG_FUNCTION_INFO_V1(pl_profiler_reset_shared); PG_FUNCTION_INFO_V1(pl_profiler_set_enabled_global); PG_FUNCTION_INFO_V1(pl_profiler_get_enabled_global); PG_FUNCTION_INFO_V1(pl_profiler_set_enabled_local); PG_FUNCTION_INFO_V1(pl_profiler_get_enabled_local); PG_FUNCTION_INFO_V1(pl_profiler_set_enabled_pid); PG_FUNCTION_INFO_V1(pl_profiler_get_enabled_pid); PG_FUNCTION_INFO_V1(pl_profiler_set_collect_interval); PG_FUNCTION_INFO_V1(pl_profiler_get_collect_interval); PG_FUNCTION_INFO_V1(pl_profiler_collect_data); PG_FUNCTION_INFO_V1(pl_profiler_callgraph_overflow); PG_FUNCTION_INFO_V1(pl_profiler_functions_overflow); PG_FUNCTION_INFO_V1(pl_profiler_lines_overflow); #endif /* PLPROFILER_H */ plprofiler-REL4_2_5/python-plprofiler/000077500000000000000000000000001465735455400201245ustar00rootroot00000000000000plprofiler-REL4_2_5/python-plprofiler/.gitignore000066400000000000000000000000261465735455400221120ustar00rootroot00000000000000build dist *.egg-info plprofiler-REL4_2_5/python-plprofiler/MANIFEST.in000066400000000000000000000000561465735455400216630ustar00rootroot00000000000000recursive-include plprofiler/lib/FlameGraph * plprofiler-REL4_2_5/python-plprofiler/README.md000066400000000000000000000004231465735455400214020ustar00rootroot00000000000000PL/pgSQL Profiler module and command line tool ============================================== This is the Python module and command line tool to control the PL/pgSQL Profiler extension for PostgreSQL. Please visit https://github.com/bigsql/plprofiler for the main project. plprofiler-REL4_2_5/python-plprofiler/plprofiler/000077500000000000000000000000001465735455400223025ustar00rootroot00000000000000plprofiler-REL4_2_5/python-plprofiler/plprofiler/__init__.py000066400000000000000000000001051465735455400244070ustar00rootroot00000000000000from .plprofiler_tool import main from .plprofiler import plprofiler plprofiler-REL4_2_5/python-plprofiler/plprofiler/lib/000077500000000000000000000000001465735455400230505ustar00rootroot00000000000000plprofiler-REL4_2_5/python-plprofiler/plprofiler/lib/FlameGraph/000077500000000000000000000000001465735455400250565ustar00rootroot00000000000000plprofiler-REL4_2_5/python-plprofiler/plprofiler/lib/FlameGraph/README000066400000000000000000000132651465735455400257450ustar00rootroot00000000000000Flame Graphs visualize profiled code-paths. Website: http://www.brendangregg.com/flamegraphs.html CPU profiling using DTrace, perf_events, SystemTap, or ktap: http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html CPU profiling using XCode Instruments: http://schani.wordpress.com/2012/11/16/flame-graphs-for-instruments/ CPU profiling using Xperf.exe: http://randomascii.wordpress.com/2013/03/26/summarizing-xperf-cpu-usage-with-flame-graphs/ Memory profiling: http://www.brendangregg.com/FlameGraphs/memoryflamegraphs.html These can be created in three steps: 1. Capture stacks 2. Fold stacks 3. flamegraph.pl 1. Capture stacks ================= Stack samples can be captured using DTrace, perf_events, SystemTap, pmcstat, Xperf, Intel VTune, or anything else than can capture full stack samples. Using DTrace to capture 60 seconds of kernel stacks at 997 Hertz: # dtrace -x stackframes=100 -n 'profile-997 /arg0/ { @[stack()] = count(); } tick-60s { exit(0); }' -o out.kern_stacks Using DTrace to capture 60 seconds of user-level stacks for PID 12345 at 97 Hertz: # dtrace -x ustackframes=100 -n 'profile-97 /pid == 12345 && arg1/ { @[ustack()] = count(); } tick-60s { exit(0); }' -o out.user_stacks Using DTrace to capture 60 seconds of user-level stacks, including while time is spent in the kernel, for PID 12345 at 97 Hertz: # dtrace -x ustackframes=100 -n 'profile-97 /pid == 12345/ { @[ustack()] = count(); } tick-60s { exit(0); }' -o out.user_stacks Switch ustack() for jstack() if the application has a ustack helper to include translated frames (eg, node.js frames; see: http://dtrace.org/blogs/dap/2012/01/05/where-does-your-node-program-spend-its-time/). The rate for user-level stack collection is deliberately slower than kernel, which is especially important when using jstack() as it performs additional work to translate frames. 2. Fold stacks ============== Use the stackcollapse programs to fold stack samples into single lines. The programs provided are: - stackcollapse.pl: for DTrace stacks - stackcollapse-perf.pl: for perf_events "perf script" output - stackcollapse-pmc.pl: for FreeBSD pmcstat -G stacks - stackcollapse-stap.pl: for SystemTap stacks - stackcollapse-instruments.pl: for XCode Instruments Usage example: $ ./stackcollapse.pl out.kern_stacks > out.kern_folded The output looks like this: unix`_sys_sysenter_post_swapgs 1401 unix`_sys_sysenter_post_swapgs;genunix`close 5 unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf 85 unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf;c2audit`audit_closef 26 unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf;c2audit`audit_setf 5 unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf;genunix`audit_getstate 6 unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf;genunix`audit_unfalloc 2 unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf;genunix`closef 48 [...] 3. flamegraph.pl ================ Use flamegraph.pl to render a SVG. $ ./flamegraph.pl out.kern_folded > kernel.svg An advantage of having the folded input file (and why this is separate to flamegraph.pl) is that you can use grep for functions of interest. Eg: $ grep cpuid out.kern_folded | ./flamegraph.pl > cpuid.svg Provided Example ================ An example output from DTrace is included, both the captured stacks and the resulting Flame Graph. You can generate it yourself using: $ ./stackcollapse.pl example-stacks.txt | ./flamegraph.pl > example.svg This was from a particular performance investigation: the Flame Graph identified that CPU time was spent in the lofs module, and quantified that time. Options ======= See the USAGE message (--help) for options: USAGE: ./flamegraph.pl [options] infile > outfile.svg --titletext # change title text --width # width of image (default 1200) --height # height of each frame (default 16) --minwidth # omit smaller functions (default 0.1 pixels) --fonttype # font type (default "Verdana") --fontsize # font size (default 12) --countname # count type label (default "samples") --nametype # name type label (default "Function:") --colors # "hot", "mem", "io" palette (default "hot") --hash # colors are keyed by function name hash --cp # use consistent palette (palette.map) eg, ./flamegraph.pl --titletext="Flame Graph: malloc()" trace.txt > graph.svg As suggested in the example, flame graphs can process traces of any event, such as malloc()s, provided stack traces are gathered. Consistent Palette ================== If you use the --cp option, it will use the $colors selection and randomly generate the palette like normal. Any future flamegraphs created using the --cp option will use the same palette map. Any new symbols from future flamegraphs will have their colors randomly generated using the $colors selection. If you don't like the palette, just delete the palette.map file. This allows your to change your colorscheme between flamegraphs to make the differences REALLY stand out. Example: Say we have 2 captures, one with a problem, and one when it was working (whatever "it" is): cat working.folded | ./flamegraph.pl --cp > working.svg # this generates a palette.map, as per the normal random generated look. cat broken.folded | ./flamegraph.pl --cp --colors mem > broken.svg # this svg will use the same palette.map for the same events, but a very # different colorscheme for any new events. Take a look at the demo directory for an example: palette-example-working.svg palette-example-broken.svg plprofiler-REL4_2_5/python-plprofiler/plprofiler/lib/FlameGraph/README-plprofiler000066400000000000000000000006271465735455400301170ustar00rootroot00000000000000This directory is a stripped down to the bare minimum version of Brendan D. Gregg's famous FlameGraph script collection. The plprofiler produces the input format of flamegraph.pl. So nothing else from the project is really needed and therfore there is no need to include it in this embedded lib directory. If you are looking for the full FlameGraph package, please visit http://www.brendangregg.com/ plprofiler-REL4_2_5/python-plprofiler/plprofiler/lib/FlameGraph/flamegraph.pl000077500000000000000000000621471465735455400275360ustar00rootroot00000000000000#!/usr/bin/perl -w # # flamegraph.pl flame stack grapher. # # This takes stack samples and renders a call graph, allowing hot functions # and codepaths to be quickly identified. Stack samples can be generated using # tools such as DTrace, perf, SystemTap, and Instruments. # # USAGE: ./flamegraph.pl [options] input.txt > graph.svg # # grep funcA input.txt | ./flamegraph.pl [options] > graph.svg # # Options are listed in the usage message (--help). # # The input is stack frames and sample counts formatted as single lines. Each # frame in the stack is semicolon separated, with a space and count at the end # of the line. These can be generated using DTrace with stackcollapse.pl, # and other tools using the stackcollapse variants. # # An optional extra column of counts can be provided to generate a differential # flame graph of the counts, colored red for more, and blue for less. This # can be useful when using flame graphs for non-regression testing. # See the header comment in the difffolded.pl program for instructions. # # The output graph shows relative presence of functions in stack samples. The # ordering on the x-axis has no meaning; since the data is samples, time order # of events is not known. The order used sorts function names alphabetically. # # While intended to process stack samples, this can also process stack traces. # For example, tracing stacks for memory allocation, or resource usage. You # can use --title to set the title to reflect the content, and --countname # to change "samples" to "bytes" etc. # # There are a few different palettes, selectable using --color. By default, # the colors are selected at random (except for differentials). Functions # called "-" will be printed gray, which can be used for stack separators (eg, # between user and kernel stacks). # # HISTORY # # This was inspired by Neelakanth Nadgir's excellent function_call_graph.rb # program, which visualized function entry and return trace events. As Neel # wrote: "The output displayed is inspired by Roch's CallStackAnalyzer which # was in turn inspired by the work on vftrace by Jan Boerhout". See: # https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and # # Copyright 2011 Joyent, Inc. All rights reserved. # Copyright 2011 Brendan Gregg. All rights reserved. # # CDDL HEADER START # # The contents of this file are subject to the terms of the # Common Development and Distribution License (the "License"). # You may not use this file except in compliance with the License. # # You can obtain a copy of the license at docs/cddl1.txt or # http://opensource.org/licenses/CDDL-1.0. # See the License for the specific language governing permissions # and limitations under the License. # # When distributing Covered Code, include this CDDL HEADER in each # file and include the License file at docs/cddl1.txt. # If applicable, add the following below this CDDL HEADER, with the # fields enclosed by brackets "[]" replaced with your own identifying # information: Portions Copyright [yyyy] [name of copyright owner] # # CDDL HEADER END # # 21-Nov-2013 Shawn Sterling Added consistent palette file option # 17-Mar-2013 Tim Bunce Added options and more tunables. # 15-Dec-2011 Dave Pacheco Support for frames with whitespace. # 10-Sep-2011 Brendan Gregg Created this. use strict; use Getopt::Long; # tunables my $encoding; my $fonttype = "Verdana"; my $imagewidth = 1200; # max width, pixels my $frameheight = 16; # max height is dynamic my $fontsize = 12; # base text size my $fontwidth = 0.59; # avg width relative to fontsize my $minwidth = 0.1; # min function width, pixels my $nametype = "Function:"; # what are the names in the data? my $countname = "samples"; # what are the counts in the data? my $colors = "hot"; # color theme my $bgcolor1 = "#eeeeee"; # background color gradient start my $bgcolor2 = "#eeeeb0"; # background color gradient stop my $nameattrfile; # file holding function attributes my $timemax; # (override the) sum of the counts my $factor = 1; # factor to scale counts by my $hash = 0; # color by function name my $palette = 0; # if we use consistent palettes (default off) my %palette_map; # palette map hash my $pal_file = "palette.map"; # palette map file name my $stackreverse = 0; # reverse stack order, switching merge end my $inverted = 0; # icicle graph my $negate = 0; # switch differential hues my $titletext = ""; # centered heading my $titledefault = "Flame Graph"; # overwritten by --title my $titleinverted = "Icicle Graph"; # " " GetOptions( 'fonttype=s' => \$fonttype, 'width=i' => \$imagewidth, 'height=i' => \$frameheight, 'encoding=s' => \$encoding, 'fontsize=f' => \$fontsize, 'fontwidth=f' => \$fontwidth, 'minwidth=f' => \$minwidth, 'title=s' => \$titletext, 'nametype=s' => \$nametype, 'countname=s' => \$countname, 'nameattr=s' => \$nameattrfile, 'total=s' => \$timemax, 'factor=f' => \$factor, 'colors=s' => \$colors, 'hash' => \$hash, 'cp' => \$palette, 'reverse' => \$stackreverse, 'inverted' => \$inverted, 'negate' => \$negate, ) or die < outfile.svg\n --title # change title text --width # width of image (default 1200) --height # height of each frame (default 16) --minwidth # omit smaller functions (default 0.1 pixels) --fonttype # font type (default "Verdana") --fontsize # font size (default 12) --countname # count type label (default "samples") --nametype # name type label (default "Function:") --colors # set color palette. choices are: hot (default), mem, io, # java, js, red, green, blue, yellow, purple, orange --hash # colors are keyed by function name hash --cp # use consistent palette (palette.map) --reverse # generate stack-reversed flame graph --inverted # icicle graph --negate # switch differential hues (blue<->red) eg, $0 --title="Flame Graph: malloc()" trace.txt > graph.svg USAGE_END # internals my $ypad1 = $fontsize * 4; # pad top, include title my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels my $xpad = 10; # pad lefm and right my $framepad = 1; # vertical padding for frames my $depthmax = 0; my %Events; my %nameattr; if ($titletext eq "") { unless ($inverted) { $titletext = $titledefault; } else { $titletext = $titleinverted; } } if ($nameattrfile) { # The name-attribute file format is a function name followed by a tab then # a sequence of tab separated name=value pairs. open my $attrfh, $nameattrfile or die "Can't read $nameattrfile: $!\n"; while (<$attrfh>) { chomp; my ($funcname, $attrstr) = split /\t/, $_, 2; die "Invalid format in $nameattrfile" unless defined $attrstr; $nameattr{$funcname} = { map { split /=/, $_, 2 } split /\t/, $attrstr }; } } if ($colors eq "mem") { $bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff"; } if ($colors eq "io") { $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8"; } # SVG functions { package SVG; sub new { my $class = shift; my $self = {}; bless ($self, $class); return $self; } sub header { my ($self, $w, $h) = @_; my $enc_attr = ''; if (defined $encoding) { $enc_attr = qq{ encoding="$encoding"}; } $self->{svg} .= < SVG } sub include { my ($self, $content) = @_; $self->{svg} .= $content; } sub colorAllocate { my ($self, $r, $g, $b) = @_; return "rgb($r,$g,$b)"; } sub group_start { my ($self, $attr) = @_; my @g_attr = map { exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : () } qw(class style onmouseover onmouseout onclick); push @g_attr, $attr->{g_extra} if $attr->{g_extra}; $self->{svg} .= sprintf qq/\n/, join(' ', @g_attr); $self->{svg} .= sprintf qq/%s<\/title>/, $attr->{title} if $attr->{title}; # should be first element within g container if ($attr->{href}) { my @a_attr; push @a_attr, sprintf qq/xlink:href="%s"/, $attr->{href} if $attr->{href}; # default target=_top else links will open within SVG push @a_attr, sprintf qq/target="%s"/, $attr->{target} || "_top"; push @a_attr, $attr->{a_extra} if $attr->{a_extra}; $self->{svg} .= sprintf qq//, join(' ', @a_attr); } } sub group_end { my ($self, $attr) = @_; $self->{svg} .= qq/<\/a>\n/ if $attr->{href}; $self->{svg} .= qq/<\/g>\n/; } sub filledRectangle { my ($self, $x1, $y1, $x2, $y2, $fill, $extra) = @_; $x1 = sprintf "%0.1f", $x1; $x2 = sprintf "%0.1f", $x2; my $w = sprintf "%0.1f", $x2 - $x1; my $h = sprintf "%0.1f", $y2 - $y1; $extra = defined $extra ? $extra : ""; $self->{svg} .= qq/\n/; } sub stringTTF { my ($self, $color, $font, $size, $angle, $x, $y, $str, $loc, $extra) = @_; $x = sprintf "%0.2f", $x; $loc = defined $loc ? $loc : "left"; $extra = defined $extra ? $extra : ""; $self->{svg} .= qq/$str<\/text>\n/; } sub svg { my $self = shift; return "$self->{svg}\n"; } 1; } sub namehash { # Generate a vector hash for the name string, weighting early over # later characters. We want to pick the same colors for function # names across different flame graphs. my $name = shift; my $vector = 0; my $weight = 1; my $max = 1; my $mod = 10; # if module name present, trunc to 1st char $name =~ s/.(.*?)`//; foreach my $c (split //, $name) { my $i = (ord $c) % $mod; $vector += ($i / ($mod++ - 1)) * $weight; $max += 1 * $weight; $weight *= 0.70; last if $mod > 12; } return (1 - $vector / $max) } sub color { my ($type, $hash, $name) = @_; my ($v1, $v2, $v3); if ($hash) { $v1 = namehash($name); $v2 = $v3 = namehash(scalar reverse $name); } else { $v1 = rand(1); $v2 = rand(1); $v3 = rand(1); } # theme palettes if (defined $type and $type eq "hot") { my $r = 205 + int(50 * $v3); my $g = 0 + int(230 * $v1); my $b = 0 + int(55 * $v2); return "rgb($r,$g,$b)"; } if (defined $type and $type eq "mem") { my $r = 0; my $g = 190 + int(50 * $v2); my $b = 0 + int(210 * $v1); return "rgb($r,$g,$b)"; } if (defined $type and $type eq "io") { my $r = 80 + int(60 * $v1); my $g = $r; my $b = 190 + int(55 * $v2); return "rgb($r,$g,$b)"; } # multi palettes if (defined $type and $type eq "java") { if ($name =~ /::/) { # C++ $type = "yellow"; } elsif ($name =~ m:/:) { # Java (match "/" in path) $type = "green" } else { # system $type = "red"; } # fall-through to color palettes } if (defined $type and $type eq "js") { if ($name =~ /::/) { # C++ $type = "yellow"; } elsif ($name =~ m:/:) { # JavaScript (match "/" in path) $type = "green" } elsif ($name =~ m/:/) { # JavaScript (match ":" in builtin) $type = "aqua" } elsif ($name =~ m/^ $/) { # Missing symbol $type = "green" } else { # system $type = "red"; } # fall-through to color palettes } # color palettes if (defined $type and $type eq "red") { my $r = 200 + int(55 * $v1); my $x = 50 + int(80 * $v1); return "rgb($r,$x,$x)"; } if (defined $type and $type eq "green") { my $g = 200 + int(55 * $v1); my $x = 50 + int(60 * $v1); return "rgb($x,$g,$x)"; } if (defined $type and $type eq "blue") { my $b = 205 + int(50 * $v1); my $x = 80 + int(60 * $v1); return "rgb($x,$x,$b)"; } if (defined $type and $type eq "yellow") { my $x = 175 + int(55 * $v1); my $b = 50 + int(20 * $v1); return "rgb($x,$x,$b)"; } if (defined $type and $type eq "purple") { my $x = 190 + int(65 * $v1); my $g = 80 + int(60 * $v1); return "rgb($x,$g,$x)"; } if (defined $type and $type eq "aqua") { my $r = 50 + int(60 * $v1); my $g = 165 + int(55 * $v1); my $b = 165 + int(55 * $v1); return "rgb($r,$g,$b)"; } if (defined $type and $type eq "orange") { my $r = 190 + int(65 * $v1); my $g = 90 + int(65 * $v1); return "rgb($r,$g,0)"; } return "rgb(0,0,0)"; } sub color_scale { my ($value, $max) = @_; my ($r, $g, $b) = (255, 255, 255); $value = -$value if $negate; if ($value > 0) { $g = $b = int(210 * ($max - $value) / $max); } elsif ($value < 0) { $r = $g = int(210 * ($max + $value) / $max); } return "rgb($r,$g,$b)"; } sub color_map { my ($colors, $func) = @_; if (exists $palette_map{$func}) { return $palette_map{$func}; } else { $palette_map{$func} = color($colors); return $palette_map{$func}; } } sub write_palette { open(FILE, ">$pal_file"); foreach my $key (sort keys %palette_map) { print FILE $key."->".$palette_map{$key}."\n"; } close(FILE); } sub read_palette { if (-e $pal_file) { open(FILE, $pal_file) or die "can't open file $pal_file: $!"; while ( my $line = ) { chomp($line); (my $key, my $value) = split("->",$line); $palette_map{$key}=$value; } close(FILE) } } my %Node; # Hash of merged frame data my %Tmp; # flow() merges two stacks, storing the merged frames and value data in %Node. sub flow { my ($last, $this, $v, $d) = @_; my $len_a = @$last - 1; my $len_b = @$this - 1; my $i = 0; my $len_same; for (; $i <= $len_a; $i++) { last if $i > $len_b; last if $last->[$i] ne $this->[$i]; } $len_same = $i; for ($i = $len_a; $i >= $len_same; $i--) { my $k = "$last->[$i];$i"; # a unique ID is constructed from "func;depth;etime"; # func-depth isn't unique, it may be repeated later. $Node{"$k;$v"}->{stime} = delete $Tmp{$k}->{stime}; if (defined $Tmp{$k}->{delta}) { $Node{"$k;$v"}->{delta} = delete $Tmp{$k}->{delta}; } delete $Tmp{$k}; } for ($i = $len_same; $i <= $len_b; $i++) { my $k = "$this->[$i];$i"; $Tmp{$k}->{stime} = $v; if (defined $d) { $Tmp{$k}->{delta} += $i == $len_b ? $d : 0; } } return $this; } # parse input my @Data; my $last = []; my $time = 0; my $delta = undef; my $ignored = 0; my $line; my $maxdelta = 1; # reverse if needed foreach (<>) { chomp; $line = $_; if ($stackreverse) { # there may be an extra samples column for differentials # XXX todo: redo these REs as one. It's repeated below. my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); my $samples2 = undef; if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { $samples2 = $samples; ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); unshift @Data, join(";", reverse split(";", $stack)) . " $samples $samples2"; } else { unshift @Data, join(";", reverse split(";", $stack)) . " $samples"; } } else { unshift @Data, $line; } } # process and merge frames foreach (sort @Data) { chomp; # process: folded_stack count # eg: func_a;func_b;func_c 31 my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); unless (defined $samples and defined $stack) { ++$ignored; next; } # there may be an extra samples column for differentials: my $samples2 = undef; if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { $samples2 = $samples; ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); } $delta = undef; if (defined $samples2) { $delta = $samples2 - $samples; $maxdelta = abs($delta) if abs($delta) > $maxdelta; } $stack =~ tr/<>/()/; # merge frames and populate %Node: $last = flow($last, [ '', split ";", $stack ], $time, $delta); if (defined $samples2) { $time += $samples2; } else { $time += $samples; } } flow($last, [], $time, $delta); warn "Ignored $ignored lines with invalid format\n" if $ignored; unless ($time) { warn "ERROR: No stack counts found\n"; my $im = SVG->new(); # emit an error message SVG, for tools automating flamegraph use my $imageheight = $fontsize * 5; $im->header($imagewidth, $imageheight); $im->stringTTF($im->colorAllocate(0, 0, 0), $fonttype, $fontsize + 2, 0.0, int($imagewidth / 2), $fontsize * 2, "ERROR: No valid input provided to flamegraph.pl.", "middle"); print $im->svg; exit 2; } if ($timemax and $timemax < $time) { warn "Specified --total $timemax is less than actual total $time, so ignored\n" if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc) undef $timemax; } $timemax ||= $time; my $widthpertime = ($imagewidth - 2 * $xpad) / $timemax; my $minwidth_time = $minwidth / $widthpertime; # prune blocks that are too narrow and determine max depth while (my ($id, $node) = each %Node) { my ($func, $depth, $etime) = split ";", $id; my $stime = $node->{stime}; die "missing start for $id" if not defined $stime; if (($etime-$stime) < $minwidth_time) { delete $Node{$id}; next; } $depthmax = $depth if $depth > $depthmax; } # draw canvas, and embed interactive JavaScript program my $imageheight = ($depthmax * $frameheight) + $ypad1 + $ypad2; my $im = SVG->new(); $im->header($imagewidth, $imageheight); my $inc = < INC $im->include($inc); $im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)'); my ($white, $black, $vvdgrey, $vdgrey) = ( $im->colorAllocate(255, 255, 255), $im->colorAllocate(0, 0, 0), $im->colorAllocate(40, 40, 40), $im->colorAllocate(160, 160, 160), ); $im->stringTTF($black, $fonttype, $fontsize + 5, 0.0, int($imagewidth / 2), $fontsize * 2, $titletext, "middle"); $im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $imageheight - ($ypad2 / 2), " ", "", 'id="details"'); $im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $fontsize * 2, "Reset Zoom", "", 'id="unzoom" onclick="unzoom()" style="opacity:0.0;cursor:pointer"'); if ($palette) { read_palette(); } # draw frames while (my ($id, $node) = each %Node) { my ($func, $depth, $etime) = split ";", $id; my $stime = $node->{stime}; my $delta = $node->{delta}; $etime = $timemax if $func eq "" and $depth == 0; my $x1 = $xpad + $stime * $widthpertime; my $x2 = $xpad + $etime * $widthpertime; my ($y1, $y2); unless ($inverted) { $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad; $y2 = $imageheight - $ypad2 - $depth * $frameheight; } else { $y1 = $ypad1 + $depth * $frameheight; $y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad; } my $samples = sprintf "%.0f", ($etime - $stime) * $factor; (my $samples_txt = $samples) # add commas per perlfaq5 =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g; my $info; if ($func eq "" and $depth == 0) { $info = "all ($samples_txt $countname, 100%)"; } else { my $pct = sprintf "%.2f", ((100 * $samples) / ($timemax * $factor)); my $escaped_func = $func; $escaped_func =~ s/&/&/g; $escaped_func =~ s//>/g; unless (defined $delta) { $info = "$escaped_func ($samples_txt $countname, $pct%)"; } else { my $d = $negate ? -$delta : $delta; my $deltapct = sprintf "%.2f", ((100 * $d) / ($timemax * $factor)); $deltapct = $d > 0 ? "+$deltapct" : $deltapct; $info = "$escaped_func ($samples_txt $countname, $pct%; $deltapct%)"; } } my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone $nameattr->{class} ||= "func_g"; $nameattr->{onmouseover} ||= "s('".$info."')"; $nameattr->{onmouseout} ||= "c()"; $nameattr->{onclick} ||= "zoom(this)"; $nameattr->{title} ||= $info; $im->group_start($nameattr); my $color; if ($func eq "-") { $color = $vdgrey; } elsif (defined $delta) { $color = color_scale($delta, $maxdelta); } elsif ($palette) { $color = color_map($colors, $func); } else { $color = color($colors, $hash, $func); } $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"'); my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth)); my $text = ""; if ($chars >= 3) { # room for one char plus two dots $text = substr $func, 0, $chars; substr($text, -2, 2) = ".." if $chars < length $func; $text =~ s/&/&/g; $text =~ s//>/g; } $im->stringTTF($black, $fonttype, $fontsize, 0.0, $x1 + 3, 3 + ($y1 + $y2) / 2, $text, ""); $im->group_end($nameattr); } print $im->svg; if ($palette) { write_palette(); } # vim: ts=8 sts=8 sw=8 noexpandtab plprofiler-REL4_2_5/python-plprofiler/plprofiler/plprofiler.py000066400000000000000000001244711465735455400250430ustar00rootroot00000000000000# ---------------------------------------------------------------------- # plprofiler_data # # Class handling all the profiler data. # ---------------------------------------------------------------------- import psycopg2 import json import time from .plprofiler_report import plprofiler_report from .sql_split import sql_split __all__ = ['plprofiler', ] class plprofiler: def __init__(self): self.dbconn = None def connect(self, connoptions): # ---- # Connect to the database and get the plprofiler schema name. # ---- if len(connoptions) == 0: connoptions['dsn'] = '' self.dbconn = psycopg2.connect(**connoptions) self.profiler_namespace = self.get_profiler_namespace() def version(self): return 40200 def versionstr(self): return "4.2" def get_profiler_namespace(self): # ---- # Find out the namespace of the plprofiler extension. # ---- cur = self.dbconn.cursor() cur.execute(""" SELECT N.nspname FROM pg_catalog.pg_extension E JOIN pg_catalog.pg_namespace N ON N.oid = E.extnamespace WHERE E.extname = 'plprofiler' """) row = cur.fetchone() if row is None: cur.execute("""SELECT pg_catalog.current_database()""") dbname = cur.fetchone()[0] cur.close() self.dbconn.rollback() raise Exception('ERROR: plprofiler extension not found in ' + 'database "%s"' %dbname) result = row[0] # ---- # We also check the version of the backend extension here. # ---- try: cur.execute(""" SELECT "%s".pl_profiler_version(), "%s".pl_profiler_versionstr() """ %(result, result)) except Exception: raise Exception("ERROR: cannot determine the version of the plprofiler extension - please upgrade the database extension to 4.1 or higher.") vrow = cur.fetchone() if vrow[0] < 40100 or vrow[0] >= 50000: raise Exception("ERROR: plprofiler extension is version %s, need 4.x" %vrow[1]) cur.close() self.dbconn.rollback() return result def save_dataset_from_local(self, opt_name, config, overwrite = False): # ---- # Aggregate the existing data found in pl_profiler_linestats_local # and pl_profiler_callgraph_local into a new entry in *_saved. # ---- cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s;""", (self.profiler_namespace, )) cur.execute("""SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;""") try: if overwrite: cur.execute("""DELETE FROM pl_profiler_saved WHERE s_name = %s""", (opt_name, )) cur.execute("""INSERT INTO pl_profiler_saved (s_name, s_options, s_callgraph_overflow, s_functions_overflow, s_lines_overflow) VALUES (%s, %s, false, false, false)""", (opt_name, json.dumps(config))) except psycopg2.IntegrityError as err: self.dbconn.rollback() raise err cur.execute("""INSERT INTO pl_profiler_saved_functions (f_s_id, f_funcoid, f_schema, f_funcname, f_funcresult, f_funcargs) SELECT currval('pl_profiler_saved_s_id_seq') as s_id, P.oid, N.nspname, P.proname, pg_catalog.pg_get_function_result(P.oid) as func_result, pg_catalog.pg_get_function_arguments(P.oid) as func_args FROM pg_catalog.pg_proc P JOIN pg_catalog.pg_namespace N on N.oid = P.pronamespace WHERE P.oid IN (SELECT * FROM unnest(pl_profiler_func_oids_local())) GROUP BY s_id, p.oid, nspname, proname ORDER BY s_id, p.oid, nspname, proname""") if cur.rowcount == 0: self.dbconn.rollback() raise Exception("No function data to save found") cur.execute("""INSERT INTO pl_profiler_saved_linestats (l_s_id, l_funcoid, l_line_number, l_source, l_exec_count, l_total_time, l_longest_time) SELECT currval('pl_profiler_saved_s_id_seq') as s_id, L.func_oid, L.line_number, coalesce(S.source, '-- SOURCE NOT FOUND'), sum(L.exec_count), sum(L.total_time), max(L.longest_time) FROM pl_profiler_linestats_local() L JOIN pl_profiler_funcs_source(pl_profiler_func_oids_local()) S ON S.func_oid = L.func_oid AND S.line_number = L.line_number GROUP BY s_id, L.func_oid, L.line_number, S.source ORDER BY s_id, L.func_oid, L.line_number""") if cur.rowcount == 0: self.dbconn.rollback() raise Exception("No plprofiler data to save") cur.execute("""INSERT INTO pl_profiler_saved_callgraph (c_s_id, c_stack, c_call_count, c_us_total, c_us_children, c_us_self) SELECT currval('pl_profiler_saved_s_id_seq') as s_id, pl_profiler_get_stack(stack), sum(call_count), sum(us_total), sum(us_children), sum(us_self) FROM pl_profiler_callgraph_local() GROUP BY s_id, stack ORDER BY s_id, stack;""") cur.execute("""RESET search_path""") cur.close() self.dbconn.commit() def save_dataset_from_shared(self, opt_name, config, overwrite = False): # ---- # Aggregate the existing data found in pl_profiler_linestats_shared # and pl_profiler_callgraph_shared into a new entry in *_saved. # ---- cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s;""", (self.profiler_namespace, )) cur.execute("""SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;""") try: if overwrite: cur.execute("""DELETE FROM pl_profiler_saved WHERE s_name = %s""", (opt_name, )) cur.execute("""INSERT INTO pl_profiler_saved (s_name, s_options, s_callgraph_overflow, s_functions_overflow, s_lines_overflow) VALUES (%s, %s, pl_profiler_callgraph_overflow(), pl_profiler_functions_overflow(), pl_profiler_lines_overflow())""", (opt_name, json.dumps(config))) except psycopg2.IntegrityError as err: self.dbconn.rollback() raise err cur.execute("""INSERT INTO pl_profiler_saved_functions (f_s_id, f_funcoid, f_schema, f_funcname, f_funcresult, f_funcargs) SELECT currval('pl_profiler_saved_s_id_seq') as s_id, P.oid, N.nspname, P.proname, pg_catalog.pg_get_function_result(P.oid) as func_result, pg_catalog.pg_get_function_arguments(P.oid) as func_args FROM pg_catalog.pg_proc P JOIN pg_catalog.pg_namespace N on N.oid = P.pronamespace WHERE P.oid IN (SELECT * FROM unnest(pl_profiler_func_oids_shared())) GROUP BY s_id, p.oid, nspname, proname ORDER BY s_id, p.oid, nspname, proname""") if cur.rowcount == 0: self.dbconn.rollback() raise Exception("No function data to save found") cur.execute("""INSERT INTO pl_profiler_saved_linestats (l_s_id, l_funcoid, l_line_number, l_source, l_exec_count, l_total_time, l_longest_time) SELECT currval('pl_profiler_saved_s_id_seq') as s_id, L.func_oid, L.line_number, coalesce(S.source, '-- SOURCE NOT FOUND'), sum(L.exec_count), sum(L.total_time), max(L.longest_time) FROM pl_profiler_linestats_shared() L JOIN pl_profiler_funcs_source(pl_profiler_func_oids_shared()) S ON S.func_oid = L.func_oid AND S.line_number = L.line_number GROUP BY s_id, L.func_oid, L.line_number, S.source ORDER BY s_id, L.func_oid, L.line_number""") if cur.rowcount == 0: self.dbconn.rollback() raise Exception("No plprofiler data to save") cur.execute("""INSERT INTO pl_profiler_saved_callgraph (c_s_id, c_stack, c_call_count, c_us_total, c_us_children, c_us_self) SELECT currval('pl_profiler_saved_s_id_seq') as s_id, pl_profiler_get_stack(stack), sum(call_count), sum(us_total), sum(us_children), sum(us_self) FROM pl_profiler_callgraph_shared() GROUP BY s_id, stack ORDER BY s_id, stack;""") cur.execute("""RESET search_path""") cur.close() self.dbconn.commit() def save_dataset_from_report(self, report_data, overwrite = False): # ---- # Save a dataset from the output of a get_*_report_data function. # This is used by the "import" command. # ---- cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s;""", (self.profiler_namespace, )) cur.execute("""SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;""") config = report_data['config'] opt_name = config['name'] # ---- # Add defaults for missing attributes in previous version. # ---- if 'callgraph_overflow' not in report_data: report_data['callgraph_overflow'] = False report_data['functions_overflow'] = False report_data['lines_overflow'] = False # ---- # Load the pl_profiler_saved entry. # ---- try: if overwrite: cur.execute("""DELETE FROM pl_profiler_saved WHERE s_name = %s""", (opt_name, )) cur.execute("""INSERT INTO pl_profiler_saved (s_name, s_options, s_callgraph_overflow, s_function_overflow, s_lines_overflow) VALUES (%s, %s, %s, %s, %s)""", (opt_name, json.dumps(config), report_data['callgraph_overflow'], report_data['functions_overflow'], report_data['lines_overflow'],)) except psycopg2.IntegrityError as err: self.dbconn.rollback() raise err # ---- # From the funcdefs, load the pl_profiler_saved_functions # and the pl_profiler_saved_linestats. # ---- for funcdef in report_data['func_defs']: cur.execute("""INSERT INTO pl_profiler_saved_functions (f_s_id, f_funcoid, f_schema, f_funcname, f_funcresult, f_funcargs) VALUES (currval('pl_profiler_saved_s_id_seq'), %s, %s, %s, %s, %s)""", (funcdef['funcoid'], funcdef['schema'], funcdef['funcname'], funcdef['funcresult'], funcdef['funcargs'])) for src in funcdef['source']: cur.execute("""INSERT INTO pl_profiler_saved_linestats (l_s_id, l_funcoid, l_line_number, l_source, l_exec_count, l_total_time, l_longest_time) VALUES (currval('pl_profiler_saved_s_id_seq'), %s, %s, %s, %s, %s, %s)""", (funcdef['funcoid'], src['line_number'], src['source'], src['exec_count'], src['total_time'], src['longest_time'], )) # ---- # Finally insert the callgraph data. # ---- for row in report_data['callgraph']: cur.execute("""INSERT INTO pl_profiler_saved_callgraph (c_s_id, c_stack, c_call_count, c_us_total, c_us_children, c_us_self) VALUES (currval('pl_profiler_saved_s_id_seq'), %s::text[], %s, %s, %s, %s)""", row) cur.execute("""RESET search_path""") cur.close() self.dbconn.commit() def get_dataset_list(self): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) cur.execute("""SELECT s_name, s_options FROM pl_profiler_saved ORDER BY s_name""") result = cur.fetchall() cur.execute("""RESET search_path""") cur.close() self.dbconn.rollback() return result def get_dataset_config(self, opt_name): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) cur.execute("""SELECT s_options FROM pl_profiler_saved WHERE s_name = %s""", (opt_name, )) if cur.rowcount == 0: self.dbconn.rollback() raise Exception("No saved data with name '" + opt_name + "' found") row = cur.fetchone() config = json.loads(row[0]) config['name'] = opt_name cur.execute("""RESET search_path""") cur.close() self.dbconn.rollback() return config def update_dataset_config(self, opt_name, new_name, config): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) cur.execute("""UPDATE pl_profiler_saved SET s_name = %s, s_options = %s WHERE s_name = %s""", (new_name, json.dumps(config), opt_name)) if cur.rowcount != 1: self.dbconn.rollback() raise Exception("Data set with name '" + opt_name + "' no longer exists") else: cur.execute("""RESET search_path""") self.dbconn.commit() cur.close() def delete_dataset(self, opt_name): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) cur.execute("""DELETE FROM pl_profiler_saved WHERE s_name = %s""", (opt_name, )) if cur.rowcount != 1: self.dbconn.rollback() raise Exception("Data set with name '" + opt_name + "' does not exists") else: cur.execute("""RESET search_path""") self.dbconn.commit() cur.close() def get_local_report_data(self, opt_name, opt_top, func_oids): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) # ---- # Create a default config. # ---- config = { 'name': opt_name, 'title': 'PL Profiler Report for %s' %(opt_name, ), 'tabstop': '8', 'svg_width': '1200', 'table_width': '80%', 'desc': '

PL Profiler Report for %s

\n' %(opt_name, ) + '

\n\n

', } # ---- # If not specified, find the top N functions by self time. # ---- found_more_funcs = False if func_oids is None or len(func_oids) == 0: func_oids_by_user = False func_oids = [] cur.execute("""SELECT stack[array_upper(stack, 1)] as func_oid, sum(us_self) as us_self FROM pl_profiler_callgraph_local() C GROUP BY func_oid ORDER BY us_self DESC LIMIT %s""", (opt_top + 1, )) for row in cur: func_oids.append(int(row[0])) if len(func_oids) > opt_top: func_oids = func_oids[:-1] found_more_funcs = True else: func_oids_by_user = True func_oids = [int(x) for x in func_oids] if len(func_oids) == 0: raise Exception("No profiling data found") # ---- # Get an alphabetically sorted list of the selected functions. # ---- cur.execute("""SELECT P.oid, N.nspname, P.proname FROM pg_catalog.pg_proc P JOIN pg_catalog.pg_namespace N ON N.oid = P.pronamespace WHERE P.oid IN (SELECT * FROM unnest(%s)) ORDER BY upper(nspname), nspname, upper(proname), proname""", (func_oids, )) func_list = [] for row in cur: func_list.append({ 'funcoid': str(row[0]), 'schema': str(row[1]), 'funcname': str(row[2]), }) # ---- # The view for linestats is extremely inefficient. We select # all of it once and cache it in a hash table. # ---- linestats = {} cur.execute("""SELECT L.func_oid, L.line_number, sum(L.exec_count)::bigint AS exec_count, sum(L.total_time)::bigint AS total_time, max(L.longest_time)::bigint AS longest_time, S.source FROM pl_profiler_linestats_local() L JOIN pl_profiler_funcs_source(pl_profiler_func_oids_local()) S ON S.func_oid = L.func_oid AND S.line_number = L.line_number GROUP BY L.func_oid, L.line_number, S.source ORDER BY L.func_oid, L.line_number""") for row in cur: if row[0] not in linestats: linestats[row[0]] = [] linestats[row[0]].append(row) # ---- # Build a list of function definitions in the order, specified # by the func_oids list. This is either the oids, requested by # the user or the oids determined above in descending order of # self_time. # ---- func_defs = [] for func_oid in func_oids: # ---- # First get the function definition and overall stats. # ---- cur.execute("""WITH SELF AS (SELECT stack[array_upper(stack, 1)] as func_oid, sum(us_self) as us_self FROM pl_profiler_callgraph_local() GROUP BY func_oid) SELECT P.oid, N.nspname, P.proname, coalesce(pg_catalog.pg_get_function_result(P.oid), ''), pg_catalog.pg_get_function_arguments(P.oid), coalesce(SELF.us_self, 0) as self_time FROM pg_catalog.pg_proc P JOIN pg_catalog.pg_namespace N ON N.oid = P.pronamespace LEFT JOIN SELF ON SELF.func_oid = P.oid WHERE P.oid = %s""", (func_oid, )) row = cur.fetchone() if row is None: raise Exception("function with Oid %d not found\n" %func_oid) # ---- # With that we can start the definition. # ---- func_def = { 'funcoid': func_oid, 'schema': row[1], 'funcname': row[2], 'funcresult': row[3], 'funcargs': row[4], 'total_time': linestats[func_oid][0][3], 'self_time': int(row[5]), 'source': [], } # ---- # Add all the source code lines to that. # ---- for row in linestats[func_oid]: func_def['source'].append({ 'line_number': int(row[1]), 'source': row[5], 'exec_count': int(row[2]), 'total_time': int(row[3]), 'longest_time': int(row[4]), }) # ---- # Add this function to the list of function definitions. # ---- func_defs.append(func_def) # ---- # Get the callgraph data. # ---- cur.execute("""SELECT array_to_string(pl_profiler_get_stack(stack), ';'), stack, call_count, us_total, us_children, us_self FROM pl_profiler_callgraph_local()""") flamedata = "" callgraph = [] for row in cur: flamedata += str(row[0]) + " " + str(row[5]) + "\n" callgraph.append(row[1:]) # ---- # That is it. Reset things and return the report data. # ---- cur.execute("""RESET search_path"""); self.dbconn.rollback() return { 'config': config, 'callgraph_overflow': False, 'functions_overflow': False, 'lines_overflow': False, 'func_list': func_list, 'func_defs': func_defs, 'flamedata': flamedata, 'callgraph': callgraph, 'func_oids_by_user': func_oids_by_user, 'found_more_funcs': found_more_funcs, } def get_shared_report_data(self, opt_name, opt_top, func_oids): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) # ---- # Create a default config. # ---- config = { 'name': opt_name, 'title': 'PL Profiler Report for %s' %(opt_name, ), 'tabstop': '8', 'svg_width': '1200', 'table_width': '80%', 'desc': '

PL Profiler Report for %s

\n' %(opt_name, ) + '

\n\n

', } # ---- # If not specified, find the top N functions by self time. # ---- found_more_funcs = False if func_oids is None or len(func_oids) == 0: func_oids_by_user = False func_oids = [] cur.execute("""SELECT stack[array_upper(stack, 1)] as func_oid, sum(us_self) as us_self FROM pl_profiler_callgraph_shared() C GROUP BY func_oid ORDER BY us_self DESC LIMIT %s""", (opt_top + 1, )) for row in cur: func_oids.append(int(row[0])) if len(func_oids) > opt_top: func_oids = func_oids[:-1] found_more_funcs = True else: func_oids_by_user = True func_oids = [int(x) for x in func_oids] if len(func_oids) == 0: raise Exception("No profiling data found") # ---- # Get an alphabetically sorted list of the selected functions. # ---- cur.execute("""SELECT P.oid, N.nspname, P.proname FROM pg_catalog.pg_proc P JOIN pg_catalog.pg_namespace N ON N.oid = P.pronamespace WHERE P.oid IN (SELECT * FROM unnest(%s)) ORDER BY upper(nspname), nspname, upper(proname), proname""", (func_oids, )) func_list = [] for row in cur: func_list.append({ 'funcoid': str(row[0]), 'schema': str(row[1]), 'funcname': str(row[2]), }) # ---- # The view for linestats is extremely inefficient. We select # all of it once and cache it in a hash table. # ---- linestats = {} cur.execute("""SELECT L.func_oid, L.line_number, sum(L.exec_count)::bigint AS exec_count, sum(L.total_time)::bigint AS total_time, max(L.longest_time)::bigint AS longest_time, S.source FROM pl_profiler_linestats_shared() L JOIN pl_profiler_funcs_source(pl_profiler_func_oids_shared()) S ON S.func_oid = L.func_oid AND S.line_number = L.line_number GROUP BY L.func_oid, L.line_number, S.source ORDER BY L.func_oid, L.line_number""") for row in cur: if row[0] not in linestats: linestats[row[0]] = [] linestats[row[0]].append(row) # ---- # Build a list of function definitions in the order, specified # by the func_oids list. This is either the oids, requested by # the user or the oids determined above in descending order of # self_time. # ---- func_defs = [] for func_oid in func_oids: # ---- # First get the function definition and overall stats. # ---- cur.execute("""WITH SELF AS (SELECT stack[array_upper(stack, 1)] as func_oid, sum(us_self) as us_self FROM pl_profiler_callgraph_shared() GROUP BY func_oid) SELECT P.oid, N.nspname, P.proname, coalesce(pg_catalog.pg_get_function_result(P.oid), ''), pg_catalog.pg_get_function_arguments(P.oid), coalesce(SELF.us_self, 0) as self_time FROM pg_catalog.pg_proc P JOIN pg_catalog.pg_namespace N ON N.oid = P.pronamespace LEFT JOIN SELF ON SELF.func_oid = P.oid WHERE P.oid = %s""", (func_oid, )) row = cur.fetchone() if row is None: raise Exception("function with Oid %d not found\n" %func_oid) # ---- # With that we can start the definition. # ---- func_def = { 'funcoid': func_oid, 'schema': row[1], 'funcname': row[2], 'funcresult': row[3], 'funcargs': row[4], 'total_time': linestats[func_oid][0][3], 'self_time': int(row[5]), 'source': [], } # ---- # Add all the source code lines to that. # ---- for row in linestats[func_oid]: func_def['source'].append({ 'line_number': int(row[1]), 'source': row[5], 'exec_count': int(row[2]), 'total_time': int(row[3]), 'longest_time': int(row[4]), }) # ---- # Add this function to the list of function definitions. # ---- func_defs.append(func_def) # ---- # Get the callgraph data. # ---- cur.execute("""SELECT array_to_string(pl_profiler_get_stack(stack), ';'), stack, call_count, us_total, us_children, us_self FROM pl_profiler_callgraph_shared()""") flamedata = "" callgraph = [] for row in cur: flamedata += str(row[0]) + " " + str(row[5]) + "\n" callgraph.append(row[1:]) # ---- # Get the status of the overflow flags. # ---- cur.execute("""SELECT pl_profiler_callgraph_overflow(), pl_profiler_functions_overflow(), pl_profiler_lines_overflow() """) overflow_flags = cur.fetchone() # ---- # That is it. Reset things and return the report data. # ---- cur.execute("""RESET search_path"""); self.dbconn.rollback() return { 'config': config, 'callgraph_overflow': overflow_flags[0], 'functions_overflow': overflow_flags[1], 'lines_overflow': overflow_flags[2], 'func_list': func_list, 'func_defs': func_defs, 'flamedata': flamedata, 'callgraph': callgraph, 'func_oids_by_user': func_oids_by_user, 'found_more_funcs': found_more_funcs, } def get_saved_report_data(self, opt_name, opt_top, func_oids): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) # ---- # Get the config of the saved dataset. # ---- cur.execute("""SELECT s_options FROM pl_profiler_saved WHERE s_name = %s""", (opt_name, )) if cur.rowcount == 0: self.dbconn.rollback() raise Exception("No saved data with name '" + opt_name + "' found") row = cur.fetchone() config = json.loads(row[0]) config['name'] = opt_name # ---- # If not specified, find the top N functions by self time. # ---- found_more_funcs = False if func_oids is None or len(func_oids) == 0: func_oids_by_user = False func_oids = [] cur.execute("""SELECT regexp_replace(c_stack[array_upper(c_stack, 1)], E'.* oid=\\([0-9]*\\)$', E'\\\\1') as func_oid, sum(c_us_self) as us_self FROM pl_profiler_saved S JOIN pl_profiler_saved_callgraph C ON C.c_s_id = S.s_id WHERE S.s_name = %s GROUP BY func_oid ORDER BY us_self DESC LIMIT %s""", (opt_name, opt_top + 1, )) for row in cur: func_oids.append(int(row[0])) if len(func_oids) > opt_top: func_oids = func_oids[:-1] found_more_funcs = True else: func_oids_by_user = True func_oids = [int(x) for x in func_oids] if len(func_oids) == 0: raise Exception("No profiling data found") # ---- # Get an alphabetically sorted list of the selected functions. # ---- cur.execute("""SELECT f_funcoid, f_schema, f_funcname FROM pl_profiler_saved S JOIN pl_profiler_saved_functions F ON F.f_s_id = S.s_id WHERE S.s_name = %s AND F.f_funcoid IN (SELECT * FROM unnest(%s)) ORDER BY upper(f_schema), f_schema, upper(f_funcname), f_funcname""", (opt_name, func_oids, )) func_list = [] for row in cur: func_list.append({ 'funcoid': str(row[0]), 'schema': str(row[1]), 'funcname': str(row[2]), }) # ---- # Build a list of function definitions in the order, specified # by the func_oids list. This is either the oids, requested by # the user or the oids determined above in descending order of # self_time. # ---- func_defs = [] for func_oid in func_oids: # ---- # First get the function definition and overall stats. # ---- cur.execute("""WITH SELF AS ( SELECT regexp_replace(c_stack[array_upper(c_stack, 1)], E'.* oid=\\([0-9]*\\)$', E'\\\\1') as func_oid, sum(c_us_self) as us_self FROM pl_profiler_saved S JOIN pl_profiler_saved_callgraph C ON C.c_s_id = S.s_id WHERE S.s_name = %s GROUP BY func_oid) SELECT l_funcoid, f_schema, f_funcname, f_funcresult, f_funcargs, coalesce(l_total_time, 0) as total_time, coalesce(SELF.us_self, 0) as self_time FROM pl_profiler_saved S LEFT JOIN pl_profiler_saved_linestats L ON l_s_id = s_id JOIN pl_profiler_saved_functions F ON f_funcoid = l_funcoid LEFT JOIN SELF ON SELF.func_oid::bigint = f_funcoid WHERE S.s_name = %s AND L.l_funcoid = %s AND L.l_line_number = 0""", (opt_name, opt_name, func_oid, )) row = cur.fetchone() if row is None: raise Exception("function with Oid %d not found\n" %func_oid) # ---- # With that we can start the definition. # ---- func_def = { 'funcoid': func_oid, 'schema': row[1], 'funcname': row[2], 'funcresult': row[3], 'funcargs': row[4], 'total_time': int(row[5]), 'self_time': int(row[6]), 'source': [], } # ---- # Add all the source code lines to that. # ---- cur.execute("""SELECT l_line_number, l_source, l_exec_count, l_total_time, l_longest_time FROM pl_profiler_saved S JOIN pl_profiler_saved_linestats L ON L.l_s_id = S.s_id WHERE S.s_name = %s AND L.l_funcoid = %s ORDER BY l_s_id, l_funcoid, l_line_number""", (opt_name, func_oid, )) for row in cur: func_def['source'].append({ 'line_number': int(row[0]), 'source': row[1], 'exec_count': int(row[2]), 'total_time': int(row[3]), 'longest_time': int(row[4]), }) # ---- # Add this function to the list of function definitions. # ---- func_defs.append(func_def) # ---- # Get the callgraph data. # ---- cur.execute("""SELECT array_to_string(c_stack, ';'), c_stack, c_call_count, c_us_total, c_us_children, c_us_self FROM pl_profiler_saved S JOIN pl_profiler_saved_callgraph C ON C.c_s_id = S.s_id WHERE S.s_name = %s""", (opt_name, )) flamedata = "" callgraph = [] for row in cur: flamedata += str(row[0]) + " " + str(row[5]) + "\n" callgraph.append(row[1:]) # ---- # That is it. Reset things and return the report data. # ---- cur.execute("""RESET search_path"""); self.dbconn.rollback() return { 'config': config, 'func_list': func_list, 'func_defs': func_defs, 'flamedata': flamedata, 'callgraph': callgraph, 'func_oids_by_user': func_oids_by_user, 'found_more_funcs': found_more_funcs, } def enable(self): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) cur.execute("""SELECT pl_profiler_set_enabled_local(true)""") cur.execute("""SELECT pl_profiler_set_collect_interval(0)""") cur.execute("""RESET search_path""") self.dbconn.commit() cur.close() def disable(self): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) cur.execute("""SELECT pl_profiler_set_enabled_local(false)""") cur.execute("""RESET search_path""") self.dbconn.commit() cur.close() def enable_monitor(self, opt_pid = None, opt_interval = 10): cur = self.dbconn.cursor() cur.execute(""" SELECT setting FROM pg_catalog.pg_settings WHERE name = 'server_version_num' """) server_version_num = int(cur.fetchone()[0]) if server_version_num < 90400: cur.execute(""" SELECT setting FROM pg_catalog.pg_settings WHERE name = 'server_version' """) server_version = cur.fetchone()[0] self.dbconn.rollback() raise Exception(("ERROR: monitor command not supported on " + "server version %s. Perform monitoring manually " + "via postgresql.conf changes and reloading " + "the postmaster.") %server_version) cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) if opt_pid is not None: cur.execute("""SELECT pl_profiler_set_enabled_pid(%s)""", (opt_pid, )) else: cur.execute("""SELECT pl_profiler_set_enabled_global(true)""") cur.execute("""SELECT pl_profiler_set_collect_interval(%s)""", (opt_interval, )) cur.execute("""RESET search_path""") cur.close() self.dbconn.commit() def disable_monitor(self): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) cur.execute("""SELECT pl_profiler_set_enabled_global(false)""") cur.execute("""SELECT pl_profiler_set_enabled_pid(0)""") cur.execute("""SELECT pl_profiler_set_collect_interval(0)"""); cur.execute("""RESET search_path""") cur.close() self.dbconn.commit() def reset_local(self): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) cur.execute("""SELECT pl_profiler_reset_local()""") cur.execute("""RESET search_path""") self.dbconn.commit() cur.close() def reset_shared(self): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) cur.execute("""SELECT pl_profiler_reset_shared()""") cur.execute("""RESET search_path""") self.dbconn.commit() cur.close() def save_collect_data(self): cur = self.dbconn.cursor() cur.execute("""SET search_path TO %s""", (self.profiler_namespace, )) cur.execute("""SELECT pl_profiler_collect_data()""") cur.execute("""RESET search_path""") self.dbconn.commit() cur.close() def execute_sql(self, sql, output = None): try: cur = self.dbconn.cursor() for query in sql_split(sql).get_statements(): if output is not None: output.write(query + '\n') start_time = time.time() try: cur.execute(query) end_time = time.time() if cur.description is not None: if cur.rowcount == 0: if output is not None: output.write("(0 rows)\n") else: if output is not None: max_col_len = max([len(d[0]) for d in cur.description]) cols = [' ' + ' '*(max_col_len - len(d[0])) + d[0] + ':' for d in cur.description] for row in cur: output.write("-- row" + str(cur.rownumber) + ":\n") for col in range(0, len(cols)): output.write(cols[col] + ' ' + str(row[col]) + '\n') output.write("----\n") output.write("(%d rows)\n" %(cur.rowcount, )) except Exception as err: end_time = time.time() output.write("ERROR: " + str(err) + '\n') latency = end_time - start_time if output is not None: output.write(cur.statusmessage + " (%.3f seconds)\n" %latency) output.write("\n") except Exception as err: raise err self.dbconn.rollback() def report(self, report_data, output_fd): report = plprofiler_report() report.generate(report_data, output_fd) plprofiler-REL4_2_5/python-plprofiler/plprofiler/plprofiler_report.py000066400000000000000000000231431465735455400264300ustar00rootroot00000000000000#!/usr/bin/env python import base64 import html import os import subprocess import sys __all__ = ['plprofiler_report'] class plprofiler_report: def __init__(self): pass def generate(self, report_data, outfd): config = report_data['config'] self.outfd = outfd self.out("") self.out("") self.out(" %s" %(html.escape(config['title']), )) self.out(HTML_SCRIPT) self.out(HTML_STYLE) self.out("") self.out("""""") self.out(config['desc']) self.out("

PL/pgSQL Call Graph

") self.out("
") self.out(self.generate_flamegraph(config, report_data['flamedata'])) self.out("
") if not report_data['func_oids_by_user']: if report_data['found_more_funcs']: hdr = "

Top %d functions (by self_time)

" %(len(report_data['func_list']),) else: hdr = "

All %d functions (by self_time)

" %(len(report_data['func_list']),) else: hdr = "

Requested functions

" self.out("

List of functions detailed below

") self.out("
") self.out(hdr) for func_def in report_data['func_defs']: self.generate_function_output(config, func_def) self.out("") self.out("") def format_d_comma(self, num): s = str(num) r = [] l = len(s) i = 0 j = l % 3 if j == 0: j = 3 while j <= l: r.append(s[i:j]) i = j j += 3 return ",".join(r) def generate_function_output(self, config, func_def): func_def['self_time_fmt'] = self.format_d_comma(func_def['self_time']) func_def['total_time_fmt'] = self.format_d_comma(func_def['total_time']) self.out("""""".format(**func_def)) self.out("""

Function {schema}.{funcname}() oid={funcoid} (show)

""".format(**func_def)) self.out("""

""") self.out("""self_time = {self_time_fmt:s} µs
""".format(**func_def)) self.out("""total_time = {total_time_fmt:s} µs""".format(**func_def)) self.out("""

""") self.out("""""") self.out(""" """) self.out(""" """.format(**func_def)) self.out(""" """.format( funcargs = func_def['funcargs'].replace(', ', ',
 '))) self.out(""" """) self.out(""" """) self.out(""" """) self.out(""" """) self.out("""
{schema}.{funcname} ({funcargs})
""") self.out("""     RETURNS {funcresult}""".format( funcresult = func_def['funcresult'].replace(' ', ' '))) self.out("""
""") self.out("""") def generate_flamegraph(self, config, data): path = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(path, 'lib', 'FlameGraph', 'flamegraph.pl', ) proc = subprocess.Popen(['perl', path, "--title=%s" %(config['title'], ), "--width=%s" %(config['svg_width'], ), ], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE); svg, err = proc.communicate(data.encode('utf-8')) if proc.returncode != 0: raise Exception("flamegraph returned with exit code %d\n%s" %( proc.returncode, str(err))) return svg.decode('utf-8') def out(self, line): self.outfd.write(line + '\n') HTML_SCRIPT = """ """ HTML_STYLE = """ """ plprofiler-REL4_2_5/python-plprofiler/plprofiler/plprofiler_tool.py000066400000000000000000001111711465735455400260710ustar00rootroot00000000000000#!/usr/bin/env python from configparser import RawConfigParser import getopt import json import os from io import StringIO import subprocess import sys import tempfile import time from .plprofiler import plprofiler __all__ = ['main'] def main(): if len(sys.argv) == 1: usage() return 2 if sys.argv[1] in ['-?', '--help', 'help']: if len(sys.argv) == 2: usage() else: if sys.argv[2] == 'save': help_save() elif sys.argv[2] == 'list': help_list() elif sys.argv[2] == 'edit': help_edit() elif sys.argv[2] == 'delete': help_delete() elif sys.argv[2] == 'reset': help_reset() elif sys.argv[2] == 'report': help_report() elif sys.argv[2] == 'export': help_export() elif sys.argv[2] == 'import': help_import() elif sys.argv[2] == 'run': help_run() elif sys.argv[2] == 'monitor': help_monitor() else: usage() return 0 if sys.argv[1] == 'save': return save_command(sys.argv[2:]) if sys.argv[1] == 'list': return list_command(sys.argv[2:]) if sys.argv[1] == 'edit': return edit_command(sys.argv[2:]) if sys.argv[1] == 'delete': return delete_command(sys.argv[2:]) if sys.argv[1] == 'reset': return reset_command(sys.argv[2:]) if sys.argv[1] == 'report': return report_command(sys.argv[2:]) if sys.argv[1] == 'export': return export_command(sys.argv[2:]) if sys.argv[1] == 'import': return import_command(sys.argv[2:]) if sys.argv[1] == 'run': return run_command(sys.argv[2:]) if sys.argv[1] == 'monitor': return monitor_command(sys.argv[2:]) sys.stderr.write("ERROR: unknown command '%s'\n" %(sys.argv[1])) return 2 # ---- # save_data_command # ---- def save_command(argv): connoptions = {} opt_name = None opt_title = None opt_desc = None opt_force = False need_edit = False # ---- # Parse command line # ---- try: opts, args = getopt.getopt(argv, # Standard connection related options "d:h:p:U:", [ 'dbname=', 'host=', 'port=', 'user=', 'help', # save command specific options 'name=', 'title=', 'desc=', 'description=', 'force', ]) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 for opt, val in opts: if opt in ['-d', '--dbname']: if val.find('=') < 0: connoptions['database'] = val else: connoptions['dsn'] = val elif opt in ['-h', '--host']: connoptions['host'] = val elif opt in ['-p', '--port']: connoptions['port'] = int(val) elif opt in ['-U', '--user']: connoptions['user'] = val elif opt in ['--help']: help_save() return 0 elif opt in ['--name']: opt_name = val elif opt in ['--title']: opt_title = val elif opt in ['--desc', '--description']: opt_desc = val elif opt in ['--force']: opt_force = True if opt_name is None: sys.stderr.write("option --name must be given\n") return 2 # ---- # Set defaults if options not given. # ---- if opt_title is None: need_edit = True opt_title = 'PL Profiler Report for ' + opt_name if opt_desc is None: need_edit = True opt_desc = '

' + opt_title + '

\n' + \ '

\n\n

\n' # ---- # Create our config. # ---- config = { 'name': opt_name, 'title': opt_title, 'tabstop': 8, 'svg_width': 1200, 'table_width': '80%', 'desc': opt_desc, } # ---- # If we set defaults for config options, invoke an editor. # ---- if need_edit: try: edit_config_info(config) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 opt_name = config['name'] try: plp = plprofiler() plp.connect(connoptions) plp.save_dataset_from_shared(opt_name, config, opt_force) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 def list_command(argv): connoptions = {} # ---- # Parse command line # ---- try: opts, args = getopt.getopt(argv, # Standard connection related options "d:h:p:U:", [ 'dbname=', 'host=', 'port=', 'user=', 'help', # list command specific options (none at the moment) ]) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 for opt, val in opts: if opt in ['-d', '--dbname']: if val.find('=') < 0: connoptions['database'] = val else: connoptions['dsn'] = val elif opt in ['-h', '--host']: connoptions['host'] = val elif opt in ['-p', '--port']: connoptions['port'] = int(val) elif opt in ['-U', '--user']: connoptions['user'] = val elif opt in ['--help']: help_list() return 0 # ---- # Get the list of saved data sets. # ---- try: plp = plprofiler() plp.connect(connoptions) rows = plp.get_dataset_list() except Exception as err: sys.stderr.write(str(err) + '\n') return 1 if len(rows) == 0: print("No saved data sets found") else: print("") max_name_len = 4 for row in rows: if len(row[0]) > max_name_len: max_name_len = len(row[0]) print('Name' + ' '*(max_name_len - 4) + ' | Title') print('-'*max_name_len + '-+-' + '-'*(79 - 3 - max_name_len)) for row in rows: config = json.loads(row[1]) pad = max_name_len - len(row[0]) print(row[0] + ' '*pad + ' | ' + config.get('title', '')) print("") print('(' + str(len(rows)) + ' data sets found)') print("") return 0 def edit_command(argv): connoptions = {} opt_name = None # ---- # Parse command line # ---- try: opts, args = getopt.getopt(argv, # Standard connection related options "d:h:p:U:", [ 'dbname=', 'host=', 'port=', 'user=', 'help', # edit command specific coptions 'name=', ]) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 for opt, val in opts: if opt in ['-d', '--dbname']: if val.find('=') < 0: connoptions['database'] = val else: connoptions['dsn'] = val elif opt in ['-h', '--host']: connoptions['host'] = val elif opt in ['-p', '--port']: connoptions['port'] = int(val) elif opt in ['-U', '--user']: connoptions['user'] = val elif opt in ['--help']: help_edit() return 0 elif opt in ['--name']: opt_name = val if opt_name is None: sys.stderr.write("option --name must be given\n") return 2 # ---- # Get the current values and create a config with that. # ---- try: plp = plprofiler() plp.connect(connoptions) config = plp.get_dataset_config(opt_name) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 # ---- # Launch the editor for the user to edit the info. # ---- try: edit_config_info(config) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 new_name = config['name'] # ---- # Update the dataset config # ---- try: plp.update_dataset_config(opt_name, new_name, config) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 return 0 def delete_command(argv): connoptions = {} opt_name = None # ---- # Parse command line # ---- try: opts, args = getopt.getopt(argv, # Standard connection related options "d:h:p:U:", [ 'dbname=', 'host=', 'port=', 'user=', 'help', # edit command specific coptions 'name=', ]) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 for opt, val in opts: if opt in ['-d', '--dbname']: if val.find('=') < 0: connoptions['database'] = val else: connoptions['dsn'] = val elif opt in ['-h', '--host']: connoptions['host'] = val elif opt in ['-p', '--port']: connoptions['port'] = int(val) elif opt in ['-U', '--user']: connoptions['user'] = val elif opt in ['--help']: help_delete() return 0 elif opt in ['--name']: opt_name = val if opt_name is None: sys.stderr.write("option --name must be given\n") return 2 # ---- # Delete the requested data set. # ---- try: plp = plprofiler() plp.connect(connoptions) plp.delete_dataset(opt_name) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 def reset_command(argv): connoptions = {} # ---- # Parse command line # ---- try: opts, args = getopt.getopt(argv, # Standard connection related options "d:h:p:U:", [ 'dbname=', 'host=', 'port=', 'user=', 'help', # edit command specific coptions ]) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 for opt, val in opts: if opt in ['-d', '--dbname']: if val.find('=') < 0: connoptions['database'] = val else: connoptions['dsn'] = val elif opt in ['-h', '--host']: connoptions['host'] = val elif opt in ['-p', '--port']: connoptions['port'] = int(val) elif opt in ['-U', '--user']: connoptions['user'] = val elif opt in ['--help']: help_reset() return 0 # ---- # Delete the collected data from the pl_profiler_linestats_shared # and pl_profiler_callgraph_shared hashtables. # ---- try: plp = plprofiler() plp.connect(connoptions) plp.reset_shared() except Exception as err: sys.stderr.write(str(err) + '\n') return 1 def report_command(argv): connoptions = {} opt_name = None opt_title = None opt_desc = None opt_top = 10 opt_output = None opt_from_shared = False need_edit = False try: opts, args = getopt.getopt(argv, # Standard connection related options "d:h:o:p:U:", [ 'dbname=', 'host=', 'port=', 'user=', 'help', # report command specific options 'name=', 'title=', 'desc=', 'description=', 'output=', 'top=', 'from-shared', ]) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 for opt, val in opts: if opt in ['-d', '--dbname']: if val.find('=') < 0: connoptions['database'] = val else: connoptions['dsn'] = val elif opt in ['-h', '--host']: connoptions['host'] = val elif opt in ['-p', '--port']: connoptions['port'] = int(val) elif opt in ['-U', '--user']: connoptions['user'] = val elif opt in ['--help']: help_report() return 0 elif opt in ['--name']: opt_name = val elif opt in ['--title']: opt_title = val elif opt in ['--desc', '--description']: opt_desc = val elif opt in ('-o', '--output', ): opt_output = val elif opt in ('--top', ): opt_top = int(val) elif opt in ('--from-shared', ): opt_from_shared = True if opt_name is None and not opt_from_shared: sys.stderr.write("option --name or --from-shared must be given\n") return 2 if opt_name is None: opt_name = 'current_data' if opt_from_shared and (opt_name is None or opt_title is None or opt_desc is None): need_edit = True if opt_output is None: output_fd = sys.stdout else: output_fd = open(opt_output, 'w') try: plp = plprofiler() plp.connect(connoptions) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 # ---- # Get the report data either from the collected *_data tables # or a saved dataset. # ---- if opt_from_shared: report_data = plp.get_shared_report_data(opt_name, opt_top, args) else: report_data = plp.get_saved_report_data(opt_name, opt_top, args) config = report_data['config'] if opt_title is not None: config['title'] = opt_title if opt_desc is not None: config['desc'] = opt_desc # ---- # Invoke the editor on the config if need be. # ---- if need_edit: try: edit_config_info(config) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 opt_name = config['name'] report_data['config'] = config plp.report(report_data, output_fd) if opt_output is not None: output_fd.close() return 0 def export_command(argv): connoptions = {} opt_all = False opt_name = None opt_title = None opt_desc = None opt_top = pow(2, 31) opt_output = None opt_from_shared = False opt_edit = False try: opts, args = getopt.getopt(argv, # Standard connection related options "d:h:o:p:U:", [ 'dbname=', 'host=', 'port=', 'user=', 'help', # report command specific options 'all', 'name=', 'title=', 'desc=', 'description=', 'edit', 'output=', 'from-shared', ]) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 for opt, val in opts: if opt in ['-d', '--dbname']: if val.find('=') < 0: connoptions['database'] = val else: connoptions['dsn'] = val elif opt in ['-h', '--host']: connoptions['host'] = val elif opt in ['-p', '--port']: connoptions['port'] = int(val) elif opt in ['-U', '--user']: connoptions['user'] = val elif opt in ['--help']: help_export() return 0 elif opt in ['--all']: opt_all = True elif opt in ['--edit']: opt_edit = True elif opt in ['--name']: opt_name = val elif opt in ['--title']: opt_title = val elif opt in ['--desc', '--description']: opt_desc = val elif opt in ('-o', '--output', ): opt_output = val elif opt in ('--from-shared', ): opt_from_shared = True if not opt_all and opt_name is None and not opt_from_shared: sys.stderr.write("option --all, --name or --from-shared must be given\n") return 2 if opt_output is None: output_fd = sys.stdout else: output_fd = open(opt_output, 'w') try: plp = plprofiler() plp.connect(connoptions) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 if opt_all: export_names = [row[0] for row in plp.get_dataset_list()] else: if opt_from_shared: export_names = ['collected_data'] else: export_names = [opt_name] # ---- # Build the export data set. # ---- export_set = [] for name in export_names: # ---- # Get the report data either from the collected *_data tables # or a saved dataset. # ---- if opt_from_shared: report_data = plp.get_shared_report_data(name, opt_top, args) else: report_data = plp.get_saved_report_data(name, opt_top, args) config = report_data['config'] if opt_title is not None: config['title'] = opt_title if opt_desc is not None: config['desc'] = opt_desc # ---- # Launch an editor if we are asked to edit configs. # ---- if opt_edit: try: edit_config_info(config) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 report_data['config'] = config export_set.append(report_data) # ---- # Write the whole thing out. # ---- output_fd.write(json.dumps(export_set, indent = 2, sort_keys = True) + "\n") if opt_output is not None: output_fd.close() return 0 def import_command(argv): connoptions = {} opt_file = None opt_edit = False opt_force = False try: opts, args = getopt.getopt(argv, # Standard connection related options "d:f:h:p:U:", [ 'dbname=', 'host=', 'port=', 'user=', 'help', # report command specific options 'file=', 'edit', 'force', ]) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 for opt, val in opts: if opt in ['-d', '--dbname']: if val.find('=') < 0: connoptions['database'] = val else: connoptions['dsn'] = val elif opt in ['-h', '--host']: connoptions['host'] = val elif opt in ['-p', '--port']: connoptions['port'] = int(val) elif opt in ['-U', '--user']: connoptions['user'] = val elif opt in ['--help']: help_import() return 0 elif opt in ['-f', '--file']: opt_file = val elif opt in ['--edit']: opt_edit = True elif opt in ['--force']: opt_force = True if opt_file is None: sys.stderr.write("option --file must be given\n") return 2 try: plp = plprofiler() plp.connect(connoptions) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 # ---- # Read the export data set and process it. # ---- with open(opt_file, 'r') as fd: import_set = json.loads(fd.read()) for report_data in import_set: # ---- # Launch an editor if we are asked to edit configs. # ---- if opt_edit: try: config = report_data['config'] edit_config_info(config) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 report_data['config'] = config # ---- # Try to save this report as a saved set. # ---- plp.save_dataset_from_report(report_data, opt_force) return 0 def run_command(argv): connoptions = {} opt_name = None opt_title = None opt_desc = None opt_sql_file = None opt_query = None opt_top = 10 opt_output = None opt_save = False opt_force = False need_edit = False try: opts, args = getopt.getopt(argv, # Standard connection related options "c:d:f:h:o:p:U:", [ 'dbname=', 'host=', 'port=', 'user=', 'help', # run command specific options 'name=', 'title=', 'desc=', 'description=', 'command=', 'file=', 'save', 'force', 'output=', 'top=', ]) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 for opt, val in opts: if opt in ['-d', '--dbname']: if val.find('=') < 0: connoptions['database'] = val else: connoptions['dsn'] = val elif opt in ['-h', '--host']: connoptions['host'] = val elif opt in ['-p', '--port']: connoptions['port'] = int(val) elif opt in ['-U', '--user']: connoptions['user'] = val elif opt in ['--help']: help_run() return 0 elif opt in ['--name']: opt_name = val elif opt in ('-T', '--title', ): opt_title = val elif opt in ('-D', '--desc', '--description', ): opt_desc = val elif opt in ('-c', '--command', ): opt_query = val elif opt in ('-f', '--file', ): opt_sql_file = val elif opt in ('--top', ): opt_top = int(val) elif opt in ('-o', '--output', ): opt_output = val elif opt in ('-s', '--save', ): opt_save = True elif opt in ('-f', '--force', ): opt_force = True if opt_name is None: need_edit = True opt_name = 'current' if opt_title is None: need_edit = True opt_title = "PL Profiler Report for %s" %(opt_name, ) if opt_desc is None: need_edit = True opt_desc = ("

PL Profiler Report for %s

\n" + "

\n\n

") %(opt_name, ) if opt_sql_file is not None and opt_query is not None: sys.stderr.write("--query and --sql-file are mutually exclusive\n") return 2 if opt_sql_file is None and opt_query is None: sys.stderr.write("One of --query or --sql-file must be given\n") return 2 if opt_query is None: with open(opt_sql_file, 'r') as fd: opt_query = fd.read() try: plp = plprofiler() plp.connect(connoptions) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 plp.enable() plp.reset_local() plp.execute_sql(opt_query, sys.stdout) # ---- # Create our config. # ---- config = { 'name': opt_name, 'title': opt_title, 'tabstop': 8, 'svg_width': 1200, 'table_width': '80%', 'desc': opt_desc, } # ---- # If we set defaults for config options, invoke an editor. # ---- if need_edit: try: edit_config_info(config) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 opt_name = config['name'] if opt_save: try: plp.save_dataset_from_local(opt_name, config, opt_force) except Exception as err: sys.stderr.write(str(err) + "\n") return 1 if opt_output is not None: with open(opt_output, 'w') as output_fd: report_data = plp.get_local_report_data(opt_name, opt_top, args) report_data['config'] = config plp.report(report_data, output_fd) output_fd.close() return 0 def monitor_command(argv): connoptions = {} opt_duration = 60 opt_interval = 10 opt_pid = None try: opts, args = getopt.getopt(argv, # Standard connection related options "d:h:p:U:", [ 'dbname=', 'host=', 'port=', 'user=', 'help', # monitor command specific options 'pid=', 'interval=', 'duration=', ]) except Exception as err: sys.stderr.write(str(err) + '\n') return 2 for opt, val in opts: if opt in ['-d', '--dbname']: if val.find('=') < 0: connoptions['database'] = val else: connoptions['dsn'] = val elif opt in ['-h', '--host']: connoptions['host'] = val elif opt in ['-p', '--port']: connoptions['port'] = int(val) elif opt in ['-U', '--user']: connoptions['user'] = val elif opt in ['--help']: help_monitor() return 0 elif opt in ('-p', '--pid', ): opt_pid = val elif opt in ('-i', '--interval', ): opt_interval = val elif opt in ('-d', '--duration', ): opt_duration = val try: plp = plprofiler() plp.connect(connoptions) except Exception as err: sys.stderr.write(str(err) + '\n') return 1 try: plp.enable_monitor(opt_pid, opt_interval) except Exception as err: print(str(err)) return 1 print("monitoring for %d seconds ..." %(int(opt_duration))) try: time.sleep(int(opt_duration)) finally: plp.disable_monitor() print("done.") return 0 def edit_config_info(config): if os.name == 'posix': default_editor = 'vi' elif os.name == 'nt': default_editor = 'notepad' else: raise Exception("unsupported OS type %s" %os.name) EDITOR = os.environ.get('EDITOR', default_editor) opts = ['title', 'tabstop', 'svg_width', 'table_width', 'desc', ] # ---- # Create a ConfigParser that contains relevant sections of the config. # ---- name = config['name'] tmp_config = RawConfigParser() tmp_config.add_section(name) for opt in opts: tmp_config.set(name, opt, str(config[opt])) # ---- # We need the temp file to edit to have the correct, OS specific # line endings. So we create a StringIO buffer first to get the # file content from the ConfigParser, then change '\n' into # os.linesep when creating the temp file. # ---- buf = StringIO() tmp_config.write(buf) tf = tempfile.NamedTemporaryFile(suffix=".tmp.conf", delete = False) tf_name = tf.name tf.write(buf.getvalue().replace('\n', os.linesep).encode('utf-8')) tf.close() # ---- # Call the editor. # ---- subprocess.call([EDITOR, tf_name]) # ---- # Remove all sections from the ConfigParser object, read back # the temp file and extract the one expected section. # ---- for s in tmp_config.sections(): tmp_config.remove_section(s) tf = open(tf_name, 'r') tmp_config.readfp(tf) tf.close() os.remove(tf_name) if len(tmp_config.sections()) != 1: raise Exception("config must have exactly one section") name = tmp_config.sections()[0] config['name'] = name for opt in opts: if tmp_config.has_option(name, opt): config[opt] = str(tmp_config.get(name, opt)) def usage(): print(""" usage: plprofiler COMMAND [OPTIONS] plprofiler is a command line tool to control the plprofiler extension for PostgreSQL. The input of this utility are the call and execution statistics, the plprofiler extension collects. The final output is an HTML report of the statistics gathered. There are several ways to collect the data, save the data permanently and even transport it from a production system to a lab system for offline analysis. Use plprofiler COMMAND --help for detailed information about one of the commands below. GENERAL OPTIONS: All commands implement the following command line options to specify the target database: -h, --host=HOST The host name of the database server. -p, --port=PORT The PostgreSQL port number. -U, --user=USER The PostgreSQL user name to connect as. -d, --dbname=DB The PostgreSQL database name or the DSN. plprofiler currently uses psycopg2 to connect to the target database. Since that is based on libpq, all the above parameters can also be specified in this option with the usual conninfo string or URI formats. --help Print the command specific help information and exit. TERMS: The following terms are used in the text below and the help output of individual commands: local-data By default the plprofiler extension collects run-time data in per-backend hashtables (in-memory). This data is only accessible in the current session and is lost when the session ends or the hash tables are explicitly reset. shared-data The plprofiler extension can copy the local-data into shared hashtables, to make the statistics available to other sessions. See the "monitor" command for details. This data still relies on the local database's system catalog to resolve Oid values into object definitions. saved-dataset The local-data as well as the shared-data can be turned into a named, saved dataset. These sets can be exported and imported onto other machines. The saved datasets are independent of the system catalog, so a report can be generated again later, even even on a different system. COMMANDS: run Runs one or more SQL statements with the plprofiler extension enabled and creates a saved-dataset and/or an HTML report from the local-data. monitor Monitors a running application for a requested time and creates a saved-dataset and/or an HTML report from the resulting shared-data. reset Deletes the data from shared hash tables. save Saves the current shared-data as a saved-dataset. list Lists the available saved-datasets. edit Edits the metadata of one saved-dataset. The metadata is used in the generation of the HTML reports. report Generates an HTML report from either a saved-dataset or the shared-data. delete Deletes a saved-dataset. export Exports one or all saved-datasets into a JSON file. import Imports the saved-datasets from a JSON file, created with the export command. """) def help_run(): print(""" usage: plprofiler run [OPTIONS] Runs one or more SQL commands (hopefully invoking one or more PL/pgSQL functions and/or triggers), then turns the local-data into an HTML report and/or a saved-dataset. OPTIONS: --name=NAME The name of the data set to use in the HTML report or saved-dataset. --title=TITLE Ditto. --desc=DESC Ditto. -c, --command=CMD The SQL string to execute. Can be multiple SQL commands, separated by semicolon. -f, --file=FILE Read SQL commands to execute from FILE. --save Create a saved-dataset. --force Overwrite an existing saved-dataset of the same NAME. --output=FILE Save an HTML report in FILE. --top=N Include up to N function detail descriptions in the report (default=10). """) def help_monitor(): print(""" usage: plprofiler monitor [OPTIONS] Turns profile data capturing and periodic saving on for either all database backends, or a single one (specified by PID), waits for a specified amount of time, then turns it back off. If during that time the application (or specific backend) is executing queries, that invoke PL/pgSQL functions, profile statistics will be collected into shared-data at the specified interval as well as every transaction end (commit or rollback). The resulting saved-data can be used with the "save" and "report" commands and cleared with "reset". NOTES: The change in configuration options will become visible to running backends when they go through the PostgreSQL TCOP loop. That is, when they receive the next "client" command, like a query or prepared statement execution request. They will not start/stop collecting data while they are in the middle of a long-running query. REQUIREMENTS: This command uses PostgreSQL features, that are only available in version 9.4 and higher. The plprofiler extension must be loaded via the configuration option "shared_preload_libraries" in the postgresql.conf file. OPTIONS: --pid=PID The PID of the backend, to monitor. If not given, the entire PostgreSQL instance will be suspect to monitoring. --interval=SEC Interval in seconds at which the monitored backend(s) will copy the local-data to shared-data and then reset their local-data. --duration=SEC Duration of the monitoring run in seconds. """) def help_reset(): print(""" usage: plprofiler reset Deletes all data from the shared hashtables. This affects all databases in the cluster. This does NOT destroy any of the saved-datasets. """) def help_save(): print(""" usage: plprofiler save [OPTIONS] The save command is used to create a saved-dataset from shared-data. Saved datasets are independent from the system catalog, since all their Oid based information has been resolved into textual object descriptions. Their reports can be recreated later or even on another system (after transport via export/import). OPTIONS: --name=NAME The name of the saved-dataset. Must be unique. --title=TITLE The title used by the report command in the tag of the generated HTML output. --desc=DESC An HTML formatted paragraph (or more) that describes the profiling report. --force Overwite an existing saved-dataset with the same NAME. NOTES: If the options for TITLE and DESC are not specified on the command line, the save command will launch an editor, allowing to edit the default report configuration. This metadata can later be changed with the "edit" command. """) def help_list(): print(""" usage: plprofiler list Lists the available saved-datasets together with their TITLE. """) def help_edit(): print(""" usage: plprofiler edit [OPTIONS] Launches an editor with the metadata of the specified saved-dataset. This allows to change not only the metadata itself, but also the NAME of the saved-dataaset. OPTIONS: --name=NAME The name of the saved-dataset to edit. """) def help_report(): print(""" usage: plprofiler report [OPTIONS] Create an HTML report from either shared-data or a saved-dataset. OPTIONS: --from-shared Use the shared-data rather than a saved-dataset. --name=NAME The name of the saved-dataset to load or the NAME to use with --from-shared. --title=TITLE Override the TITLE found in the saved-dataset's metadata, or the TITLE to use with --from-shared. --desc=DESC Override the DESC found in the saved-dataset's metadata, or the DESC to use with --from-shared. --output=FILE Destination for the HTML report (default=stdout). --top=N Include up to N function detail descriptions in the report (default=10). """) def help_delete(): print(""" usage: plprofiler delete [OPTIONS] Delete the named saved-dataset. OPTIONS: --name=NAME The name of the saved-dataset to delete. """) def help_export(): print(""" usage: plprofiler export [OPTIONS] Export the shared-data or one or more saved-datasets as a JSON document. OPTIONS: --all Export all saved-datasets. --from-shared Export the shared-data instead of a saved-dataset. --name=NAME The NAME of the dataset to save. --title=TITLE The TITLE of the dataset in the export. --desc=DESC The DESC of the dataset in the export. --edit Launches the config editor for each dataset, included in the export. --output=FILE Save the JSON export data in FILE (default=stdout). """) def help_import(): print(""" usage: plprofiler import [OPTIONS] Imports one or more datasets from an export file. OPTIONS: -f, --file=FILE Read the profile data from FILE. This should be the output of a previous "export" command. --edit Edit each dataset's metadata before storing it as a saved-dataset. --force Overwrite any existing saved-datasets with the same NAMEs, as they appear in the input file (or after editing). """) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������plprofiler-REL4_2_5/python-plprofiler/plprofiler/sql_split.py���������������������������������������0000664�0000000�0000000�00000013544�14657354554�0024675�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import re import sys def main(): for fname in sys.argv[1:]: with open(fname, 'r') as fd: statements = sql_split(fd.read()).statements # print statements class sql_split: def __init__(self, sql_str): self.sql_str = sql_str self.sql_len = len(sql_str) self.sql_idx = 0 self.statements = [] self.cur_stmt = "" self.paren_level = 0 self.dol_quote_re = re.compile("(\\$[^$\\s]*\\$)") self.state_normal() if not self.cur_stmt.isspace(): self.have_stmt() def get_statements(self): return self.statements def state_normal(self): while self.sql_idx < self.sql_len: if self.sql_str[self.sql_idx] == "'": self.state_quote() elif self.sql_str[self.sql_idx] == '"': self.state_d_quote() elif self.sql_str[self.sql_idx:self.sql_idx + 2] == "--": self.state_sql_comment() elif self.sql_str[self.sql_idx:self.sql_idx + 2] == "/*": self.state_C_comment() elif self.sql_str[self.sql_idx:self.sql_idx + 2] == "E'": self.state_e_quote("E'") elif self.sql_str[self.sql_idx:self.sql_idx + 2] == "e'": self.state_e_quote("e'") elif self.sql_str[self.sql_idx] == "(": self.cur_stmt += "(" self.sql_idx += 1 self.paren_level += 1 elif self.sql_str[self.sql_idx] == ")": self.cur_stmt += ")" self.sql_idx += 1 self.paren_level -= 1 elif self.sql_str[self.sql_idx] == "[": self.cur_stmt += "[" self.sql_idx += 1 self.paren_level += 1 elif self.sql_str[self.sql_idx] == "]": self.cur_stmt += "]" self.sql_idx += 1 self.paren_level -= 1 elif self.sql_str[self.sql_idx] == ";": self.cur_stmt += ";" self.sql_idx += 1 if self.paren_level == 0: self.have_stmt() else: m = self.dol_quote_re.match(self.sql_str[self.sql_idx:]) if m is not None: self.state_dollar_quote(m.groups()[0]) else: self.cur_stmt += self.sql_str[self.sql_idx] self.sql_idx += 1 def have_stmt(self): if self.cur_stmt.strip() != "": self.statements.append(self.cur_stmt) self.cur_stmt = "" while self.sql_idx < self.sql_len: if self.sql_str[self.sql_idx].isspace(): self.sql_idx += 1 else: break def state_sql_comment(self): self.cur_stmt += "--" self.sql_idx += 2 while self.sql_idx < self.sql_len: if self.sql_str[self.sql_idx:self.sql_idx + 2] == '\r\n': self.cur_stmt += '\r\n' self.sql_idx += 2 return elif self.sql_str[self.sql_idx] == '\n': self.cur_stmt += '\n' self.sql_idx += 1 return else: self.cur_stmt += self.sql_str[self.sql_idx] self.sql_idx += 1 def state_C_comment(self): self.cur_stmt += "/*" self.sql_idx += 2 while self.sql_idx < self.sql_len: if self.sql_str[self.sql_idx:self.sql_idx + 2] == '*/': self.cur_stmt += '*/' self.sql_idx += 2 return else: self.cur_stmt += self.sql_str[self.sql_idx] self.sql_idx += 1 def state_quote(self): self.cur_stmt += "'" self.sql_idx += 1 while self.sql_idx < self.sql_len: if self.sql_str[self.sql_idx:self.sql_idx + 2] == "''": self.cur_stmt += "''" self.sql_idx += 2 elif self.sql_str[self.sql_idx] == "'": self.cur_stmt += "'" self.sql_idx += 1 return else: self.cur_stmt += self.sql_str[self.sql_idx] self.sql_idx += 1 def state_d_quote(self): self.cur_stmt += '"' self.sql_idx += 1 while self.sql_idx < self.sql_len: if self.sql_str[self.sql_idx:self.sql_idx + 2] == '""': self.cur_stmt += '""' self.sql_idx += 2 elif self.sql_str[self.sql_idx] == '"': self.cur_stmt += '"' self.sql_idx += 1 return else: self.cur_stmt += self.sql_str[self.sql_idx] self.sql_idx += 1 def state_e_quote(self): self.cur_stmt += "'" self.sql_idx += 1 while self.sql_idx < self.sql_len: if self.sql_str[self.sql_idx] == '\\': self.cur_stmt += self.sql_str[self.sql_idx:self.sql_idx + 2] self.sql_idx += 2 elif self.sql_str[self.sql_idx:self.sql_idx + 2] == "''": self.cur_stmt += "''" self.sql_idx += 2 elif self.sql_str[self.sql_idx] == "'": self.cur_stmt += "'" self.sql_idx += 1 return else: self.cur_stmt += self.sql_str[self.sql_idx] self.sql_idx += 1 def state_dollar_quote(self, tag): tag_len = len(tag) self.cur_stmt += tag self.sql_idx += tag_len while self.sql_idx < self.sql_len: if self.sql_str[self.sql_idx:self.sql_idx + tag_len] == tag: self.cur_stmt += tag self.sql_idx += tag_len return self.cur_stmt += self.sql_str[self.sql_idx] self.sql_idx += 1 if __name__ == '__main__': main() ������������������������������������������������������������������������������������������������������������������������������������������������������������plprofiler-REL4_2_5/python-plprofiler/setup.py������������������������������������������������������0000664�0000000�0000000�00000002052�14657354554�0021635�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from setuptools import setup setup( name = 'plprofiler-client', description = 'PL/pgSQL Profiler module and command line tool', version = '4.2', author = 'Jan Wieck', author_email = 'jan@wi3ck.info', url = 'https://github.com/bigsql/plprofiler', license = 'Artistic License and CDDL', packages = ['plprofiler', ], long_description = """PL/pgSQL Profiler module and command line tool ============================================== This is the Python module and command line tool to control the PL/pgSQL Profiler extension for PostgreSQL. Please visit https://github.com/bigsql/plprofiler for the main project.""", long_description_content_type = 'text/markdown', package_data = { 'plprofiler': [ 'lib/FlameGraph/README', 'lib/FlameGraph/README-plprofiler', 'lib/FlameGraph/flamegraph.pl', ], }, install_requires = [ 'configparser', ], entry_points = { 'console_scripts': [ 'plprofiler = plprofiler:main', ] }, ) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������