├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── NEWS.md ├── README.md ├── bootstrap.php ├── composer.json ├── doc ├── measure_body_size.php └── sample.php ├── phpunit.xml.dist ├── src ├── Pinba.php ├── PinbaClient.php ├── PinbaFunctions.php └── Prtbfr.php └── tests ├── 01GatherTest.php ├── 02FlushTest.php ├── 03ClassTest.php ├── APITest.php ├── benchmark.php ├── ci ├── docker-compose.yml ├── images │ ├── php │ │ ├── Dockerfile │ │ ├── config │ │ │ ├── apache_phpfpm_proxyfcgi │ │ │ ├── apache_vhost │ │ │ └── codecoverage_xdebug.ini │ │ ├── entrypoint.sh │ │ └── setup │ │ │ ├── create_user.sh │ │ │ ├── install_packages.sh │ │ │ ├── setup_adminer.sh │ │ │ ├── setup_apache.sh │ │ │ ├── setup_code_coverage.sh │ │ │ ├── setup_composer.sh │ │ │ ├── setup_php.sh │ │ │ ├── setup_pinba.sh │ │ │ └── setup_pinboard.sh │ ├── pinba │ │ ├── Dockerfile │ │ ├── entrypoint.sh │ │ └── setup │ │ │ └── mysql_init.sql │ └── pinba2 │ │ ├── Dockerfile │ │ ├── entrypoint.sh │ │ └── setup │ │ ├── mysql_init.sql │ │ └── setup_mysql.sh └── vm.sh ├── phpunit_coverage.php └── pinba.proto /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.operating-system }} 8 | env: 9 | PINBA_SERVER: 127.0.0.1 10 | PINBA_PORT: 30002 11 | PINBA_DB_SERVER: 127.0.0.1 12 | PINBA_DB_PORT: 3306 13 | PINBA_DB_USER: pinba 14 | PINBA_DB_PASSWORD: pinba 15 | PINBA_DB_DATABASE: pinba 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | # @see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners 21 | # @todo run some tests on 'windows-latest' (needs test env setup scripts for windows to be developed) 22 | - php: '8.1' 23 | pinba-container: anchorfree/pinba2 24 | operating-system: ubuntu-22.04 25 | - php: '8.0' 26 | pinba-container: tony2001/pinba 27 | operating-system: ubuntu-22.04 28 | - php: 'default' 29 | # this runs on php 7.4 with the native pinba ext installed (ver 1.1.1) 30 | pinba-container: anchorfree/pinba2 31 | operating-system: ubuntu-20.04 32 | - php: '7.3' 33 | pinba-container: tony2001/pinba 34 | operating-system: ubuntu-22.04 35 | # nb: the version of phpunit we use does not support code coverage generation on php 8. 36 | # we prefer to run code coverage on a php install without the native extension, hence here 37 | code-coverage: true 38 | - php: '7.2' 39 | pinba-container: anchorfree/pinba2 40 | operating-system: ubuntu-20.04 41 | - php: '7.1' 42 | pinba-container: tony2001/pinba 43 | operating-system: ubuntu-20.04 44 | - php: '7.0' 45 | pinba-container: anchorfree/pinba2 46 | operating-system: ubuntu-20.04 47 | - php: '5.6' 48 | pinba-container: tony2001/pinba 49 | operating-system: ubuntu-20.04 50 | - php: '5.5' 51 | pinba-container: anchorfree/pinba2 52 | operating-system: ubuntu-22.04 53 | - php: '5.4' 54 | pinba-container: tony2001/pinba 55 | operating-system: ubuntu-20.04 56 | steps: 57 | - name: checkout code 58 | uses: actions/checkout@v3 59 | 60 | # Although this action is quite nice, we prefer to use the same script to set up php that we use for the 61 | # docker image used for local testing. This allows us to make sure that script is always in good shape 62 | #- name: set up php 63 | # uses: shivammathur/setup-php@v2 64 | # with: 65 | # php-version: ${{ matrix.php }} 66 | # extensions: curl, dom, mbstring, xsl 67 | # ini-values: 'cgi.fix_pathinfo=1, always_populate_raw_post_data=-1' 68 | # #tools: ${{ matrix.phpunit-version }} 69 | # coverage: ${{ matrix.code-coverage}} 70 | 71 | - name: set up env 72 | # @todo add env setup scripts for windows 73 | run: | 74 | chmod 755 ./tests/ci/images/php/setup/*.sh 75 | sudo --preserve-env=GITHUB_ACTIONS ./tests/ci/images/php/setup/setup_apache.sh 76 | sudo --preserve-env=GITHUB_ACTIONS ./tests/ci/images/php/setup/setup_php.sh ${{ matrix.php }} 77 | sudo --preserve-env=GITHUB_ACTIONS ./tests/ci/images/php/setup/setup_composer.sh 78 | # no need for pinboard (yet) 79 | #sudo --preserve-env=GITHUB_ACTIONS ./tests/ci/images/php/setup/setup_pinboard.sh 80 | # fix fs perms for recent Apache versions configuration (ie. starting from Jammy) 81 | f="$(pwd)"; while [[ $f != / ]]; do sudo chmod +rx "$f"; f=$(dirname "$f"); done; 82 | find . -type d -exec chmod +rx {} \; 83 | find . -type f -exec chmod +r {} \; 84 | 85 | - name: start pinba as container 86 | # @todo could we use the `services` config key? to assign a container name, we could use `options: --name mycontainer` 87 | run: | 88 | # in case mysql is running on port 3306 89 | sudo /etc/init.d/mysql stop || true 90 | if [ "${{ matrix.pinba-container }}" = anchorfree/pinba2 ]; then 91 | container=$(docker run -d -p 127.0.0.1:30002:3002/udp -p 127.0.0.1:3306:3306/tcp "${{ matrix.pinba-container }}") 92 | # @todo instead of waiting, ping the server in a loop for a few seconds 93 | sleep 20 94 | docker exec -i "$container" mysql -h 127.0.0.1 -u root < ./tests/ci/images/pinba2/setup/mysql_init.sql 95 | else 96 | container=$(docker run -d -p 127.0.0.1:30002:30002/udp -p 127.0.0.1:3306:3306/tcp "${{ matrix.pinba-container }}") 97 | docker exec "$container" apt-get update 98 | docker exec "$container" apt-get -y install mysql-client 99 | # @todo instead of waiting, ping the server in a loop for a few seconds 100 | sleep 5 101 | docker exec -i "$container" mysql -h 127.0.0.1 -u root < ./tests/ci/images/pinba/setup/mysql_init.sql 102 | fi 103 | 104 | 105 | # Avoid downloading composer deps on every workflow run. Is this useful for us? Caching the installation of 106 | # php/apache or the docker container images would be more useful... 107 | #- 108 | # uses: actions/cache@v2 109 | # with: 110 | # path: /tmp/composer-cache 111 | # key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} 112 | 113 | - name: install dependencies 114 | run: 'composer install' 115 | 116 | - name: run tests and upload coverage info if needed 117 | run: | 118 | if [ -z "${{ matrix.code-coverage }}" ]; then 119 | ./vendor/bin/phpunit -v tests 120 | else 121 | ./tests/ci/images/php/setup/setup_code_coverage.sh enable 122 | ./vendor/bin/phpunit -v --coverage-clover=coverage.clover tests 123 | if [ -f coverage.clover ]; then 124 | wget https://uploader.codecov.io/latest/linux/codecov && \ 125 | chmod +x codecov && \ 126 | ./codecov -f coverage.clover 127 | else 128 | echo "WARNING: code coverage not generated. Is xdebug disabled?" 129 | fi 130 | fi 131 | 132 | # @todo would it be useful to run a 2nd test with composer --prefer-lowest? After all the only dependencies we have are testing tools 133 | 134 | - name: failure troubleshooting 135 | if: ${{ failure() }} 136 | run: | 137 | #env 138 | #php -i 139 | #ps auxwww 140 | docker ps 141 | #dpkg --list | grep php 142 | #ps auxwww | grep fpm 143 | #pwd 144 | #sudo env 145 | #systemctl status apache2.service 146 | #ls -la /etc/apache2/mods-enabled 147 | #ls -la /etc/apache2/conf-enabled 148 | #ls -la /etc/apache2/mods-available 149 | #ls -la /etc/apache2/conf-available 150 | #ls -la /etc/apache2/sites-available/ 151 | #sudo cat /etc/apache2/envvars 152 | #sudo cat /etc/apache2/sites-available/000-default.conf 153 | #ls -ltr /var/log 154 | #ls -ltr /var/log/apache2 155 | sudo cat /var/log/apache2/error.log 156 | sudo cat /var/log/apache2/other_vhosts_access.log 157 | sudo cat /var/log/php*.log 158 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.scannerwork 2 | /.idea 3 | /vendor/* 4 | /.phpunit.result.cache 5 | /composer.lock 6 | /phpunit.xml 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | v1.1.1 - 13/12/2022 2 | 3 | * improved: if any of the string values sent to the Pinba server include a NUL character (`chr(0)`), behave the same way 4 | as the php extension does, ie. truncating the string to the part before the first NUL character occurrence 5 | * improved: added to the tests directory the .proto file, and made available within the test containers the `protoc` 6 | command-line tool, to simplify decoding of raw messages 7 | 8 | v1.1 - 12/12/2022 9 | 10 | * improved: added new ini setting `pinba.inhibited` which drastically reduces time and memory usage when you want 11 | to really disable all measurement overhead while leaving code instrumented 12 | * improved: fixed memory consumption measurement in `benchmark.php` 13 | 14 | v1.0 - 10/12/2022 15 | 16 | * improved: replicate extension behaviour: convert tag values to string upon setting them 17 | * improved: replicate extension behaviour: delete stopped timers when calling `flush` even if `pinba.enabled=0` 18 | * improved: increased test code coverage. Also, use utf8 for the reports table in the pinba2 test db 19 | * improved: added a benchmark file and documented expected performances 20 | 21 | v0.4 - 8/12/2022 22 | 23 | * fixed a bug in merging timers tags, introduced in v0.3 24 | * fixed: request tags were not being properly sent to the server 25 | * fixed: parsing IPv6 addresses, or addresses in the form `[127.0.0.1]:8080` in `pinba_server` configuration option 26 | * fixed: return type of `pinba_timers_get` 27 | * improved: added support for `pinba.auto_flush` configuration option 28 | * improved: report automatically to Pinba the script's http status code by default 29 | * improved: support custom `hit_count` values on timer creation 30 | * improved: added 4th parameter `$hit_count = 1` to `pinba_timer_add` 31 | * improved: support (undocumented) function `pinba_get_data` 32 | * improved: support (undocumented) function `pinba_reset` 33 | * improved: replicate extension behaviour: return `false` instead of `null` on a failed `pinba_tag_get` call 34 | * improved: replicate extension behaviour: default `req_count` is 1 in data from `get_info()`, but 0 as sent to the server 35 | * improved: replicate extension behaviour: once flushed, timers are not visible any more in `pinba_get_info` and `pinba_timers_get` calls 36 | * improved: replicate extension behaviour: a `PinbaClient` object will not flush automatically upon destruction if 37 | it was flushed manually beforehand 38 | * improved: added one more sample file: doc/measure_body_size.php 39 | 40 | v0.3 - 6/12/2022 41 | 42 | * fixed default value for `$flag` argument of `pinba_timers_get` 43 | * fixed return value of `pinba_tag_delete` 44 | * fixed: when creating two or more timers with the same tag values, but tags in different order, they would not be merged 45 | * added: class `PinbaClient` - with a few limitations, see README 46 | * added method: `pinba_reset` 47 | * improved: added more sanity checks of function arguments values, closely matching the behaviour of the extension 48 | * improved test code coverage 49 | 50 | v0.2 - 5/12/2022 51 | 52 | * added constants: `PINBA_FLUSH_ONLY_STOPPED_TIMERS`, `PINBA_FLUSH_RESET_DATA`, `PINBA_ONLY_RUNNING_TIMERS`, `PINBA_AUTO_FLUSH`, 53 | `PINBA_ONLY_STOPPED_TIMERS` 54 | * added methods: `pinba_timer_add`, `pinba_timers_get`, `pinba_schema_set`, `pinba_server_name_set`, `pinba_request_time_set` 55 | `pinba_tag_set`, `pinba_tag_get`, `pinba_tag_delete`, `pinba_tags_get` 56 | * fixed return value for methods: `pinba_script_name_set`, `pinba_hostname_set` 57 | * fixed: `pinba_flush` now stops all timers by default. It also supports 2nd argument `$flags` to change its behaviour 58 | * added non-API methods: `Pinba::ini_set` and `Pinba::ini_get` 59 | * made sure CI tests can successfully connect to the pinba servers and query them 60 | 61 | v0.1 - 3/12/2022 62 | 63 | Changes compared to the previous state (2013 commits): this thing now works well enough to send data to a Pinba server 64 | 65 | * fix bugs with non-existing class method being called 66 | * fix float fields in protobuf messages sent 67 | * various API compatibility fixes 68 | * add CI tests using GitHub Actions 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Polyfill-Pinba 2 | ============== 3 | 4 | Pure-php reimplementation of the "PHP extension for Pinba". 5 | 6 | See http://pinba.org for the original. 7 | 8 | ## Requirements 9 | 10 | PHP 5.3 or any later version. 11 | 12 | A Pinba server to send the data to. Known servers include http://pinba.org/ and https://github.com/badoo/pinba2. Both 13 | are available as Docker Container images for trying out. 14 | 15 | ## Installation 16 | 17 | composer require gggeek/polyfill-pinba 18 | 19 | then set up configuration settings `pinba.enabled` and `pinba.server` in `php.ini` as described at 20 | https://github.com/tony2001/pinba_engine/wiki/PHP-extension#INI_Directives 21 | 22 | That's all. 23 | 24 | **NB** gathering of metrics and sending them to the server is disabled by default. You _have_ to enable it via ini settings, 25 | unless you have added explicit calls to the pinba api in your php code. 26 | 27 | ## Usage 28 | 29 | See the API described at https://github.com/tony2001/pinba_engine/wiki/PHP-extension 30 | 31 | A trivial usage example can be found in [doc/sample.php](doc/sample.php). 32 | 33 | For viewing the gathered metrics, check out https://github.com/intaro/pinboard, https://github.com/pinba-server/pinba-server 34 | or https://github.com/ClickHouse-Ninja/Proton 35 | 36 | ### Extensions to the original API 37 | 38 | #### Pinba::ini_set 39 | 40 | If the pinba php extension is not enabled in your setup (which is most likely the case, as otherwise you would not 41 | be using this package), it is not possible from php code to modify the values for ini options `pinba.enabled` and 42 | `pinba.server`. While it is possible to set their value in `php.ini`, if you want to modify their value at runtime you 43 | will have instead to use methods `\PinbaPhp\Polyfill\pinba::ini_set($option, $value)`. You should also use corresponding 44 | method `\PinbaPhp\Polyfill\pinba::ini_get($option)` to check it. 45 | 46 | ### ini option `pinba.inhibited` 47 | 48 | In case you want to keep your code instrumented with `pinba_timer_add`, `pinba_timer_stop` and similar calls but are 49 | not collecting the pinba data anymore, and you want to reduce as much as possible the overhead imposed by this package, 50 | please set in `pinba.inhibited=1` `php.ini`. 51 | 52 | Using `pinba.enabled=0` or `pinba.auto_flush=0` is not recommended in that scenario, as, while they both disable the sending 53 | of data, they do not prevent timers to be actually created. 54 | 55 | ## Compatibility 56 | 57 | We strive to implement the same API as Pinba extension ver. 1.1.2. 58 | 59 | As for the server side, the library is tested for compatibility against both a Pinba server and a Pinba2 one. 60 | 61 | Features not (yet) supported: 62 | - Timers data misses `ru_utime` and `ru_stime` members. This is true also for timers added to `PinbaClient` instances 63 | 64 | Known issues - which cannot / won't be fixed: 65 | - lack in precision in time reporting: the time reported for page execution will be much shorter with any php code than 66 | it can be measured with a php extension. We suggest thus not to take the time reported by this package as an absolute 67 | value, but rather use it to check macro-issues, such as a page taking 10 seconds to run, or 10 times as much as another 68 | page. In the demo file [doc/sample.php](doc/sample.php) we showcase how to make time measurement as precise as possible 69 | - impact on system performances: the cpu time and ram used by this implementation (which runs on every page of your site!) 70 | are also bigger than the resources used by the php extension. It is up to you to decide if the extra load added to 71 | your server by using this package is worth it or not, esp. for heavily loaded production servers 72 | - the warnings raised when incorrect data is passed to the pinba php functions are of severity `E_USER_WARNING` instead of 73 | `E_WARNING` 74 | - in the data reported by `pinba_get_info` and reported to the Pinba server, `doc_size` has always a value of 0. This 75 | can be worked around by using an instance of `PinbaClient` and calling `setDocumentSize` - see 76 | [doc/measure_body_size.php](doc/measure_body_size.php) for an example 77 | - in the data reported to the Pinba server, `memory_footprint` has always a fixed value of 0 or is not reported at all. 78 | Again, using a `PinbaClient` instance can fix that - but there is no php function available that I know of which can 79 | report the equivalent usage of the `mallinfo` C call done by the php extension 80 | - ini setting `pinba.resolve_interval` is not supported and most likely never will 81 | - the default value reported to the Pina engine for the `schema` field is an empty string, rather than not being set at 82 | all. This results in the database table storing a value of '' instead of NULL. At the same time, sending a 83 | value of NULL makes the server-side engine re-use the last non-null value from a previous pinba packet, which seems a 84 | faulty behaviour 85 | - the `pinba_reset` call does delete all exiting timers, unlike what the same function from the php extension does. 86 | Again, the upstream behaviour does feel faulty 87 | 88 | ## Performances 89 | 90 | These results are indicative of the time and memory overhead of executing 1000 function calls in a loop, and instrumenting 91 | each execution with a separate timer. 92 | 93 | As you can see, the execution delay introduced is very small, less than 1 millisecond. The memory overhead is proportional 94 | to the number of timers added and the tags attached to each timer. 95 | 96 | ``` 97 | No timing: 0.00001 secs, 0 bytes used 98 | Pinba-extension: 0.00072 secs, 280.640 bytes used 99 | PHP-Pinba: 0.00062 secs, 412.920 bytes used 100 | ``` 101 | 102 | NB: weirdly enough, the php extension seems to be slightly slower on average than the pure-php implementations. Having 103 | taken a cursory look at the C code of the extension, I suspect this is because it executes too many `gettimeofday` calls... 104 | 105 | In case you want to keep your code instrumented with lots of `pinba_timer_start` calls, and reduce the overhead of using 106 | the extension as much as possible (while of course not measuring anything anymore), you can set `pinba.inhibited=1` in php.ini. 107 | 108 | With that set, this is the overhead you can expect for "timing" 1000 executions of a function call: 109 | 110 | ``` 111 | No timing: 0.00001 secs, 0 bytes used 112 | PHPPinba timed: 0.00009 secs, 0 bytes used 113 | ``` 114 | 115 | (tests executed with php 7.4 in an Ubuntu Focal container, running within an Ubuntu Jammy VM with 4 vCPU allocated) 116 | 117 | ## Notes 118 | 119 | Includes code from the Protobuf for PHP lib by Iván -DrSlump- Montes: https://github.com/drslump/Protobuf-PHP 120 | 121 | Other known packages exist implementing the same idea, such as: https://github.com/vearutop/pinba-pure-php 122 | 123 | ## FAQ 124 | 125 | - **Q:** Can I run the polyfill in conjunction with the pinba php extension? **A:** yes, although I fail to see the 126 | reason why you would do that. When doing so, unless taking care to selectively disable either the php extension 127 | or this bundle (f.e. via calls to `Pinba::ini_set` in your code), you will get double data reported to the Pinba server 128 | 129 | ## Running tests 130 | 131 | The recommended way to run the library's test suite is via the provided Docker containers and the corresponding 132 | Docker Compose configuration. 133 | A handy shell script is available that simplifies usage of Docker and Docker Compose. 134 | 135 | The full sequence of operations is: 136 | 137 | ./tests/ci/vm.sh build 138 | ./tests/ci/vm.sh start 139 | ./tests/ci/vm.sh runtests 140 | ./tests/ci/vm.sh stop 141 | 142 | # and, once you have finished all testing related work: 143 | ./tests/ci/vm.sh cleanup 144 | 145 | By default, tests are run using php 7.4 in a Container based on Ubuntu 20 Focal. The data is sent to a container running 146 | the original Pinba server - it can also be configured to be sent to a container running the Pinba2 server instead. 147 | 148 | You can change the version of PHP and Ubuntu in use by setting the environment variables `PHP_VERSION` and `UBUNTU_VERSION` 149 | before building the containers. 150 | 151 | You can switch the target Container used for the testsuite to the one running Pinba2 by setting the environment variables 152 | `PINBA_DB_SERVER=pinba2`, `PINBA_SERVER=pinba2` and `PINBA_PORT=3002` before starting the containers. 153 | 154 | ### Testing tips 155 | 156 | * to debug the communication between php and the Pinba server, it is possible to use tcpdump within the `php` container: 157 | 158 | sudo tcpdump udp -w packets.cap 159 | 160 | once the capture file is saved, it can be analyzed by fe. opening it in Wireshark. To improve the ability of 161 | Wireshark of decoding the protobuf-formatted messages, set up its configuration as described at https://wiki.wireshark.org/Protobuf.md. 162 | The `.proto` file describing the messages used by Pinba can be found at https://github.com/badoo/pinba2/blob/master/proto/pinba.proto 163 | 164 | As an alternative which does not require Wireshark, it is also possible to save to a file the string resulting from 165 | `pinba_get_data()` calls, then decode it using the `protoc` tool - which is included by default in the test container: 166 | 167 | php myTestFile > test.rawmsg 168 | protoc --decode=Pinba.Request tests/pinba.proto < test.rawmsg 169 | 170 | ## License 171 | 172 | Use of this software is subject to the terms in the [license](LICENSE) file 173 | 174 | [![License](https://poser.pugx.org/gggeek/polyfill-pinba/license)](https://packagist.org/packages/gggeek/polyfill-pinba) 175 | [![Latest Stable Version](https://poser.pugx.org/gggeek/polyfill-pinba/v/stable)](https://packagist.org/packages/gggeek/polyfill-pinba) 176 | [![Total Downloads](https://poser.pugx.org/gggeek/polyfill-pinba/downloads)](https://packagist.org/packages/gggeek/polyfill-pinba) 177 | 178 | [![Build Status](https://github.com/gggeek/pinba_php/actions/workflows/ci.yml/badge.svg)](https://github.com/gggeek/pinba_php/actions/workflows/ci.yml) 179 | [![Code Coverage](https://codecov.io/gh/gggeek/pinba_php/branch/master/graph/badge.svg)](https://app.codecov.io/gh/gggeek/pinba_php) 180 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | "value". Cannot contain numeric indexes for obvious reasons. 20 | * @param array $data optional array with user data, not sent to the server. 21 | * @param int $hit_count 22 | * @return resource|int Always returns new timer resource. 23 | */ 24 | function pinba_timer_start($tags, $data = array(), $hit_count = 1) 25 | { 26 | return pinba::timer_start($tags, $data, $hit_count); 27 | } 28 | } 29 | 30 | if (!function_exists('pinba_timer_stop')) { 31 | /** 32 | * Stops the timer. 33 | * 34 | * @param resource|int $timer valid timer resource. 35 | * @return bool Returns true on success and false on failure (if the timer has already been stopped). 36 | */ 37 | function pinba_timer_stop($timer) 38 | { 39 | return pinba::timer_stop($timer); 40 | } 41 | } 42 | 43 | if (!function_exists('pinba_timer_add')) { 44 | /** 45 | * Creates new timer. This timer is already stopped and have specified time value. 46 | * 47 | * @param array $tags an array of tags and their values in the form of "tag" => "value". Cannot contain numeric indexes for obvious reasons. 48 | * @param int $value timer value for new timer. 49 | * @param array $data optional array with user data, not sent to the server. 50 | * @param int $hit_count 51 | * @return resource|int|false Always returns new timer resource. 52 | */ 53 | function pinba_timer_add($tags, $value, $data = null, $hit_count = 1) 54 | { 55 | return pinba::timer_add($tags, $value, $data, $hit_count); 56 | } 57 | } 58 | 59 | if (!function_exists('pinba_timer_delete')) { 60 | /** 61 | * Deletes the timer. 62 | * 63 | * @param resource|int $timer valid timer resource. 64 | * @return bool Returns true on success and false on failure. 65 | */ 66 | function pinba_timer_delete($timer) 67 | { 68 | return pinba::timer_delete($timer); 69 | } 70 | } 71 | 72 | if (!function_exists('pinba_timer_tags_merge')) { 73 | /** 74 | * Merges $tags array with the timer tags replacing existing elements. 75 | * 76 | * @param resource|int $timer - valid timer resource 77 | * @param array $tags - an array of tags. 78 | * @return bool 79 | */ 80 | function pinba_timer_tags_merge($timer, $tags) 81 | { 82 | return pinba::timer_tags_merge($timer, $tags); 83 | } 84 | } 85 | 86 | if (!function_exists('pinba_timer_tags_replace')) { 87 | /** 88 | * Replaces timer tags with the passed $tags array. 89 | * 90 | * @param resource|int $timer - valid timer resource 91 | * @param array $tags - an array of tags. 92 | * @return bool 93 | */ 94 | function pinba_timer_tags_replace($timer, $tags) 95 | { 96 | return pinba::timer_tags_replace($timer, $tags); 97 | } 98 | } 99 | 100 | if (!function_exists('pinba_timer_data_merge')) { 101 | /** 102 | * Merges $data array with the timer user data replacing existing elements. 103 | * 104 | * @param resource|int $timer valid timer resource 105 | * @param array $data an array of user data. 106 | * @return bool Returns true on success and false on failure. 107 | */ 108 | function pinba_timer_data_merge($timer, $data) 109 | { 110 | return pinba::timer_data_merge($timer, $data); 111 | } 112 | } 113 | 114 | if (!function_exists('pinba_timer_data_replace')) { 115 | /** 116 | * Replaces timer user data with the passed $data array. 117 | * Use NULL value to reset user data in the timer. 118 | * 119 | * @param resource|int $timer valid timer resource 120 | * @param array $data an array of user data. 121 | * @return bool Returns true on success and false on failure. 122 | */ 123 | function pinba_timer_data_replace($timer, $data) 124 | { 125 | return pinba::timer_data_replace($timer, $data); 126 | } 127 | } 128 | 129 | if (!function_exists('pinba_timer_get_info')) { 130 | /** 131 | * Returns timer data. 132 | * 133 | * @param resource|int $timer - valid timer resource. 134 | * @return array Output example: 135 | * array(4) { 136 | * ["value"]=> float(0.0213) 137 | * ["tags"]=> array(1) { 138 | * ["foo"]=> string(3) "bar" 139 | * } 140 | * ["started"]=> bool(true) 141 | * ["data"]=> NULL 142 | * } 143 | */ 144 | function pinba_timer_get_info($timer) 145 | { 146 | return pinba::timer_get_info($timer); 147 | } 148 | } 149 | 150 | if (!function_exists('pinba_timers_stop')) { 151 | /** 152 | * Stops all running timers. 153 | * 154 | * @return bool 155 | */ 156 | function pinba_timers_stop() 157 | { 158 | return pinba::timers_stop(); 159 | } 160 | } 161 | 162 | if (!function_exists('pinba_timers_get')) { 163 | /** 164 | * Get all timers' info. 165 | * 166 | * @param int $flag - can be set to PINBA_ONLY_STOPPED_TIMERS 167 | * @return resource[]|int[] 168 | */ 169 | function pinba_timers_get($flag = 0) 170 | { 171 | return pinba::timers_get($flag); 172 | } 173 | } 174 | 175 | if (!function_exists('pinba_get_info')) { 176 | /** 177 | * Returns all request data (including timers user data). 178 | * 179 | * @return array Example: 180 | * array(9) { 181 | * ["mem_peak_usage"]=> int(786432) 182 | * ["req_time"]=> float(0.001529) 183 | * ["ru_utime"]=> float(0) 184 | * ["ru_stime"]=> float(0) 185 | * ["req_count"]=> int(1) 186 | * ["doc_size"]=> int(0) 187 | * ["server_name"]=> string(7) "unknown" 188 | * ["script_name"]=> string(1) "-" 189 | * ["timers"]=> array(1) { 190 | * [0]=> array(4) { 191 | * ["value"]=> float(4.5E-5) 192 | * ["tags"]=> array(1) { 193 | * ["foo"]=> string(3) "bar" 194 | * } 195 | * ["started"]=> bool(true) 196 | * ["data"]=> NULL 197 | * } 198 | * } 199 | * } 200 | */ 201 | function pinba_get_info() 202 | { 203 | return pinba::get_info(); 204 | } 205 | } 206 | 207 | if (!function_exists('pinba_schema_set')) { 208 | /** 209 | * Set request schema (HTTP/HTTPS/whatever). 210 | * 211 | * @param string $schema 212 | * @return bool 213 | */ 214 | function pinba_schema_set($schema) 215 | { 216 | return pinba::schema_set($schema); 217 | } 218 | } 219 | 220 | if (!function_exists('pinba_script_name_set')) { 221 | /** 222 | * Set custom script name instead of $_SERVER['SCRIPT_NAME'] used by default. 223 | * Useful for those using front controllers, when all requests are served by one PHP script. 224 | * 225 | * @param string $script_name 226 | * @return bool 227 | */ 228 | function pinba_script_name_set($script_name) 229 | { 230 | return pinba::script_name_set($script_name); 231 | } 232 | } 233 | 234 | if (!function_exists('pinba_server_name_set')) { 235 | /** 236 | * Set custom server name instead of $_SERVER['SERVER_NAME'] used by default. 237 | * 238 | * @param string $server_name 239 | * @return bool 240 | */ 241 | function pinba_server_name_set($server_name) 242 | { 243 | return pinba::server_name_set($server_name); 244 | } 245 | } 246 | 247 | if (!function_exists('pinba_request_time_set')) { 248 | /** 249 | * Set custom Set custom request time. 250 | * 251 | * @param float $request_time 252 | * @return bool 253 | */ 254 | function pinba_request_time_set($request_time) 255 | { 256 | return pinba::request_time_set($request_time); 257 | } 258 | } 259 | 260 | if (!function_exists('pinba_tag_set')) { 261 | /** 262 | * @param string $tag 263 | * @param string $value 264 | * @return bool 265 | */ 266 | function pinba_tag_set($tag, $value) 267 | { 268 | return pinba::tag_set($tag, $value); 269 | } 270 | } 271 | 272 | if (!function_exists('pinba_tag_get')) { 273 | /** 274 | * @param string $tag 275 | * @return string|false 276 | */ 277 | function pinba_tag_get($tag) 278 | { 279 | return pinba::tag_get($tag); 280 | } 281 | } 282 | 283 | if (!function_exists('pinba_tag_delete')) { 284 | /** 285 | * @param string $tag 286 | * @return bool 287 | */ 288 | function pinba_tag_delete($tag) 289 | { 290 | return pinba::tag_delete($tag); 291 | } 292 | } 293 | 294 | if (!function_exists('pinba_tags_get')) { 295 | /** 296 | * @return array 297 | */ 298 | function pinba_tags_get() 299 | { 300 | return pinba::tags_get(); 301 | } 302 | } 303 | 304 | if (!function_exists('pinba_hostname_set')) { 305 | /** 306 | * Set custom hostname instead of the result of gethostname() used by default. 307 | * 308 | * @param string $hostname 309 | * @return bool 310 | */ 311 | function pinba_hostname_set($hostname) 312 | { 313 | return pinba::hostname_set($hostname); 314 | } 315 | } 316 | 317 | if (!function_exists('pinba_flush')) { 318 | /** 319 | * Useful when you need to send request data to the server immediately (for long-running scripts). 320 | * You can use optional argument script_name to set custom script name. 321 | * 322 | * @param string $script_name 323 | * @param int $flags Possible values (it's a bitmask, so you can add the constants): 324 | * PINBA_FLUSH_ONLY_STOPPED_TIMERS - flush only stopped timers (by default all existing timers are stopped and flushed) 325 | * PINBA_FLUSH_RESET_DATA - reset common request 326 | */ 327 | function pinba_flush($script_name = null, $flags = 0) 328 | { 329 | return pinba::flush($script_name, $flags); 330 | } 331 | } 332 | 333 | if (!function_exists('pinba_get_data')) { 334 | /** 335 | * @param int $flags 336 | * @return string 337 | */ 338 | function pinba_get_data($flags = 0) 339 | { 340 | return pinba::get_data($flags); 341 | } 342 | 343 | } 344 | 345 | if (!function_exists('pinba_reset')) { 346 | /** 347 | * Reset common request data. 348 | * NB: unlike the php extension, this version also deletes all timers. 349 | * @return void 350 | */ 351 | function pinba_reset() 352 | { 353 | pinba::reset(); 354 | } 355 | 356 | } 357 | 358 | if (!defined('PINBA_FLUSH_ONLY_STOPPED_TIMERS')) { 359 | define('PINBA_FLUSH_ONLY_STOPPED_TIMERS', Pinba::FLUSH_ONLY_STOPPED_TIMERS); 360 | } 361 | 362 | if (!defined('PINBA_FLUSH_RESET_DATA')) { 363 | define('PINBA_FLUSH_RESET_DATA', Pinba::FLUSH_RESET_DATA); 364 | } 365 | 366 | if (!defined('PINBA_ONLY_RUNNING_TIMERS')) { 367 | define('PINBA_ONLY_RUNNING_TIMERS', Pinba::ONLY_RUNNING_TIMERS); 368 | } 369 | 370 | if (!defined('PINBA_AUTO_FLUSH')) { 371 | define('PINBA_AUTO_FLUSH', Pinba::AUTO_FLUSH); 372 | } 373 | 374 | if (!defined('PINBA_ONLY_STOPPED_TIMERS')) { 375 | define('PINBA_ONLY_STOPPED_TIMERS', Pinba::ONLY_STOPPED_TIMERS); 376 | } 377 | 378 | if (!class_exists('PinbaClient')) { 379 | class_alias('PinbaPhp\Polyfill\PinbaClient', 'PinbaClient'); 380 | } 381 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gggeek/polyfill-pinba", 3 | "description": "A pure-php implementation of the API exposed by the native Pinba extension", 4 | "license": "GPL-2.0", 5 | "provide": { 6 | "ext-pinba": "*" 7 | }, 8 | "require": { 9 | "php": "^5.3.0 || ^7.0 || ^8.0" 10 | }, 11 | "require-dev": { 12 | "ext-mysqli": "*", 13 | "phpunit/phpunit": "^4.8 || ^5.7 || ^8.5.31", 14 | "phpunit/phpunit-selenium": "*", 15 | "yoast/phpunit-polyfills": "*" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "PinbaPhp\\Polyfill\\": "src/" 20 | }, 21 | "files": [ 22 | "bootstrap.php" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /doc/measure_body_size.php: -------------------------------------------------------------------------------- 1 | setRequestTime($startTime); 22 | 23 | // Do some stuff here 24 | // ... 25 | echo "hello world"; 26 | // ... 27 | // Finally: 28 | 29 | // Display the page's contents to the end user, while measuring its length 30 | $size = ob_get_length(); 31 | ob_end_flush(); 32 | 33 | $pc->setDocumentSize($size); 34 | -------------------------------------------------------------------------------- /doc/sample.php: -------------------------------------------------------------------------------- 1 | "mysql", "server"=>"db1", "operation"=>"select")); 21 | $result = mysqli_query("SELECT ... FROM ... WHERE ...", $connection); 22 | pinba_timer_stop($t); 23 | 24 | // Do some more stuff 25 | // ... 26 | // Finally: that's all folks! 27 | 28 | // Memory usage, execution time, timers info etc. will be automatically collected and at the end of the execution 29 | // of this page will be flushed to the Pinba server via an udp network packet, provided you have set `pinba.enabled` and 30 | // `pinba.server` in php.ini. 31 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | ./ 12 | 13 | ./doc 14 | ./github 15 | ./tests 16 | ./vendor 17 | 18 | 19 | 20 | 21 | 22 | 23 | ./tests 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Pinba.php: -------------------------------------------------------------------------------- 1 | array("hostname", Prtbfr::TYPE_STRING), // bytes for pinba2 15 | 2 => array("server_name", Prtbfr::TYPE_STRING), // bytes for pinba2 16 | 3 => array("script_name", Prtbfr::TYPE_STRING), // bytes for pinba2 17 | 4 => array("request_count", Prtbfr::TYPE_UINT32), 18 | 5 => array("document_size", Prtbfr::TYPE_UINT32), 19 | 6 => array("memory_peak", Prtbfr::TYPE_UINT32), 20 | 7 => array("request_time", Prtbfr::TYPE_FLOAT), 21 | 8 => array("ru_utime", Prtbfr::TYPE_FLOAT), 22 | 9 => array("ru_stime", Prtbfr::TYPE_FLOAT), 23 | 10 => array("timer_hit_count", Prtbfr::TYPE_UINT32, Prtbfr::ELEMENT_REPEATED), 24 | 11 => array("timer_value", Prtbfr::TYPE_FLOAT, Prtbfr::ELEMENT_REPEATED), 25 | 12 => array("timer_tag_count", Prtbfr::TYPE_UINT32, Prtbfr::ELEMENT_REPEATED), 26 | 13 => array("timer_tag_name", Prtbfr::TYPE_UINT32, Prtbfr::ELEMENT_REPEATED), 27 | 14 => array("timer_tag_value", Prtbfr::TYPE_UINT32, Prtbfr::ELEMENT_REPEATED), 28 | 15 => array("dictionary", Prtbfr::TYPE_STRING, Prtbfr::ELEMENT_REPEATED), // bytes for pinba2 29 | 16 => array("status", Prtbfr::TYPE_UINT32, Prtbfr::ELEMENT_OPTIONAL), 30 | 17 => array("memory_footprint", Prtbfr::TYPE_UINT32, Prtbfr::ELEMENT_OPTIONAL), 31 | 18 => array("requests", Prtbfr::TYPE_REQUEST, Prtbfr::ELEMENT_REPEATED), 32 | 19 => array("schema", Prtbfr::TYPE_STRING, Prtbfr::ELEMENT_OPTIONAL), // bytes for pinba2 33 | 20 => array("tag_name", Prtbfr::TYPE_UINT32, Prtbfr::ELEMENT_REPEATED), 34 | 21 => array("tag_value", Prtbfr::TYPE_UINT32, Prtbfr::ELEMENT_REPEATED), 35 | 22 => array("timer_ru_utime", Prtbfr::TYPE_FLOAT, Prtbfr::ELEMENT_REPEATED), 36 | 23 => array("timer_ru_stime", Prtbfr::TYPE_FLOAT, Prtbfr::ELEMENT_REPEATED), 37 | ); 38 | 39 | protected $timers = array(); 40 | protected $script_name = null; 41 | protected $server_name = null; 42 | protected $hostname = null; 43 | // Note: we initialize this to '' instead of NULL because we found out during testing the pinba server, when it does 44 | // receive a packet with no schema member set, reuses the last 'schema' value received in a previous packet. 45 | // This way we force the packets sent to always have the 'schema' field set, by default to a zero-length string. 46 | // That in turn makes the pinba engine store the value '' in the db. 47 | // This behaviour makes testing more deterministic, and honestly looks more like a bugfix than anything else 48 | protected $schema = ''; 49 | protected $request_time = null; 50 | protected $request_count = 0; 51 | protected $memory_footprint = null; 52 | protected $memory_peak = null; 53 | protected $document_size = null; 54 | protected $status = null; 55 | protected $rusage = array(); 56 | protected $tags = array(); 57 | 58 | protected static $options = array(); 59 | protected static $inhibited = null; 60 | 61 | /// Make this class "abstract", in a way that subclasses can still instantiate it, but no one else can 62 | protected function __construct() { 63 | } 64 | 65 | // *** public API (exposed by PinbaClient) *** 66 | 67 | public function setHostname($hostname) 68 | { 69 | $this->hostname = $hostname; 70 | return true; 71 | } 72 | 73 | public function setServername($server_name) 74 | { 75 | $this->server_name = $server_name; 76 | return true; 77 | } 78 | 79 | public function setScriptname($script_name) 80 | { 81 | $this->script_name = $script_name; 82 | return true; 83 | } 84 | 85 | public function setSchema($schema) 86 | { 87 | $this->schema = $schema; 88 | return true; 89 | } 90 | 91 | public function setRequestTime($request_time) 92 | { 93 | if ($request_time < 0) { 94 | trigger_error("negative time value passed ($request_time), changing it to 0", E_USER_WARNING); 95 | $request_time = 0; 96 | } 97 | 98 | $this->request_time = $request_time; 99 | return true; 100 | } 101 | 102 | // *** methods for subclass use *** 103 | 104 | protected function deleteTimers($flags) 105 | { 106 | foreach ($this->timers as &$timer) { 107 | if (($flags & self::ONLY_STOPPED_TIMERS) && $timer["started"]) { 108 | continue; 109 | } 110 | $timer['deleted'] = true; 111 | } 112 | } 113 | 114 | protected function stopTimers($time) 115 | { 116 | foreach ($this->timers as &$timer) { 117 | if ($timer["started"] && !$timer['deleted']) { 118 | $timer["started"] = false; 119 | $timer["value"] = $time - $timer["value"]; 120 | } 121 | } 122 | return true; 123 | } 124 | 125 | /** 126 | * NB: works for deleted timers too 127 | * @param int $timer 128 | * @param float $time 129 | * @param bool $removeHitCount 130 | * @return false|array 131 | */ 132 | protected function getTimerInfo($timer, $time, $removeHitCount = true) 133 | { 134 | if (isset($this->timers[$timer])) { 135 | $timer = $this->timers[$timer]; 136 | if ($timer["started"]) { 137 | $timer["value"] = $time - $timer["value"]; 138 | } 139 | unset($timer['deleted']); 140 | if ($removeHitCount) { 141 | unset($timer['hit_count']); 142 | } 143 | return $timer; 144 | } 145 | 146 | trigger_error("pinba_timer_get_info(): supplied resource is not a valid pinba timer resource", E_USER_WARNING); 147 | return false; 148 | } 149 | 150 | /** 151 | * Checks that tags are fit for usage 152 | * @param array $tags 153 | * @return bool 154 | */ 155 | protected static function verifyTags($tags) 156 | { 157 | if (!$tags) { 158 | trigger_error("tags array cannot be empty", E_USER_WARNING); 159 | return false; 160 | } 161 | foreach ($tags as $key => $val) { 162 | if (is_object($val) || is_array($val) || is_resource($val)) { 163 | trigger_error("tags cannot have non-scalar values", E_USER_WARNING); 164 | return false; 165 | } 166 | if (is_int($key)) { 167 | trigger_error("tags can only have string names (i.e. tags array cannot contain numeric indexes)", E_USER_WARNING); 168 | return false; 169 | } 170 | } 171 | return true; 172 | } 173 | 174 | protected function getInfo($removeHitCount = true, $flags = 0) 175 | { 176 | $time = microtime(true); 177 | 178 | if ($this->rusage) { 179 | $ruUtime = reset($this->rusage); 180 | $ruStime = end($this->rusage); 181 | } else { 182 | $ruUtime = 0; 183 | $ruStime = 0; 184 | if (function_exists('getrusage')) { 185 | $rUsage = getrusage(); 186 | if (isset($rUsage['ru_utime.tv_usec'])) { 187 | $ruUtime = $rUsage['ru_utime.tv_usec'] / 1000000; 188 | } 189 | if (isset($rUsage['ru_utime.tv_usec'])) { 190 | $ruStime = $rUsage['ru_stime.tv_usec'] / 1000000; 191 | } 192 | } 193 | } 194 | 195 | $timers = array(); 196 | foreach ($this->timers as $id => $t) { 197 | if ($t['deleted'] || (($flags & self::ONLY_STOPPED_TIMERS) && $t['started'])) { 198 | continue; 199 | } 200 | $timers[] = $this->getTimerInfo($id, $time, $removeHitCount);; 201 | } 202 | 203 | $hostname = $this->hostname; 204 | if ($hostname === null) { 205 | if (php_sapi_name() == 'cli') { 206 | $hostname = 'php'; 207 | } else { 208 | $hostname = gethostname(); 209 | } 210 | } 211 | $script_name = $this->script_name; 212 | if ($script_name === null && isset($_SERVER['SCRIPT_NAME'])) { 213 | $script_name = $_SERVER['SCRIPT_NAME']; 214 | } 215 | $server_name = $this->server_name; 216 | if ($server_name === null && isset($_SERVER['SERVER_NAME'])) { 217 | $server_name = $_SERVER['SERVER_NAME']; 218 | } 219 | $document_size = $this->document_size; 220 | /// @todo parse the results of `headers_list()` looking for `Content-Length` 221 | /// Note: that might not work for most scenarios, including simple ones such as using php-fpm and no 222 | /// frontend controllers at all, even if there is a `flush` call executed before we reach here 223 | //if ($document_size === null) { 224 | //} 225 | 226 | return array( 227 | /// @todo in the extension, memory_get_peak_usage is not used when this is called from a PinbaClient 228 | 'mem_peak_usage' => ($this->memory_peak !== null ? $this->memory_peak : memory_get_peak_usage(true)), 229 | 'req_time' => $time - $this->request_time, 230 | 'ru_utime' => $ruUtime, 231 | 'ru_stime' => $ruStime, 232 | 'req_count' => ($this->request_count !== null ? $this->request_count : 0), 233 | 'doc_size' => ($document_size !== null ? $document_size : 0), 234 | 'schema' => $this->schema, 235 | 'server_name' => ($server_name !== null ? $server_name : 'unknown'), 236 | 'script_name' => ($script_name !== null ? $script_name : 'unknown'), 237 | 'hostname' => ($hostname !== null ? $hostname : 'unknown'), 238 | 'timers' => $timers, 239 | 'tags' => $this->tags 240 | ); 241 | } 242 | 243 | /** 244 | * Builds the php array structure to be sent to the pinba server, and encodes it as protobuffer. 245 | * @param null|array $struct Allows injecting the data returned by `getInfo`, after having modified it. NB: data 246 | * from this object will be added to that! 247 | * @return string 248 | */ 249 | protected function getPacket($struct = null) 250 | { 251 | // allow injection of custom starting data 252 | if ($struct === null) { 253 | $struct = $this->getInfo(false); 254 | } 255 | 256 | // massage info into correct format for pinba server 257 | 258 | $status = $this->status; 259 | if ($status === null) { 260 | if (($code = http_response_code()) !== false) { 261 | $status = $code; 262 | } 263 | } 264 | 265 | $struct["status"] = $status; 266 | $struct["memory_footprint"] = $this->memory_footprint; 267 | 268 | foreach (array( 269 | "mem_peak_usage" => "memory_peak", 270 | "req_time" => "request_time", 271 | "req_count" => "request_count", 272 | "doc_size" => "document_size") as $old => $new) { 273 | $struct[$new] = $struct[$old]; 274 | } 275 | 276 | // merge timers by tags (replacing them within $struct) 277 | $timersByTags = array(); 278 | foreach ($struct["timers"] as $id => $timer) { 279 | $ttags = $timer["tags"]; 280 | ksort($ttags); 281 | $tagHash = md5(var_export($ttags, true)); 282 | if (isset($timersByTags[$tagHash])) { 283 | $originalId = $timersByTags[$tagHash]; 284 | $struct["timers"][$originalId]["value"] = $struct["timers"][$originalId]["value"] + $timer["value"]; 285 | $struct["timers"][$originalId]["hit_count"] = $struct["timers"][$originalId]["hit_count"] + $timer["hit_count"]; 286 | unset($struct["timers"][$id]); 287 | } else { 288 | $timersByTags[$tagHash] = $id; 289 | } 290 | } 291 | 292 | // build tag dictionary and add to timers and tags the dictionary ids 293 | $dict = array(); 294 | foreach ($struct['timers'] as $id => $timer) { 295 | foreach ($timer['tags'] as $tag => $value) { 296 | if (($tagId = array_search($tag, $dict)) === false) { 297 | $tagId = count($dict); 298 | $dict[] = $tag; 299 | } 300 | if (($valueid = array_search($value, $dict)) === false) { 301 | $valueid = count($dict); 302 | $dict[] = $value; 303 | } 304 | $struct['timers'][$id]['tagids'][$tagId] = $valueid; 305 | } 306 | } 307 | $tagIds = array(); 308 | foreach ($struct["tags"] as $tag => $value) { 309 | if (($tagId = array_search($tag, $dict)) === false) { 310 | $tagId = count($dict); 311 | $dict[] = $tag; 312 | } 313 | if (($valueid = array_search($value, $dict)) === false) { 314 | $valueid = count($dict); 315 | $dict[] = $value; 316 | } 317 | $tagIds[$tagId] = $valueid; 318 | } 319 | 320 | $struct["timer_hit_count"] = array(); 321 | $struct["timer_value"] = array(); 322 | $struct["timer_tag_count"] = array(); 323 | $struct["timer_tag_name"] = array(); 324 | $struct["timer_tag_value"] = array(); 325 | foreach ($struct["timers"] as $timer) { 326 | $struct["timer_hit_count"][] = $timer["hit_count"]; 327 | $struct["timer_value"][] = $timer["value"]; 328 | $struct["timer_tag_count"][] = count($timer["tagids"]); 329 | foreach ($timer["tagids"] as $key => $val) { 330 | $struct["timer_tag_name"][] = $key; 331 | $struct["timer_tag_value"][] = $val; 332 | } 333 | } 334 | 335 | $struct["dictionary"] = array(); 336 | foreach ($dict as $word) { 337 | $struct["dictionary"][] = $word; 338 | } 339 | 340 | $struct["tag_name"] = array(); 341 | $struct["tag_value"] = array(); 342 | foreach ($tagIds as $name => $value) { 343 | $struct["tag_name"][] = $name; 344 | $struct["tag_value"][] = $value; 345 | } 346 | 347 | /// @todo implement the missing fields below 348 | 349 | $struct["requests"] = array(); /// @todo 350 | $struct["timer_ru_utime"] = array(); /// @todo 351 | $struct["timer_ru_stime"] = array(); /// @todo 352 | 353 | return Prtbfr::encode($struct, self::$message_proto); 354 | } 355 | 356 | /** 357 | * @param string $server see https://github.com/tony2001/pinba_engine/wiki/PHP-extension#pinbaserver for the supportde syntax 358 | * @param string $message 359 | * @return bool 360 | */ 361 | protected static function _send($server, $message) 362 | { 363 | $port = 30002; 364 | if (preg_match('/^\\[(.+)\\]:([0-9]+)$/', $server, $matches)) { 365 | $server = $matches[1]; 366 | $port = (int)$matches[2]; 367 | } else { 368 | if (count($parts = explode(':', $server)) == 2) { 369 | // IPv4 with port 370 | $port = (int)$parts[1]; 371 | $server = $parts[0]; 372 | } 373 | } 374 | 375 | /// @todo should we log a more specific warning in case of failures to open the udp socket? f.e. the pinba 376 | /// extension on invalid hostname triggers: 377 | /// PHP Warning: Unknown: failed to resolve Pinba server hostname 'xxx': Name or service not known in Unknown on line 0 378 | $fp = fsockopen("udp://$server", $port, $errno, $errstr); 379 | if ($fp) { 380 | $msgLen = strlen($message); 381 | $len = fwrite($fp, $message, $msgLen); 382 | fclose($fp); 383 | 384 | if ($len < $msgLen) { 385 | trigger_error("failed to send data to Pinba server", E_USER_WARNING); 386 | } 387 | 388 | return $msgLen == $len; 389 | } 390 | 391 | return false; 392 | } 393 | 394 | protected static function isInhibited() 395 | { 396 | if (self::$inhibited === null) { 397 | self::$inhibited = (bool)self::ini_get('pinba.inhibited'); 398 | } 399 | 400 | return self::$inhibited; 401 | } 402 | 403 | /** 404 | * Sadly it is not possible to set in php code values for 'pinba.enabled', at least when the pinba extension is 405 | * not on board. When using `php -d pinba.enabled=1` or values in php.ini, `ini_get` will also not work, whereas 406 | * `get_cfg_var` will. 407 | * We try to make life easy for the users of the polyfill as well as for the test code by allowing usage of values 408 | * set both in php.ini and in php code 409 | * @param string $option 410 | * @return string|false 411 | * @see Pinba::ini_set() 412 | */ 413 | public static function ini_get($option) 414 | { 415 | if (array_key_exists($option, self::$options)) { 416 | return self::$options[$option]; 417 | } 418 | 419 | $val = ini_get($option); 420 | if ($val === false) { 421 | $val = get_cfg_var($option); 422 | } 423 | 424 | return $val; 425 | } 426 | 427 | /** 428 | * Allow to set config values specific to pinba, without messing with php.ini stuff. 429 | * @param string $option 430 | * @param string|int|float|bool|null $value 431 | * @return string|null 432 | */ 433 | public static function ini_set($option, $value) 434 | { 435 | if (array_key_exists($option, self::$options)) { 436 | $oldValue = self::$options[$option]; 437 | } else { 438 | // we do not return false, as that would indicate a failure ;-) 439 | $oldValue = null; 440 | } 441 | self::$options[$option] = (string)$value; 442 | return $oldValue; 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /src/PinbaClient.php: -------------------------------------------------------------------------------- 1 | servers = $servers; 33 | $this->flags = $flags; 34 | } 35 | 36 | public function __destruct() 37 | { 38 | if ($this->flags & pinba::AUTO_FLUSH && ! $this->dataSent) { 39 | $this->send(); 40 | } 41 | } 42 | 43 | public function setRequestCount($request_count) 44 | { 45 | $this->request_count = $request_count; 46 | return true; 47 | } 48 | 49 | public function setMemoryFootprint($memory_footprint) 50 | { 51 | $this->memory_footprint = $memory_footprint; 52 | return true; 53 | } 54 | 55 | public function setMemoryPeak($memory_peak) 56 | { 57 | $this->memory_peak = $memory_peak; 58 | return true; 59 | } 60 | 61 | public function setDocumentSize($document_size) 62 | { 63 | $this->document_size = $document_size; 64 | return true; 65 | } 66 | 67 | public function setStatus($status) 68 | { 69 | $this->status = $status; 70 | return true; 71 | } 72 | 73 | public function setRusage($rusage) 74 | { 75 | if (count($rusage) !== 2) { 76 | trigger_error("rusage array must contain exactly 2 elements", E_USER_WARNING); 77 | return false; 78 | } 79 | 80 | $this->rusage = $rusage; 81 | return true; 82 | } 83 | 84 | public function setTag($name, $value) 85 | { 86 | if (self::$inhibited === true || self::isInhibited()) { 87 | return false; 88 | } 89 | 90 | $this->tags[$name] = (string)$value; 91 | return true; 92 | } 93 | 94 | public function setTimer($tags, $value, $rusage = array(), $hit_count = 1) 95 | { 96 | return $this->upsertTimer(false, $tags, $value, $rusage, $hit_count); 97 | } 98 | 99 | public function addTimer($tags, $value, $rusage = array(), $hit_count = 1) 100 | { 101 | return $this->upsertTimer(true, $tags, $value, $rusage, $hit_count); 102 | } 103 | 104 | protected function upsertTimer($add, $tags, $value, $rusage = array(), $hit_count = 1) 105 | { 106 | if (self::$inhibited === true || self::isInhibited()) { 107 | return ''; 108 | } 109 | 110 | if (!is_array($tags)) { 111 | trigger_error("setTimer() expects parameter 1 to be array, " . gettype($tags) . " given", E_USER_WARNING); 112 | return false; 113 | } 114 | if (!self::verifyTags($tags)) 115 | { 116 | return false; 117 | } 118 | if ($hit_count <= 0) { 119 | trigger_error("hit_count must be greater than 0 ($hit_count was passed)", E_USER_WARNING); 120 | return false; 121 | } 122 | if ($value < 0) { 123 | trigger_error("negative time value passed ($value), changing it to 0", E_USER_WARNING); 124 | $value = 0; 125 | } 126 | 127 | $tagsHash = $tags; 128 | ksort($tagsHash); 129 | $tagsHash = md5(var_export($tagsHash, true)); 130 | 131 | if ($add && isset($this->timers[$tagsHash])) { 132 | // no need to update 'tags' (same value) nor 'started' (client timers are always stopped) 133 | /// @todo what about 'deleted' ? 134 | $this->timers[$tagsHash]['value'] = $this->timers[$tagsHash]['value'] + $value; 135 | $this->timers[$tagsHash]['hit_count'] = $this->timers[$tagsHash]['hit_count'] + $hit_count; 136 | } else { 137 | $this->timers[$tagsHash] = array( 138 | "value" => $value, 139 | "tags" => $tags, 140 | "started" => false, 141 | "data" => null, 142 | "hit_count" => $hit_count, 143 | "deleted" => false 144 | ); 145 | } 146 | 147 | return $tagsHash; 148 | } 149 | 150 | /** 151 | * @param null|int $flags - optional flags, bitmask. Override object flags if specified. NB: 0 != null 152 | * @return bool 153 | */ 154 | public function send($flags = null) 155 | { 156 | if (self::$inhibited === true || self::isInhibited()) { 157 | return ''; 158 | } 159 | 160 | if (!count($this->servers)) { 161 | return false; 162 | } 163 | 164 | $message = $this->getData($flags); 165 | 166 | $out = true; 167 | foreach($this->servers as $server) { 168 | $out = $out & self::_send($server, $message); 169 | } 170 | 171 | $this->dataSent = true; 172 | 173 | return $out; 174 | } 175 | 176 | /** 177 | * Returns raw packet data. This is basically a copy of send(), but instead of sending it just returns the data. 178 | * @param null|int $flags - optional flags, bitmask. Override object flags if specified. NB: 0 != null 179 | * @return string 180 | */ 181 | public function getData($flags = null) 182 | { 183 | if ($flags === null) { 184 | $flags = $this->flags; 185 | } 186 | 187 | if (!($flags & self::FLUSH_ONLY_STOPPED_TIMERS)) { 188 | $this->stopTimers(microtime(true)); 189 | } 190 | 191 | $info = $this->getInfo(false, $flags); 192 | 193 | return $this->getPacket($info); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/PinbaFunctions.php: -------------------------------------------------------------------------------- 1 | "value". Cannot contain numeric indexes for obvious reasons. 23 | * @param array $data optional array with user data, not sent to the server. 24 | * @return int|false Always returns new timer resource, except on failure. 25 | * 26 | * @todo support $hit_count 27 | */ 28 | public static function timer_start($tags, $data = null, $hit_count = 1) 29 | { 30 | // we return 0 to make it possible to later call other `timer_` functions on this without warnings 31 | if (self::$inhibited === true || self::isInhibited()) { 32 | return 0; 33 | } 34 | 35 | if (!is_array($tags)) { 36 | trigger_error("pinba_timer_start() expects parameter 1 to be array, " . gettype($tags) . " given", E_USER_WARNING); 37 | return false; 38 | } 39 | if ($data !== null && !is_array($data)) { 40 | trigger_error("pinba_timer_start() expects parameter 2 to be array, " . gettype($data) . " given", E_USER_WARNING); 41 | return false; 42 | } 43 | if (!self::verifyTags($tags)) { 44 | return false; 45 | } 46 | if ($hit_count <= 0) { 47 | trigger_error("hit_count must be greater than 0 ($hit_count was passed)", E_USER_WARNING); 48 | return false; 49 | } 50 | 51 | $i = self::instance(); 52 | $timer = count($i->timers); 53 | $i->timers[$timer] = array( 54 | "value" => microtime(true), 55 | "tags" => $tags, 56 | "started" => true, 57 | "data" => $data, 58 | "hit_count" => $hit_count, 59 | "deleted" => false, 60 | ); 61 | return $timer; 62 | } 63 | 64 | /** 65 | * Stops the timer. 66 | * 67 | * @param int $timer valid timer resource. 68 | * @return bool Returns true on success and false on failure (if the timer has already been stopped). 69 | */ 70 | public static function timer_stop($timer) 71 | { 72 | if (self::$inhibited === true || self::isInhibited()) { 73 | return false; 74 | } 75 | 76 | if (!is_int($timer)) { 77 | trigger_error("pinba_timer_stop() expects parameter 1 to be int, " . gettype($timer) . " given", E_USER_WARNING); 78 | return false; 79 | } 80 | $time = microtime(true); 81 | $i = self::instance(); 82 | if (isset($i->timers[$timer])) { 83 | if ($i->timers[$timer]["started"]) { 84 | $i->timers[$timer]["started"] = false; 85 | $i->timers[$timer]["value"] = $time - $i->timers[$timer]["value"]; 86 | return true; 87 | } else { 88 | trigger_error("timer is already stopped", E_USER_WARNING); 89 | } 90 | } 91 | return false; 92 | } 93 | 94 | /** 95 | * Creates new timer. This timer is already stopped and has specified time value. 96 | * 97 | * @param array $tags an array of tags and their values in the form of "tag" => "value". Cannot contain numeric indexes for obvious reasons. 98 | * @param int $value timer value for new timer. 99 | * @param array $data optional array with user data, not sent to the server. 100 | * @param int $hit_count 101 | * @return int|false Always returns new timer resource, except on failure. 102 | */ 103 | public static function timer_add($tags, $value, $data = null, $hit_count = 1) 104 | { 105 | // we return 0 to make it possible to later call other `timer_` functions on this without warnings 106 | if (self::$inhibited === true || self::isInhibited()) { 107 | return 0; 108 | } 109 | 110 | if (!is_array($tags)) { 111 | trigger_error("pinba_timer_add() expects parameter 1 to be array, " . gettype($tags) . " given", E_USER_WARNING); 112 | return false; 113 | } 114 | if ($data !== null && !is_array($data)) { 115 | trigger_error("pinba_timer_add() expects parameter 3 to be array, " . gettype($data) . " given", E_USER_WARNING); 116 | return false; 117 | } 118 | if (!self::verifyTags($tags)) { 119 | return false; 120 | } 121 | if ($hit_count <= 0) { 122 | trigger_error("hit_count must be greater than 0 ($hit_count was passed)", E_USER_WARNING); 123 | return false; 124 | } 125 | if ($value < 0) { 126 | trigger_error("negative time value passed ($value), changing it to 0", E_USER_WARNING); 127 | $value = 0; 128 | } 129 | 130 | $i = self::instance(); 131 | $timer = count($i->timers); 132 | $i->timers[$timer] = array( 133 | "value" => $value, 134 | "tags" => $tags, 135 | "started" => false, 136 | "data" => $data, 137 | "deleted" => false, 138 | "hit_count" => $hit_count 139 | ); 140 | return $timer; 141 | } 142 | 143 | /** 144 | * Deletes the timer. 145 | * 146 | * @param int $timer valid timer resource. 147 | * @return bool Returns true on success and false on failure. 148 | */ 149 | public static function timer_delete($timer) 150 | { 151 | $i = self::instance(); 152 | if (isset($i->timers[$timer])) { 153 | unset($i->timers[$timer]); 154 | return true; 155 | } 156 | return false; 157 | } 158 | 159 | /** 160 | * Merges $tags array with the timer tags replacing existing elements. 161 | * 162 | * @param int $timer - valid timer resource 163 | * @param array $tags - an array of tags. 164 | * @return bool 165 | */ 166 | public static function timer_tags_merge($timer, $tags) 167 | { 168 | /// @todo should we check for type of $tags? 169 | 170 | $i = self::instance(); 171 | if (isset($i->timers[$timer])) { 172 | $i->timers[$timer]["tags"] = array_merge($i->timers[$timer]["tags"], $tags); 173 | return true; 174 | } 175 | return false; 176 | } 177 | 178 | /** 179 | * Replaces timer tags with the passed $tags array. 180 | * 181 | * @param int $timer - valid timer resource 182 | * @param array $tags - an array of tags. 183 | * @return bool 184 | */ 185 | public static function timer_tags_replace($timer, $tags) 186 | { 187 | /// @todo should we check for type of $tags? 188 | 189 | // NB: strangely enough, the extension returns true if the tags array is empty... 190 | if (!self::verifyTags($tags)) { 191 | return false; 192 | } 193 | 194 | $i = self::instance(); 195 | if (isset($i->timers[$timer])) { 196 | $i->timers[$timer]["tags"] = $tags; 197 | return true; 198 | } 199 | 200 | /// @todo log warning 201 | return false; 202 | } 203 | 204 | /** 205 | * Merges $data array with the timer user data replacing existing elements. 206 | * 207 | * @param int $timer valid timer resource 208 | * @param array $data an array of user data. 209 | * @return bool Returns true on success and false on failure. 210 | */ 211 | public static function timer_data_merge($timer, $data) 212 | { 213 | /// @todo should we check for type of $data? 214 | 215 | $i = self::instance(); 216 | if (isset($i->timers[$timer])) { 217 | $i->timers[$timer]["data"] = array_merge($i->timers[$timer]["data"], $data); 218 | return true; 219 | } 220 | return false; 221 | } 222 | 223 | /** 224 | * Replaces timer user data with the passed $data array. 225 | * Use NULL value to reset user data in the timer. 226 | * 227 | * @param int $timer valid timer resource 228 | * @param array $data an array of user data. 229 | * @return bool Returns true on success and false on failure. 230 | */ 231 | public static function timer_data_replace($timer, $data) 232 | { 233 | /// @todo should we check for type of $data? 234 | 235 | $i = self::instance(); 236 | if (isset($i->timers[$timer])) { 237 | $i->timers[$timer]["data"] = $data; 238 | return true; 239 | } 240 | return false; 241 | } 242 | 243 | /** 244 | * Returns timer data. 245 | * 246 | * @param int $timer - valid timer resource. 247 | * @return array|false Output example: 248 | * array(4) { 249 | * ["value"]=> float(0.0213) 250 | * ["tags"]=> array(1) { 251 | * ["foo"]=> string(3) "bar" 252 | * } 253 | * ["started"]=> bool(true) 254 | * ["data"]=> NULL 255 | * } 256 | */ 257 | public static function timer_get_info($timer) 258 | { 259 | $time = microtime(true); 260 | return self::instance()->getTimerInfo($timer, $time); 261 | } 262 | 263 | /** 264 | * Stops all running timers. 265 | * 266 | * @return bool 267 | */ 268 | public static function timers_stop() 269 | { 270 | $time = microtime(true); 271 | return self::instance()->stopTimers($time); 272 | } 273 | 274 | /** 275 | * Get all timers' ids. 276 | * 277 | * @param int $flag 278 | * @return int[]|resource[] 279 | */ 280 | public static function timers_get($flag = 0) 281 | { 282 | $out = array(); 283 | $i = self::instance(); 284 | foreach($i->timers as $id => $t) { 285 | if ($t['deleted'] || (($flag & self::ONLY_STOPPED_TIMERS) && $t['started'])) { 286 | continue; 287 | } 288 | $out[] = $id; 289 | } 290 | return $out; 291 | } 292 | 293 | /** 294 | * Returns all request data (including timers user data). 295 | * 296 | * @return array Example: 297 | * array(9) { 298 | * ["mem_peak_usage"]=> int(786432) 299 | * ["req_time"]=> float(0.001529) 300 | * ["ru_utime"]=> float(0) 301 | * ["ru_stime"]=> float(0) 302 | * ["req_count"]=> int(1) 303 | * ["doc_size"]=> int(0) 304 | * ["server_name"]=> string(7) "unknown" 305 | * ["script_name"]=> string(1) "-" 306 | * ["hostname"]=> string(3) "php" 307 | * ["timers"]=> array(1) { 308 | * [0]=> array(4) { 309 | * ["value"]=> float(4.5E-5) 310 | * ["tags"]=> array(1) { 311 | * ["foo"]=> string(3) "bar" 312 | * } 313 | * ["started"]=> bool(true) 314 | * ["data"]=> NULL 315 | * } 316 | * } 317 | * ["tags"] => array(0) { 318 | * } 319 | * } 320 | */ 321 | public static function get_info() 322 | { 323 | $struct = self::instance()->getInfo(); 324 | // weird hack, copied from pinba extension! We send 0 by default to pinba, but display 1... 325 | $struct["req_count"] = $struct["req_count"] + 1; 326 | return $struct; 327 | } 328 | 329 | /** 330 | * Set request schema (HTTP/HTTPS/whatever). 331 | * 332 | * @param string $schema 333 | * @return bool 334 | */ 335 | public static function schema_set($schema) 336 | { 337 | return self::instance()->setSchema($schema); 338 | } 339 | 340 | /** 341 | * Set custom script name instead of $_SERVER['SCRIPT_NAME'] used by default. 342 | * Useful for those using front controllers, when all requests are served by one PHP script. 343 | * 344 | * @param string $script_name 345 | * @return bool 346 | */ 347 | public static function script_name_set($script_name) 348 | { 349 | return self::instance()->setScriptname($script_name); 350 | } 351 | 352 | /** 353 | * Set custom server name instead of $_SERVER['SERVER_NAME'] used by default. 354 | * 355 | * @param string $server_name 356 | * @return bool 357 | */ 358 | public static function server_name_set($server_name) 359 | { 360 | return self::instance()->setServername($server_name); 361 | } 362 | 363 | /** 364 | * Set custom Set custom request time. 365 | * 366 | * @param float $request_time 367 | * @return bool 368 | */ 369 | public static function request_time_set($request_time) 370 | { 371 | return self::instance()->setRequestTime($request_time); 372 | } 373 | 374 | /** 375 | * @param string $tag 376 | * @param string $value 377 | * @return bool 378 | */ 379 | public static function tag_set($tag, $value) 380 | { 381 | if (self::$inhibited === true || self::isInhibited()) { 382 | return false; 383 | } 384 | 385 | if ($value === "") { 386 | trigger_error("tag name cannot be empty", E_USER_WARNING); 387 | return false; 388 | } 389 | self::instance()->tags[$tag] = (string)$value; 390 | return true; 391 | } 392 | 393 | /** 394 | * @param string $tag 395 | * @return string|false 396 | */ 397 | public static function tag_get($tag) 398 | { 399 | return isset(self::instance()->tags[$tag]) ? self::instance()->tags[$tag] : false; 400 | } 401 | 402 | /** 403 | * @param string $tag 404 | * @return bool 405 | */ 406 | public static function tag_delete($tag) 407 | { 408 | if (array_key_exists($tag, self::instance()->tags)) { 409 | unset(self::instance()->tags[$tag]); 410 | return true; 411 | } 412 | return false; 413 | } 414 | 415 | /** 416 | * @return array 417 | */ 418 | public static function tags_get() 419 | { 420 | return self::instance()->tags; 421 | } 422 | 423 | /** 424 | * Set custom hostname instead of the result of gethostname() used by default. 425 | * 426 | * @param string $hostname 427 | * @return bool 428 | */ 429 | public static function hostname_set($hostname) 430 | { 431 | return self::instance()->setHostname($hostname); 432 | } 433 | 434 | /** 435 | * Useful when you need to send request data to the server immediately (for long-running scripts). 436 | * You can use optional argument script_name to set custom script name. 437 | * 438 | * @param string $script_name 439 | * @param int $flags Possible values (it's a bitmask, so you can add the constants): 440 | * FLUSH_ONLY_STOPPED_TIMERS - flush only stopped timers (by default all existing timers are stopped and flushed) 441 | * FLUSH_RESET_DATA - reset common request 442 | * NB: we stop timers even if the socket can not be opened. But not if 'pinba.enabled' is false 443 | * @return bool false if extension is disabled, or if there are network issues 444 | */ 445 | public static function flush($script_name = null, $flags = 0) 446 | { 447 | if (self::$inhibited === true || self::isInhibited()) { 448 | return false; 449 | } 450 | 451 | $i = self::instance(); 452 | 453 | // replicate behaviour of php ext: stop running timers even if pinba.enabled is set to false 454 | if (!($flags & self::FLUSH_ONLY_STOPPED_TIMERS)) { 455 | $i->stopTimers(microtime(true)); 456 | } 457 | 458 | if (!self::ini_get('pinba.enabled')) { 459 | // delete stopped timers, even though we will not be sending the data via udp 460 | $i->deleteTimers($flags); 461 | return false; 462 | } 463 | 464 | $info = $i->getInfo(false, $flags); 465 | 466 | if ($script_name != null) { 467 | $info["script_name"] = $script_name; 468 | } 469 | 470 | $ok = self::_send(self::ini_get('pinba.server'), $i->getPacket($info)); 471 | 472 | $i->deleteTimers($flags); 473 | 474 | if ($flags & self::FLUSH_RESET_DATA) { 475 | self::reset(); 476 | } 477 | 478 | return $ok; 479 | } 480 | 481 | public static function reset() 482 | { 483 | $i = self::instance(); 484 | foreach ($i->timers as &$timer) { 485 | $timer['deleted'] = true; 486 | } 487 | $i->tags = array(); 488 | $i->request_time = microtime(true); 489 | $i->document_size = null; 490 | $i->memory_peak = null; 491 | $i->request_count = 0; 492 | /// @todo double check in extension C code: are these reset to null, or to current rusage? 493 | $i->rusage = array(); 494 | } 495 | 496 | /** 497 | * @param int $flags 498 | * @return string 499 | * @todo add support for self::ONLY_RUNNING_TIMERS (but what if both are set?) 500 | */ 501 | public static function get_data($flags = 0) 502 | { 503 | $i = self::instance(); 504 | 505 | $info = $i->getInfo(false, $flags); 506 | 507 | return $i->getPacket($info); 508 | } 509 | 510 | // *** End of Pinba API *** 511 | 512 | /// Make this class a singleton: private constructor 513 | protected function __construct() 514 | { 515 | } 516 | 517 | /** 518 | * Make this class a singleton: factory 519 | * @return Pinba we can not instantiate a Pinba object, as it has been declared abstract 520 | */ 521 | protected static function instance() 522 | { 523 | if (self::$instance == null) { 524 | self::$instance = new Pinba(); 525 | } 526 | return self::$instance; 527 | } 528 | 529 | public static function autoFlush() 530 | { 531 | $enabled = self::ini_get('pinba.auto_flush'); 532 | // manually tested: all possible "falsey" values of ini settings. When not set at all, we get false instead 533 | if ($enabled !== '' && $enabled !== '0') { 534 | self::flush(); 535 | } 536 | } 537 | 538 | /** 539 | * A function not in the pinba extension api, needed to calculate total req. time and to insure we flush at end of 540 | * script execution. To be called as close as possible to the beginning of the main script. 541 | */ 542 | public static function init($time=null) 543 | { 544 | $i = self::instance(); 545 | if ($i->request_time == null || $time != null) { 546 | if ($time == null) 547 | { 548 | $time = microtime(true); 549 | } 550 | $i->setRequestTime($time); 551 | } 552 | if (!self::$shutdown_registered) { 553 | self::$shutdown_registered = true; 554 | register_shutdown_function('PinbaPhp\Polyfill\PinbaFunctions::autoFlush'); 555 | } 556 | } 557 | } 558 | -------------------------------------------------------------------------------- /src/Prtbfr.php: -------------------------------------------------------------------------------- 1 | array("hostname", prtbfr::TYPE_STRING), 59 | * 10 => array("timer_hit_count", prtbfr::TYPE_INT, prtbfr::REPEATED) 60 | * ) 61 | * 62 | * @todo support encoding objects, not only arrays 63 | */ 64 | public static function encode($struct, $proto) 65 | { 66 | $result = ''; 67 | ksort($proto, SORT_NUMERIC); 68 | foreach ($proto as $pos => $def) 69 | { 70 | $field = $def[0]; 71 | $type = $def[1]; 72 | $cardinality = isset($def[2]) ? $def[2] : self::ELEMENT_REQUIRED; 73 | switch($cardinality) 74 | { 75 | case self::ELEMENT_REPEATED: 76 | /// @todo we should probably allow non existing struct elements to count as empty arrays 77 | foreach($struct[$field] as $value) 78 | { 79 | $result .= self::encode_value($value, $type, $pos); 80 | } 81 | break; 82 | case self::ELEMENT_OPTIONAL: 83 | if (isset($struct[$field]) && $struct[$field] !== null) 84 | { 85 | $result .= self::encode_value($struct[$field], $type, $pos); 86 | } 87 | break; 88 | default: 89 | $result .= self::encode_value($struct[$field], $type, $pos); 90 | } 91 | } 92 | return $result; 93 | } 94 | 95 | protected static function encode_value($value, $type, $position) 96 | { 97 | $wiretype = self::wiretype($type); 98 | $header = self::varint_encode(($position << 3) | $wiretype); 99 | 100 | switch($type) 101 | { 102 | case self::TYPE_INT64: 103 | case self::TYPE_UINT64: 104 | case self::TYPE_INT32: 105 | case self::TYPE_UINT32: 106 | case self::TYPE_BOOL: // casting bools to integers is correct 107 | $value = (integer)$value; 108 | $value = self::varint_encode($value); 109 | break; 110 | case self::TYPE_SINT32: // ZigZag 111 | case self::TYPE_SINT64: // ZigZag 112 | $value = (integer)$value; 113 | $value = ($value >> 1) ^ (-($value & 1)); 114 | $value = self::varint_encode($value); 115 | break; 116 | case self::TYPE_DOUBLE: 117 | $value = self::double_encode($value); 118 | break; 119 | case self::TYPE_FIXED64: 120 | $value = self::fixed64_encode($value); 121 | break; 122 | case self::TYPE_SFIXED64: 123 | $value = self::sfixed64_encode($value); 124 | break; 125 | case self::TYPE_FLOAT: 126 | $value = (float)$value; 127 | $value = self::float_encode($value); 128 | break; 129 | case self::TYPE_FIXED32: 130 | $value = self::fixed32_encode($value); 131 | break; 132 | case self::TYPE_SFIXED32: 133 | $value = self::sfixed32_encode($value); 134 | break; 135 | case self::TYPE_STRING: 136 | case self::TYPE_BYTES: 137 | // handle NUL chars the same way as the C code does 138 | $value = (string)$value; 139 | if (strpos($value, chr(0)) !== false) { 140 | $value = strstr($value, chr(0), true); 141 | } 142 | $value = self::varint_encode(strlen($value)) . $value; 143 | break; 144 | case self::TYPE_ENUM: 145 | $value = self::varint_encode($value); 146 | break; 147 | /// @todo support self::TYPE_REQUEST 148 | default: 149 | throw new Exception("Unknown field type $type"); 150 | } 151 | 152 | return $header . $value; 153 | } 154 | 155 | protected static function varint_encode($value) 156 | { 157 | if ($value < 0) throw new Exception("$value is negative"); 158 | 159 | if ($value < 128) { 160 | return chr($value); 161 | } 162 | 163 | $values = array(); 164 | while ($value !== 0) 165 | { 166 | $values[] = 0x80 | ($value & 0x7f); 167 | $value = $value >> 7; 168 | } 169 | $values[count($values)-1] &= 0x7f; 170 | 171 | $bytes = implode('', array_map('chr', $values)); 172 | return $bytes; 173 | } 174 | 175 | protected static function sfixed32_encode($value) 176 | { 177 | $bytes = pack('l*', $value); 178 | if (self::isBigEndian()) 179 | { 180 | $bytes = strrev($bytes); 181 | } 182 | return substr($bytes, 0, 4); 183 | } 184 | 185 | protected static function fixed32_encode($value) 186 | { 187 | $bytes = pack('N*', $value); 188 | return substr($bytes, 0, 4); 189 | } 190 | 191 | protected static function sfixed64_encode($value) 192 | { 193 | $bytes = pack('V*', $value & 0xffffffff, $value / (0xffffffff+1)); 194 | return substr($bytes, 0, 8); 195 | } 196 | 197 | protected static function fixed64_encode($value) 198 | { 199 | return self::sfixed64_encode($value); 200 | } 201 | 202 | protected static function float_encode($value) 203 | { 204 | $bytes = pack('f*', $value); 205 | if (self::isBigEndian()) 206 | { 207 | $bytes = strrev($bytes); 208 | } 209 | return substr($bytes, 0, 4); 210 | } 211 | 212 | protected static function double_encode($value) 213 | { 214 | $bytes = pack('d*', $value); 215 | if (self::isBigEndian()) 216 | { 217 | $bytes = strrev($bytes); 218 | } 219 | return substr($bytes, 0, 8); 220 | } 221 | 222 | protected static function wiretype($type, $wire=null) 223 | { 224 | switch ($type) 225 | { 226 | case self::TYPE_INT32: 227 | case self::TYPE_INT64: 228 | case self::TYPE_UINT32: 229 | case self::TYPE_UINT64: 230 | case self::TYPE_SINT32: 231 | case self::TYPE_SINT64: 232 | case self::TYPE_BOOL: 233 | case self::TYPE_ENUM: 234 | return self::WIRETYPE_VARINT; 235 | case self::TYPE_FIXED64: 236 | case self::TYPE_SFIXED64: 237 | case self::TYPE_DOUBLE: 238 | return self::WIRETYPE_FIXED64; 239 | case self::TYPE_STRING: 240 | case self::TYPE_BYTES: 241 | case self::TYPE_MESSAGE: 242 | return self::WIRETYPE_LENGTH_DELIMITED; 243 | case self::TYPE_FIXED32: 244 | case self::TYPE_SFIXED32: 245 | case self::TYPE_FLOAT: 246 | return self::WIRETYPE_FIXED32; 247 | default: 248 | // Unknown fields just return the reported wire type 249 | return $wire; 250 | } 251 | } 252 | 253 | protected static function isBigEndian() 254 | { 255 | if (self::$_endianness === NULL) 256 | { 257 | list(,$result) = unpack('L', pack('V', 1)); 258 | if ($result === 1) 259 | { 260 | self::$_endianness = self::LITTLE_ENDIAN; 261 | } 262 | else 263 | { 264 | self::$_endianness = self::BIG_ENDIAN; 265 | } 266 | } 267 | return self::$_endianness === self::BIG_ENDIAN; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /tests/01GatherTest.php: -------------------------------------------------------------------------------- 1 | pReset(); 16 | } 17 | 18 | /** 19 | * @dataProvider listAPIPrefixes 20 | */ 21 | function testGetInfo($prefix) 22 | { 23 | $v = $this->cpf($prefix, 'get_info'); 24 | $this->assertGreaterThan(0, $v['req_time'], 'Request time should be bigger than 0'); 25 | $this->assertGreaterThan(1024000, $v['mem_peak_usage'], 'Used mem time should be bigger than 1MB'); 26 | $this->assertSame('php', $v['hostname'], 'Host name should default to php for cli scripts'); 27 | $this->assertSame('./vendor/bin/phpunit', $v['script_name'], 'Script name should match phpunit runner'); 28 | $this->assertSame('unknown', $v['server_name'], 'Server name should be unknown'); 29 | $this->assertSame(1, $v['req_count'], 'Req count should be 1 for cli scripts'); 30 | } 31 | 32 | /** 33 | * @dataProvider listAPIPrefixes 34 | */ 35 | function testSetInfo($prefix) 36 | { 37 | $this->cpf($prefix, 'hostname_set', 'hello'); 38 | $this->cpf($prefix, 'script_name_set', 'world.php'); 39 | $v = $this->cpf($prefix, 'get_info'); 40 | $this->assertSame('hello', $v['hostname'], 'Host name should match what was set'); 41 | $this->assertSame('world.php', $v['script_name'], 'Script name should match what was set'); 42 | /// @todo reset nostname, script_name, in case they are used by other test later / the same test with another $prefix 43 | } 44 | 45 | /** 46 | * @dataProvider listAPIPrefixes 47 | */ 48 | function testTimer($prefix) 49 | { 50 | $t = $this->cpf($prefix, 'timer_start', array('timer' => 'testTimer', 'tag2' => '10'), array('whatever')); 51 | usleep(10); 52 | $v = $this->cpf($prefix, 'timer_get_info', $t); 53 | usleep(100000); 54 | $r = $this->cpf($prefix, 'timer_stop', $t); 55 | 56 | $this->assertSame(true, $r, 'timer_stop should return true for running timers'); 57 | $this->assertSame(true, $v['started'], 'Timer started should be true before stop'); 58 | $this->assertGreaterThan(0, $v['value'], 'Timer time should be bigger than zero after start'); 59 | $this->assertLessThan(0.01, $v['value'], 'Timer time should be less than 0.01 secs after start'); 60 | 61 | $v = $this->cpf($prefix, 'timer_get_info', $t); 62 | $this->assertGreaterThan(0.1, $v['value'], 'Timer time should be bigger than sleep time'); 63 | ksort($v['tags']); 64 | $this->assertSame(array('tag2' => '10', 'timer' => 'testTimer'), $v['tags'], 'Timer tags should keep injected value'); 65 | $this->assertSame(array('whatever'), $v['data'], 'Timer data should keep injected value'); 66 | $this->assertSame(false, $v['started'], 'Timer started should be false after stop'); 67 | 68 | $v1 = $v['value']; 69 | /// @todo this generates a warning. Move it to its own test, tagged as expect exception 70 | //$r = $this->cpf($prefix, 'timer_stop', $t); 71 | //$this->assertSame(false, $r, 'timer_stop should return false for stopped timers'); 72 | 73 | usleep(100000); 74 | $v = $this->cpf($prefix, 'timer_get_info', $t); 75 | $this->assertSame($v1, $v['value'], 'Timer time should not increase after stop'); 76 | 77 | $v2 = $this->cpf($prefix, 'get_info'); 78 | $this->assertSame($v, $v2['timers'][0], 'get_info should return same timer as timer_get_info'); 79 | 80 | $this->cpf($prefix, 'timer_data_merge', $t, array('x' => 'y')); 81 | $v = $this->cpf($prefix, 'timer_get_info', $t); 82 | $this->assertSame(array('whatever', 'x' => 'y'), $v['data'], 'Timer data should have merged value'); 83 | 84 | $this->cpf($prefix, 'timer_data_replace', $t, array('x' => 'y')); 85 | $v = $this->cpf($prefix, 'timer_get_info', $t); 86 | $this->assertSame(array('x' => 'y'), $v['data'], 'Timer data should have replaced value'); 87 | 88 | $this->cpf($prefix, 'timer_tags_merge', $t, array('x' => 'y')); 89 | $v = $this->cpf($prefix, 'timer_get_info', $t); 90 | ksort($v['tags']); 91 | $this->assertSame(array('tag2' => '10', 'timer' => 'testTimer', 'x' => 'y'), $v['tags'], 'Timer tags should have merged value'); 92 | 93 | $this->cpf($prefix, 'timer_tags_replace', $t, array('x' => 'y')); 94 | $v = $this->cpf($prefix, 'timer_get_info', $t); 95 | $this->assertSame(array('x' => 'y'), $v['tags'], 'Timer tags should have replaced value'); 96 | } 97 | 98 | /** 99 | * @dataProvider listAPIPrefixes 100 | */ 101 | function testTimerAdd($prefix) 102 | { 103 | $t = $this->cpf($prefix, 'timer_add', array('timer' => 'testTimerAdd'), 2.0); 104 | $v = $this->cpf($prefix, 'timer_get_info', $t); 105 | 106 | $this->assertSame(false, $v['started'], 'Timer started should be false before stop'); 107 | $this->assertSame(2.0, $v['value'], 'Timer time should be the same as set'); 108 | } 109 | 110 | /** 111 | * @dataProvider listAPIPrefixes 112 | */ 113 | function testNoTimer($prefix) 114 | { 115 | // might be a warning or a notice, depending upon the API in use 116 | $this->expectException('\PHPUnit\Framework\Error\Error'); 117 | $v = $this->cpf($prefix, 'timer_get_info', -1); 118 | } 119 | 120 | // this test can not be easily run using the pinba extension: we would have to use an invalid resource instead of -1 121 | function testBadStop1($prefix = self::PPC) 122 | { 123 | $v = $this->cpf($prefix, 'timer_stop', -1); 124 | $this->assertSame(false, $v, 'Timer stop should fail on non-timer'); 125 | } 126 | 127 | /** 128 | * @dataProvider listAPIPrefixes 129 | */ 130 | function testBadStop2($prefix) 131 | { 132 | // might be a warning or a notice, depending upon the API in use 133 | $this->expectException('\PHPUnit\Framework\Error\Error'); 134 | $t = $this->cpf($prefix, 'timer_add', array('timer' => 'testBadStop2'), 1.0); 135 | $v = $this->cpf($prefix, 'timer_stop', $t); 136 | $v = $this->cpf($prefix, 'timer_stop', $t); 137 | } 138 | 139 | /** 140 | * @dataProvider listAPIPrefixes 141 | */ 142 | function testTimerDelete($prefix) 143 | { 144 | $t = $this->cpf($prefix, 'timer_start', array('timer' => 'testTimerDelete')); 145 | $r = $this->cpf($prefix, 'timer_delete', $t); 146 | $this->assertSame(true, $r, 'the timer should have been deleted'); 147 | $timers = $this->cpf($prefix, 'timers_get'); 148 | $this->assertSame(0, count($timers), 'there should be no timers'); 149 | if ($prefix != 'pinba_') { 150 | /// @todo we should find a resource to pass instead of an invalid int 151 | $r = $this->cpf($prefix, 'timer_delete', 999); 152 | $this->assertSame(false, $r, 'inexisting timer should not have been deleted'); 153 | } 154 | } 155 | 156 | /** 157 | * @dataProvider listAPIPrefixes 158 | */ 159 | function testGetTimers($prefix) 160 | { 161 | $t1 = $this->cpf($prefix, 'timer_start', array('timer' => 'testGetTimers_1')); 162 | $t2 = $this->cpf($prefix, 'timer_start', array('timer' => 'testGetTimers_2')); 163 | $this->cpf($prefix, 'timer_stop', $t1); 164 | $timers = $this->cpf($prefix, 'timers_get'); 165 | $this->assertSame(2, count($timers), 'there should be 2 timers'); 166 | $timers = $this->cpf($prefix, 'timers_get', pinba::ONLY_STOPPED_TIMERS); 167 | $this->assertSame(1, count($timers), 'there should be 1 stopped timer'); 168 | $this->cpf($prefix, 'timers_stop'); 169 | $timers = $this->cpf($prefix, 'timers_get'); 170 | $this->assertSame(2, count($timers), 'there should be 2 timers'); 171 | $timers = $this->cpf($prefix, 'timers_get', pinba::ONLY_STOPPED_TIMERS); 172 | $this->assertSame(2, count($timers), 'there should be 2 stopped timers'); 173 | $t2 = $this->cpf($prefix, 'timer_add', array('timer' => 'testGetTimers_3'), 1); 174 | $timers = $this->cpf($prefix, 'timers_get'); 175 | $this->assertSame(3, count($timers), 'there should be 3 timers'); 176 | $timers = $this->cpf($prefix, 'timers_get', pinba::ONLY_STOPPED_TIMERS); 177 | $this->assertSame(3, count($timers), 'there should be 3 stopped timers'); 178 | } 179 | 180 | /** 181 | * @dataProvider listAPIPrefixes 182 | */ 183 | function testTags($prefix) 184 | { 185 | $v = $this->cpf($prefix, 'tag_get', 'hey'); 186 | $this->assertSame(false, $v, 'there should be no tag'); 187 | $this->cpf($prefix, 'tag_set', 'hey', 'there'); 188 | $v = $this->cpf($prefix, 'tag_get', 'hey'); 189 | $this->assertSame('there', $v, 'there should a tag'); 190 | $this->cpf($prefix, 'tag_set', 'hey', 'you'); 191 | $v = $this->cpf($prefix, 'tag_get', 'hey'); 192 | $this->assertSame('you', $v, 'tag should have been modified'); 193 | $this->cpf($prefix, 'tag_set', 'you', 10); 194 | $v = $this->cpf($prefix, 'tags_get'); 195 | $this->assertSame(array('hey' => 'you', 'you' => '10'), $v, 'tags should be present and converted to string'); 196 | $v = $this->cpf($prefix, 'get_info'); 197 | $this->assertSame(array('hey' => 'you', 'you' => '10'), $v['tags'], 'tags should be present'); 198 | $v = $this->cpf($prefix, 'tag_delete', 'hey'); 199 | $this->assertSame(true, $v, 'tag deletion should succeed'); 200 | $v = $this->cpf($prefix, 'tag_get', 'hey'); 201 | $this->assertSame(false, $v, 'there should be no tag'); 202 | $v = $this->cpf($prefix, 'tag_delete', 'hey'); 203 | $this->assertSame(false, $v, 'tag deletion should fail'); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /tests/02FlushTest.php: -------------------------------------------------------------------------------- 1 | pReset(); 44 | 45 | // generate a unique id for the test, transparently used in flush() calls 46 | $this->id = uniqid('02'); 47 | pinba::script_name_set($this->id); 48 | if (extension_loaded('pinba')) { 49 | pinba_script_name_set($this->id); 50 | } 51 | 52 | // in case any test sets pinba.enabled=0 53 | pinba::ini_set('pinba.enabled', 1); 54 | if (extension_loaded('pinba')) { 55 | ini_set('pinba.enabled', 1); 56 | } 57 | } 58 | 59 | /** 60 | * @dataProvider listAPIPrefixes 61 | */ 62 | function testFlush($prefix) 63 | { 64 | $t1 = $this->cpf($prefix, 'timer_start', array('timer' => 'testFlush')); 65 | $this->cpf($prefix, 'tag_set', 'class', 'FlushTest'); 66 | $this->cpf($prefix, 'tag_set', 'test', 'testFlush'); 67 | 68 | $this->cpf($prefix, 'flush'); 69 | 70 | $v1 = $this->cpf($prefix, 'timer_get_info', $t1); 71 | $this->assertSame(false, $v1['started'], 'timer should have been stopped by flush call'); 72 | 73 | $v = $this->cpf($prefix, 'get_info'); 74 | $this->assertSame(0, count($v['timers']), 'timer should have been deleted by flush call'); 75 | 76 | sleep(2); // we can not reduce it, as we have to wait for rollup into the reports tables 77 | 78 | if (self::$pinba1) { 79 | $r = self::$db->query("SELECT * FROM request WHERE script_name='" . self::$db->escape_string($this->id) ."';")->fetch_all(MYSQLI_ASSOC); 80 | 81 | $this->assertSame(1, count($r), 'no request data found in the db for a flush call'); 82 | $r = $r[0]; 83 | $this->assertSame($v['hostname'], $r['hostname'], 'hostname data was not sent correctly to the db'); 84 | $this->assertSame(0, (int)$r['req_count'], 'req_count data was not sent correctly to the db'); 85 | $this->assertSame($v['server_name'], $r['server_name'], 'server_name data was not sent correctly to the db'); 86 | $this->assertSame($v['script_name'], $r['script_name'], 'script_name data was not sent correctly to the db'); 87 | $this->assertSame($v['doc_size'], (int)$r['doc_size'], 'doc_size data was not sent correctly to the db'); 88 | $this->assertSame((int)round($v['mem_peak_usage']/1024), (int)$r['mem_peak_usage'], 'mem_peak_usage data was not sent correctly to the db'); 89 | $this->assertSame(1, (int)$r['timers_cnt'], 'timers data was not sent correctly to the db'); 90 | $this->assertContains($r['schema'], array('', ''), 'schema data was not sent correctly to the db'); 91 | $this->assertSame(2, (int)$r['tags_cnt'], 'tags data was not sent correctly to the db'); 92 | $this->assertSame('class=FlushTest,test=testFlush', $r['tags'], 'tags data was not sent correctly to the db'); 93 | 94 | $r = self::$db->query("SELECT t.*, g.name AS tagname, tt.value AS tagvalue FROM timer t, timertag tt, tag g, request r WHERE t.id=tt.timer_id AND tt.tag_id = g.id AND t.request_id = r.id AND r.script_name='" . self::$db->escape_string($this->id) ."';")->fetch_all(MYSQLI_ASSOC); 95 | $this->assertSame(1, count($r), 'no timer data found in the db for a flush call'); 96 | $r = $r[0]; 97 | $this->assertSame(1, (int)$r['hit_count'], 'timer hit_count was not sent correctly to the db'); 98 | $this->assertSame('testFlush', $r['tagvalue'], 'timer tag value was not sent correctly to the db'); 99 | $this->assertSame('timer', $r['tagname'], 'timer tag name was not sent correctly to the db'); 100 | $this->assertSame(round($v1['value'], 3), round($r['value'], 3), 'timer value was not sent correctly to the db'); 101 | } 102 | 103 | if (self::$pinba1) { 104 | $col = 'script_name'; 105 | } else { 106 | $col = 'script'; 107 | } 108 | $r = self::$db->query("SELECT * FROM report_by_script_name WHERE $col='" . self::$db->escape_string($this->id) ."';")->fetch_all(MYSQLI_ASSOC); 109 | $this->assertSame(1, count($r), 'no aggregate data found in the db for a flush call'); 110 | } 111 | 112 | /** 113 | * @dataProvider listAPIPrefixesMatrix 114 | */ 115 | function testFlushOnlyStoppedTimers($prefix, $pinbaEnabled) 116 | { 117 | if ($prefix == 'pinba_') { 118 | $this->cpf('', 'ini_set', 'pinba.enabled', $pinbaEnabled); 119 | $case = '0'; 120 | } else { 121 | $this->cpf($prefix, 'ini_set', 'pinba.enabled', $pinbaEnabled); 122 | $case = '1'; 123 | } 124 | 125 | $t1 = $this->cpf($prefix, 'timer_start', array('timer' => 'testFlushOnlyStoppedTimers_1_' . $case . $pinbaEnabled)); 126 | $t2 = $this->cpf($prefix, 'timer_add', array('timer' => 'testFlushOnlyStoppedTimers_2_' . $case . $pinbaEnabled), 1); 127 | $this->cpf($prefix, 'flush', null, pinba::FLUSH_ONLY_STOPPED_TIMERS); 128 | 129 | $v1 = $this->cpf($prefix, 'timer_get_info', $t1); 130 | $this->assertSame(true, $v1['started'], 'timer should not have been stopped by flush call'); 131 | $v = $this->cpf($prefix, 'timers_get'); 132 | $this->assertSame(1, count($v), 'one timer should not have been deleted by flush call'); 133 | $ti = $this->cpf($prefix, 'timer_get_info', $v[0]); 134 | $this->assertSame($ti['tags'], $v1['tags'], 'started timer should not have been deleted by flush call'); 135 | $v = $this->cpf($prefix, 'get_info'); 136 | $this->assertSame(1, count($v['timers']), 'one timer should not have been deleted by flush call'); 137 | //$this->assertSame($v['timers'][0]['tags'], $v1['tags'], 'started timer should not have been deleted by flush call'); 138 | $this->assertSame($v['timers'][0]['tags'], $v1['tags'], 'started timer should not have been deleted by flush call'); 139 | 140 | $v = $this->cpf($prefix, 'timer_get_info', $t2); 141 | $this->assertNotEquals(false, $v, 'flushed timer info should still be available'); 142 | } 143 | 144 | /** 145 | * @dataProvider listAPIPrefixes 146 | */ 147 | function testFlushAdditiveTimers($prefix) 148 | { 149 | if (!self::$pinba1) { 150 | $this->markTestSkipped('Can not test flushed timers on pinba2'); 151 | } 152 | 153 | $t1 = $this->cpf($prefix, 'timer_add', array('timer' => 'testFlushAdditiveTimers', 'extra' => md5($prefix)), 2); 154 | $t2 = $this->cpf($prefix, 'timer_add', array('extra' => md5($prefix), 'timer' => 'testFlushAdditiveTimers'), 3); 155 | $t3 = $this->cpf($prefix, 'timer_start', array('timer' => 'testFlushAdditiveTimers', 'extra' => md5($prefix)), array('whatever'), 2); 156 | usleep(100000); 157 | $this->cpf($prefix, 'flush'); 158 | sleep(1); 159 | $r = self::$db->query("SELECT t.* FROM timer t, request r WHERE t.request_id = r.id AND r.script_name='" . self::$db->escape_string($this->id) ."';")->fetch_all(MYSQLI_ASSOC); 160 | $this->assertSame(1, count($r), 'no timer data found in the db for a flush call'); 161 | $r = $r[0]; 162 | $this->assertSame(4, (int)$r['hit_count'], 'timer hit_count was not sent correctly to the db'); 163 | $this->assertSame(round(5.1, 1), round((float)$r['value'], 1), 'timer value was not sent correctly to the db'); 164 | } 165 | 166 | /** 167 | * @dataProvider listAPIPrefixes 168 | */ 169 | function testFlushUTF8($prefix) 170 | { 171 | // greek word 'kosme' 172 | $id = uniqid() . '_κόσμε'; 173 | 174 | $this->cpf($prefix, 'flush', $id); 175 | sleep(2); // we can not reduce it, as we have to wait for rollup into the reports tables 176 | if (self::$pinba1) { 177 | $col = 'script_name'; 178 | } else { 179 | $col = 'script'; 180 | } 181 | $r = self::$db->query("SELECT * FROM report_by_script_name WHERE $col='" . self::$db->escape_string($id) ."';")->fetch_all(MYSQLI_ASSOC); 182 | 183 | $this->assertSame(1, count($r), 'no aggregate data found in the db for a flush call'); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /tests/03ClassTest.php: -------------------------------------------------------------------------------- 1 | setHostname('hostname'); 20 | $c->setRequestCount(99); 21 | $c->setServername('servername'); 22 | $c->setScriptname($id); 23 | $c->setDocumentSize(123); 24 | $c->setMemoryPeak(456000); 25 | $c->setMemoryFootprint(321000); 26 | $c->setStatus(999); 27 | $c->setSchema('http99'); 28 | $c->setRusage($rusage); 29 | $c->setTag('tag', 'gat'); 30 | $c->addTimer(array('ClientTimer' => 'testPinbaClient'), 1, $rusage, 2); 31 | // this timer should replace the previous one 32 | $c->setTimer(array('ClientTimer' => 'testPinbaClient'), 0.1); 33 | // these 2 timers should get merged 34 | $c->addTimer(array('ClientTimer' => 'testPinbaClient', 'extra' => '1'), 2, $rusage); 35 | $c->addTimer(array('extra' => '1', 'ClientTimer' => 'testPinbaClient'), 3, $rusage); 36 | 37 | $v = $c->send(); 38 | sleep(2); // we can not reduce it, as we have to wait for rollup into the reports tables 39 | 40 | if (self::$pinba1) { 41 | $r = self::$db->query("SELECT * FROM request WHERE script_name='" . self::$db->escape_string($id) ."';")->fetch_all(MYSQLI_ASSOC); 42 | 43 | $this->assertSame(1, count($r), 'no request data found in the db for a flush call'); 44 | $r = $r[0]; 45 | $this->assertSame('hostname', $r['hostname'], 'hostname data was not sent correctly to the db'); 46 | $this->assertSame(99, (int)$r['req_count'], 'req_count data was not sent correctly to the db'); 47 | $this->assertSame('servername', $r['server_name'], 'server_name data was not sent correctly to the db'); 48 | $this->assertSame($id, $r['script_name'], 'script_name data was not sent correctly to the db'); 49 | $this->assertSame(round(123/1204, 1), round($r['doc_size'], 1), 'doc_size data was not sent correctly to the db'); 50 | $this->assertSame(round(456000/1024, 2), round($r['mem_peak_usage'], 2), 'mem_peak_usage data was not sent correctly to the db'); 51 | $this->assertSame(round(321000/1024, 2), round($r['memory_footprint'], 2), 'mem_peak_usage data was not sent correctly to the db'); 52 | $this->assertSame(999, (int)$r['status'], 'status data was not sent correctly to the db'); 53 | $this->assertSame('http99', $r['schema'], 'schema data was not sent correctly to the db'); 54 | $this->assertSame(round(0.5, 2), round($r['ru_utime'], 2), 'ru_utime data was not sent correctly to the db'); 55 | $this->assertSame(round(0.6, 2), round($r['ru_stime'], 2), 'ru_stime data was not sent correctly to the db'); 56 | $this->assertSame(1, (int)$r['tags_cnt'], 'tags data was not sent correctly to the db'); 57 | $this->assertSame('tag=gat', $r['tags'], 'tags data was not sent correctly to the db'); 58 | $this->assertSame(2, (int)$r['timers_cnt'], 'timers data was not sent correctly to the db'); 59 | 60 | $r = self::$db->query("SELECT t.*, g.name AS tagname, tt.value AS tagvalue FROM timer t, timertag tt, tag g, request r WHERE t.id=tt.timer_id AND tt.tag_id = g.id AND t.request_id = r.id AND r.script_name='" . self::$db->escape_string($id) ."' ORDER BY t.id, tagname;")->fetch_all(MYSQLI_ASSOC); 61 | $this->assertSame(3, count($r), 'no timer data found in the db for a flush call'); 62 | $this->assertSame(1, (int)$r[0]['hit_count'], 'timer hit_count was not sent correctly to the db'); 63 | $this->assertSame('testPinbaClient', $r[0]['tagvalue'], 'timer tag value was not sent correctly to the db'); 64 | $this->assertSame('ClientTimer', $r[0]['tagname'], 'timer tag name was not sent correctly to the db'); 65 | $this->assertSame(round(0.1, 3), round($r[0]['value'], 3), 'timer value was not sent correctly to the db'); 66 | } 67 | 68 | if (self::$pinba1) { 69 | $col = 'script_name'; 70 | } else { 71 | $col = 'script'; 72 | } 73 | $r = self::$db->query("SELECT * FROM report_by_script_name WHERE $col='" . self::$db->escape_string($id) ."';")->fetch_all(MYSQLI_ASSOC); 74 | $this->assertSame(1, count($r), 'no aggregate data found in the db for a flush call'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/APITest.php: -------------------------------------------------------------------------------- 1 | connect_errno) { 104 | /// @todo find an exception existing from phpunit 4 to 8 105 | throw new PHPUnit_Framework_Exception("Can not connect to the Pinba DB"); 106 | } 107 | 108 | // Pinba 2 does not have raw data tables 109 | $r = self::$db->query("SELECT table_name FROM information_schema.tables WHERE table_schema='pinba' AND table_name='request';")->fetch_row(); 110 | if (is_array($r) && count($r)) { 111 | self::$pinba1 = true; 112 | } else { 113 | // the test db in the pinba2 container defaults to latin-1, but we create the report table using utf8 114 | self::$db->set_charset('utf8'); 115 | } 116 | // this is required to "start" the reporting table 117 | self::$db->query("SELECT * FROM report_by_script_name;")->fetch_all(MYSQLI_ASSOC); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/benchmark.php: -------------------------------------------------------------------------------- 1 | '1'); //, 'longLongTag' => "This is possibly too long for its own good: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); 10 | 11 | function wait($usecs) 12 | { 13 | /*if ($usecs === 0) { 14 | return; 15 | } 16 | usleep($usecs);*/ 17 | } 18 | 19 | $m = memory_get_usage(); 20 | $time = microtime(true); 21 | for($i = 0; $i < ITERATIONS; $i++) { 22 | wait(PAUSEUSECS); 23 | } 24 | $time = microtime(true) - $time; 25 | $m = memory_get_usage() - $m; 26 | 27 | if (extension_loaded('pinba')) 28 | { 29 | ini_set('pinba.enanled', 1); 30 | ini_set('pinba.server', getenv('PINBA_SERVER') . ':' . getenv('PINBA_PORT')); 31 | 32 | $m1 = memory_get_usage(); 33 | $time1 = microtime(true); 34 | for($i = 0; $i < ITERATIONS; $i++) { 35 | $t = pinba_timer_start($TAGS); 36 | wait(PAUSEUSECS); 37 | pinba_timer_stop($t); 38 | } 39 | $time1 = microtime(true) - $time1; 40 | $m1 = memory_get_usage() - $m1; 41 | 42 | ini_set('pinba.enanled', 0); 43 | pinba_flush(); 44 | pinba_reset(); 45 | } else { 46 | $m1 = 0; 47 | $time1 = 0; 48 | } 49 | 50 | $m2 = memory_get_usage(); 51 | $time2 = microtime(true); 52 | for($i = 0; $i < ITERATIONS; $i++) { 53 | $t = pinba::timer_start($TAGS); 54 | wait(PAUSEUSECS); 55 | pinba::timer_stop($t); 56 | } 57 | $time2 = microtime(true) - $time2; 58 | $m2 = memory_get_usage() - $m2; 59 | 60 | echo "Tested execution of " . ITERATIONS . " empty function calls in a loop:\n"; 61 | echo "No timing: " . sprintf("%.5f", $time) . " secs, " . sprintf("%7d", $m) . " bytes used\n"; 62 | echo "Pinba-timed: " . sprintf("%.5f", $time1) . " secs, " . sprintf("%7d", $m1) . " bytes used\n"; 63 | echo "PHPPinba timed: " . sprintf("%.5f", $time2) . " secs, " . sprintf("%7d", $m2) . " bytes used\n"; 64 | -------------------------------------------------------------------------------- /tests/ci/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | php: 6 | image: ${COMPOSE_PROJECT_NAME:-pinbapolyfill}/php 7 | build: 8 | context: ./images/php 9 | args: 10 | ubuntu_version: ${TESTSTACK_UBUNTU_VERSION:-focal} 11 | php_version: ${TESTSTACK_PHP_VERSION:-default} 12 | hostname: php 13 | container_name: ${COMPOSE_PROJECT_NAME:-pinbapolyfill}-php 14 | environment: 15 | # Configuration used by the boot/setup scripts 16 | - CONTAINER_USER_UID=${CONTAINER_USER_UID:-1000} 17 | - CONTAINER_GROUP_GID=${CONTAINER_GROUP_GID:-1000} 18 | # q: are these 2 used by composer? 19 | - http_proxy 20 | - https_proxy 21 | # Composer configuration 22 | - COMPOSER_AUTH 23 | - COMPOSER_PREFER_LOWEST 24 | # Tests configuration 25 | - PINBA_SERVER=${PINBA_SERVER:-pinba} 26 | - PINBA_PORT=${PINBA_PORT:-30002} 27 | - PINBA_DB_SERVER=${PINBA_DB_SERVER:-pinba} 28 | - PINBA_DB_PORT=3306 29 | - PINBA_DB_USER=pinba 30 | - PINBA_DB_PASSWORD=pinba 31 | - PINBA_DB_DATABASE=pinba 32 | # As opposed to TRAVIS=true ;-) 33 | - DOCKER=true 34 | volumes: 35 | - ../../:/home/docker/build 36 | # - ${TESTSTACK_COMPOSER_CACHE:-...}/:/home/docker/.composer 37 | ports: 38 | # used to expose adminer and other courtesy tools for the tester 39 | - "${TESTSTACK_WEB_PORT:-8080}:80" 40 | 41 | pinba: 42 | image: ${COMPOSE_PROJECT_NAME:-pinbapolyfill}/pinba 43 | build: 44 | context: ./images/pinba 45 | hostname: pinba 46 | container_name: ${COMPOSE_PROJECT_NAME:-pinbapolyfill}-pinba 47 | environment: 48 | #- CONTAINER_USER_UID=${CONTAINER_USER_UID:-1000} 49 | #- CONTAINER_USER_GID=${CONTAINER_USER_GID:-1000} 50 | # As opposed to TRAVIS=true ;-) 51 | - DOCKER=true 52 | #ports: 53 | # - "3307:3306" 54 | # - "30003:30002" 55 | 56 | pinba2: 57 | image: ${COMPOSE_PROJECT_NAME:-pinbapolyfill}/pinba2 58 | build: 59 | context: ./images/pinba2 60 | hostname: pinba2 61 | container_name: ${COMPOSE_PROJECT_NAME:-pinbapolyfill}-pinba2 62 | environment: 63 | #- CONTAINER_USER_UID=${CONTAINER_USER_UID:-1000} 64 | #- CONTAINER_USER_GID=${CONTAINER_USER_GID:-1000} 65 | # As opposed to TRAVIS=true ;-) 66 | - DOCKER=true 67 | #ports: 68 | # - "3308:3306" 69 | # - "30004:3002" 70 | 71 | #networks: 72 | # default: 73 | # ipam: 74 | # config: 75 | # - subnet: "${TESTSTACK_SUBNET:-172.19.30}.0/24" 76 | -------------------------------------------------------------------------------- /tests/ci/images/php/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ubuntu_version=focal 2 | 3 | FROM ubuntu:${ubuntu_version} 4 | 5 | ARG php_version=default 6 | 7 | # Copy all the required shell scripts and config files 8 | COPY setup/*.sh /root/build/ 9 | COPY config/* /root/config/ 10 | 11 | RUN mkdir -p /usr/share/man/man1; mkdir -p /usr/share/man/man7; \ 12 | apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y upgrade && \ 13 | cd /root/build/ && \ 14 | chmod 755 *.sh && \ 15 | ./install_packages.sh && \ 16 | ./create_user.sh && \ 17 | ./setup_apache.sh && \ 18 | ./setup_php.sh "${php_version}" && \ 19 | ./setup_composer.sh && \ 20 | cd /var/www/html && \ 21 | # no need for pinboard (yet) 22 | #/root/build/setup_pinboard.sh && \ 23 | /root/build/setup_adminer.sh 24 | 25 | COPY entrypoint.sh /root/ 26 | RUN chmod 755 /root/entrypoint*.sh 27 | 28 | # @todo can we avoid hardcoding this here? We can f.e. get it passed down as ARG... 29 | WORKDIR /home/docker/build 30 | 31 | EXPOSE 80 32 | 33 | ENTRYPOINT ["/root/entrypoint.sh"] 34 | -------------------------------------------------------------------------------- /tests/ci/images/php/config/apache_phpfpm_proxyfcgi: -------------------------------------------------------------------------------- 1 | # @todo check: templatize this, to make it work with any php version 2 | 3 | # Redirect to local php-fpm if mod_php is not available 4 | 5 | 6 | 7 | 8 | # Enable http authorization headers 9 | 10 | SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1 11 | 12 | 13 | 14 | SetHandler "proxy:unix:/run/php/php-fpm.sock|fcgi://localhost" 15 | 16 | 17 | # Deny access to raw php sources by default 18 | # To re-enable it's recommended to enable access to the files 19 | # only in specific virtual host or directory 20 | Require all denied 21 | 22 | # Deny access to files without filename (e.g. '.php') 23 | 24 | Require all denied 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/ci/images/php/config/apache_vhost: -------------------------------------------------------------------------------- 1 | ## Uses env vars: 2 | ## HTTPSERVER 3 | ## TESTS_ROOT_DIR 4 | 5 | 6 | 7 | DocumentRoot /var/www/html 8 | 9 | #ErrorLog "${TESTS_ROOT_DIR}/apache_error.log" 10 | #CustomLog "${TESTS_ROOT_DIR}/apache_access.log" combined 11 | 12 | # Env vars used by the test code, which we get from the environment 13 | ##SetEnv HTTPSERVER ${HTTPSERVER} 14 | 15 | 16 | Options FollowSymLinks MultiViews 17 | AllowOverride All 18 | 19 | Require all granted 20 | 21 | # needed for basic auth (PHP_AUTH_USER and PHP_AUTH_PW) 22 | #RewriteEngine on 23 | #RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 24 | #RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}] 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/ci/images/php/config/codecoverage_xdebug.ini: -------------------------------------------------------------------------------- 1 | # php.ini settings used to enable code coverage with xdebug 2 | 3 | zend_extension=xdebug.so 4 | 5 | # xdebug 3 6 | xdebug.mode=coverage 7 | # xdebug 2 8 | xdebug.coverage_enable=1 9 | 10 | memory_limit = -1 11 | -------------------------------------------------------------------------------- /tests/ci/images/php/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | USERNAME="${1:-docker}" 4 | 5 | echo "[$(date)] Bootstrapping the Test container..." 6 | 7 | clean_up() { 8 | # Perform program exit housekeeping 9 | 10 | echo "[$(date)] Stopping the Web server" 11 | service apache2 stop 12 | 13 | echo "[$(date)] Stopping FPM" 14 | service php-fpm stop 15 | 16 | echo "[$(date)] Exiting" 17 | exit 18 | } 19 | 20 | # Fix UID & GID for user 21 | 22 | echo "[$(date)] Fixing filesystem permissions..." 23 | 24 | ORIGPASSWD=$(cat /etc/passwd | grep "^${USERNAME}:") 25 | ORIG_UID=$(echo "$ORIGPASSWD" | cut -f3 -d:) 26 | ORIG_GID=$(echo "$ORIGPASSWD" | cut -f4 -d:) 27 | CONTAINER_USER_HOME=$(echo "$ORIGPASSWD" | cut -f6 -d:) 28 | CONTAINER_USER_UID=${CONTAINER_USER_UID:=$ORIG_UID} 29 | CONTAINER_USER_GID=${CONTAINER_USER_GID:=$ORIG_GID} 30 | 31 | if [ "$CONTAINER_USER_UID" != "$ORIG_UID" -o "$CONTAINER_USER_GID" != "$ORIG_GID" ]; then 32 | groupmod -g "$CONTAINER_USER_GID" "${USERNAME}" 33 | usermod -u "$CONTAINER_USER_UID" -g "$CONTAINER_USER_GID" "${USERNAME}" 34 | fi 35 | if [ "$(stat -c '%u' "${CONTAINER_USER_HOME}")" != "${CONTAINER_USER_UID}" -o "$(stat -c '%g' "${CONTAINER_USER_HOME}")" != "${CONTAINER_USER_GID}" ]; then 36 | chown "${CONTAINER_USER_UID}":"${CONTAINER_USER_GID}" "${CONTAINER_USER_HOME}" 37 | chown -R "${CONTAINER_USER_UID}":"${CONTAINER_USER_GID}" "${CONTAINER_USER_HOME}"/.* 38 | if [ -d /usr/local/php ]; then 39 | chown -R "${CONTAINER_USER_UID}":"${CONTAINER_USER_GID}" /usr/local/php 40 | fi 41 | fi 42 | 43 | echo "[$(date)] Fixing Apache configuration..." 44 | 45 | ##sed -e "s?^export TESTS_ROOT_DIR=.*?export TESTS_ROOT_DIR=${TESTS_ROOT_DIR}?g" --in-place /etc/apache2/envvars 46 | sed -e "s?^export APACHE_RUN_USER=.*?export APACHE_RUN_USER=${USERNAME}?g" --in-place /etc/apache2/envvars 47 | sed -e "s?^export APACHE_RUN_GROUP=.*?export APACHE_RUN_GROUP=${USERNAME}?g" --in-place /etc/apache2/envvars 48 | 49 | echo "[$(date)] Fixing FPM configuration..." 50 | 51 | PHPVER=$(php -r 'echo implode(".",array_slice(explode(".",PHP_VERSION),0,2));' 2>/dev/null) 52 | FPMCONF="/etc/php/${PHPVER}/fpm/pool.d/www.conf" 53 | sed -e "s?^user =.*?user = ${USERNAME}?g" --in-place "${FPMCONF}" 54 | sed -e "s?^group =.*?group = ${USERNAME}?g" --in-place "${FPMCONF}" 55 | sed -e "s?^listen.owner =.*?listen.owner = ${USERNAME}?g" --in-place "${FPMCONF}" 56 | sed -e "s?^listen.group =.*?listen.group = ${USERNAME}?g" --in-place "${FPMCONF}" 57 | 58 | echo "[$(date)] Running Composer..." 59 | 60 | # @todo if there is a composer.lock file present, there are chances it might be a leftover from when running the 61 | # container using a different php version. We should then back it up / do some symlink magic to make sure that 62 | # it matches the current php version and a hash of composer.json... 63 | sudo "${USERNAME}" -c "cd ${TESTS_ROOT_DIR} && composer install" 64 | 65 | # @todo fix the config of pinboard to point to the correct pinba target. 66 | 67 | if [ -z "${PINBA_PORT}" -a "${PINBA_SERVER}" = pinba2 ]; then 68 | PINBA_PORT=3002 69 | fi 70 | 71 | echo "[$(date)] Setting up pinba_php configuration..." 72 | if [ -f "/etc/php/${PHPVER}/cli/conf.d/20-pinba.ini" ]; then 73 | echo "extension=pinba.so" > "/etc/php/${PHPVER}/cli/conf.d/20-pinba.ini" 74 | echo "pinba.enabled=1" >> "/etc/php/${PHPVER}/cli/conf.d/20-pinba.ini" 75 | echo "pinba.server=${PINBA_SERVER}:${PINBA_PORT}" >> "/etc/php/${PHPVER}/cli/conf.d/20-pinba.ini" 76 | fi 77 | if [ -f "/etc/php/${PHPVER}/fpm/conf.d/20-pinba.ini" ]; then 78 | echo "extension=pinba.so" > "/etc/php/${PHPVER}/fpm/conf.d/20-pinba.ini" 79 | echo "pinba.enabled=1" >> "/etc/php/${PHPVER}/fpm/conf.d/20-pinba.ini" 80 | echo "pinba.server=${PINBA_SERVER}:${PINBA_PORT}" >> "/etc/php/${PHPVER}/fpm/conf.d/20-pinba.ini" 81 | fi 82 | 83 | trap clean_up TERM 84 | 85 | echo "[$(date)] Starting FPM..." 86 | service php-fpm start 87 | 88 | echo "[$(date)] Starting the Web server..." 89 | service apache2 start 90 | 91 | echo "[$(date)] Bootstrap finished" 92 | 93 | tail -f /dev/null & 94 | child=$! 95 | wait "$child" 96 | -------------------------------------------------------------------------------- /tests/ci/images/php/setup/create_user.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # @todo make the GID & UID of the user variable (we picked 2000 as it is the one used by default by Travis) 4 | 5 | set -e 6 | 7 | USERNAME="${1:-docker}" 8 | 9 | addgroup --gid 2000 "${USERNAME}" 10 | adduser --system --uid=2000 --gid=2000 --home "/home/${USERNAME}" --shell /bin/bash "${USERNAME}" 11 | adduser "${USERNAME}" "${USERNAME}" 12 | 13 | mkdir -p "/home/${USERNAME}/.ssh" 14 | cp /etc/skel/.[!.]* "/home/${USERNAME}" 15 | 16 | chown -R "${USERNAME}:${USERNAME}" "/home/${USERNAME}" 17 | 18 | if [ -f /etc/sudoers ]; then 19 | adduser "${USERNAME}" sudo 20 | sed -i "\$ a ${USERNAME} ALL=\(ALL:ALL\) NOPASSWD: ALL" /etc/sudoers 21 | fi 22 | -------------------------------------------------------------------------------- /tests/ci/images/php/setup/install_packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Has to be run as admin 4 | 5 | set -e 6 | 7 | echo "Installing base software packages..." 8 | 9 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 10 | git protobuf-compiler tcpdump sudo unzip wget 11 | 12 | echo "Done installing base software packages" 13 | -------------------------------------------------------------------------------- /tests/ci/images/php/setup/setup_adminer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | wget -q -O adminer.php https://github.com/vrana/adminer/releases/download/v4.8.1/adminer-4.8.1.php 6 | -------------------------------------------------------------------------------- /tests/ci/images/php/setup/setup_apache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Install and configure apache2 4 | # Has to be run as admin 5 | # @todo test: does this work across all ubuntu versions (precise to jammy)? 6 | # @todo pass in web root dir as arg 7 | 8 | echo "Installing and configuring Apache2..." 9 | 10 | set -e 11 | 12 | SCRIPT_DIR="$(dirname -- "$(readlink -f "$0")")" 13 | 14 | DEBIAN_FRONTEND=noninteractive apt-get install -y apache2 15 | 16 | # set up Apache for php-fpm 17 | 18 | a2enmod rewrite proxy_fcgi setenvif ssl http2 19 | 20 | # in case mod-php was enabled (this is the case at least on GHA's ubuntu with php 5.x and shivammathur/setup-php) 21 | if [ -n "$(ls /etc/apache2/mods-enabled/php* 2>/dev/null)" ]; then 22 | rm /etc/apache2/mods-enabled/php* 23 | fi 24 | 25 | # configure apache virtual hosts 26 | 27 | cp -f "$SCRIPT_DIR/../config/apache_vhost" /etc/apache2/sites-available/000-default.conf 28 | 29 | # default apache siteaccess found in GHA Ubuntu. We remove it just in case 30 | if [ -f /etc/apache2/sites-available/default-ssl.conf ]; then 31 | rm /etc/apache2/sites-available/default-ssl.conf 32 | fi 33 | 34 | if [ -n "${GITHUB_ACTION}" ]; then 35 | ln -s "$(pwd)" /var/www/html/pinba 36 | else 37 | if [ ! -d /home/docker/build ]; then mkdir -p /home/docker/build; fi 38 | ln -s /home/docker/build /var/www/html/pinba 39 | fi 40 | 41 | service apache2 restart 42 | 43 | echo "Done Installing and configuring Apache2" 44 | -------------------------------------------------------------------------------- /tests/ci/images/php/setup/setup_code_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # @todo add 'query' action 4 | # @todo avoid reloading php-fpm if config did not change 5 | 6 | # Note: we have php set up either via phpenv (TRAVIS=true), Ubuntu packages (PHP_VERSION=default) or Sury packages. 7 | # xdebug comes either at version 2 or 3 8 | 9 | set -e 10 | 11 | if [ "$TRAVIS" != true ]; then 12 | PHPCONFDIR_CLI=$(php -i | grep 'Scan this dir for additional .ini files' | sed 's|Scan this dir for additional .ini files => ||') 13 | PHPCONFDIR_FPM=$(echo "$PHPCONFDIR_CLI" | sed 's|/cli/|/fpm/|') 14 | fi 15 | 16 | enable_cc() { 17 | if [ "$TRAVIS" = true ]; then 18 | phpenv config-add tests/ci/images/php/config/codecoverage_xdebug.ini 19 | 20 | pkill php-fpm 21 | "~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm" 22 | else 23 | if [ -L "${PHPCONFDIR_CLI}/99-codecoverage_xdebug.ini" ]; then sudo rm "${PHPCONFDIR_CLI}/99-codecoverage_xdebug.ini"; fi 24 | sudo ln -s "$(realpath tests/ci/images/php/config/codecoverage_xdebug.ini)" "${PHPCONFDIR_CLI}/99-codecoverage_xdebug.ini" 25 | if [ -L "${PHPCONFDIR_FPM}/99-codecoverage_xdebug.ini" ]; then sudo rm "${PHPCONFDIR_FPM}/99-codecoverage_xdebug.ini"; fi 26 | sudo ln -s "$(realpath tests/ci/images/php/config/codecoverage_xdebug.ini)" "${PHPCONFDIR_FPM}/99-codecoverage_xdebug.ini" 27 | 28 | sudo service php-fpm restart 29 | fi 30 | } 31 | 32 | disable_cc() { 33 | if [ "$TRAVIS" = true ]; then 34 | phpenv config-rm tests/ci/images/php/config/codecoverage_xdebug.ini 35 | 36 | pkill php-fpm 37 | "~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm" 38 | else 39 | if [ -L "${PHPCONFDIR_CLI}/99-codecoverage_xdebug.ini" ]; then sudo rm "${PHPCONFDIR_CLI}/99-codecoverage_xdebug.ini"; fi 40 | if [ -L "${PHPCONFDIR_FPM}/99-codecoverage_xdebug.ini" ]; then sudo rm "${PHPCONFDIR_FPM}/99-codecoverage_xdebug.ini"; fi 41 | 42 | sudo service php-fpm restart 43 | fi 44 | } 45 | 46 | case "$1" in 47 | enable | on) 48 | enable_cc 49 | ;; 50 | disable | off) 51 | disable_cc 52 | ;; 53 | *) 54 | echo "ERROR: unknown action '${1}', please use 'enable' or 'disable'" >&2 55 | exit 1 56 | ;; 57 | esac 58 | -------------------------------------------------------------------------------- /tests/ci/images/php/setup/setup_composer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Installs Composer (latest version, to avoid relying on old ones bundled with the OS) 4 | # @todo allow users to lock down to Composer v1 if needed 5 | 6 | echo "Installing Composer..." 7 | 8 | if dpkg -l composer 2>/dev/null; then 9 | apt-get remove -y composer 10 | fi 11 | 12 | ### Code below taken from https://getcomposer.org/doc/faqs/how-to-install-composer-programmatically.md 13 | 14 | EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)" 15 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" 16 | ACTUAL_SIGNATURE="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" 17 | 18 | if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ] 19 | then 20 | >&2 echo 'ERROR: Invalid installer signature' 21 | rm composer-setup.php 22 | exit 1 23 | fi 24 | 25 | php composer-setup.php --quiet --install-dir=/usr/local/bin 26 | RESULT=$? 27 | rm composer-setup.php 28 | 29 | ### 30 | 31 | if [ -f /usr/local/bin/composer.phar -a "$RESULT" = 0 ]; then 32 | mv /usr/local/bin/composer.phar /usr/local/bin/composer && chmod 755 /usr/local/bin/composer 33 | fi 34 | 35 | echo "Done installing Composer" 36 | 37 | exit $RESULT 38 | -------------------------------------------------------------------------------- /tests/ci/images/php/setup/setup_php.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Has to be run as admin 4 | 5 | # @todo make it optional to install xdebug. It is fe. missing in sury's ppa for Xenial 6 | # @todo make it optional to install fpm. It is not needed for the cd workflow 7 | # @todo make it optional to disable xdebug ? 8 | 9 | set -e 10 | 11 | echo "Installing PHP version '${1}'..." 12 | 13 | SCRIPT_DIR="$(dirname -- "$(readlink -f "$0")")" 14 | 15 | configure_php_ini() { 16 | # note: these settings are not required for cli config 17 | echo "cgi.fix_pathinfo = 1" >> "${1}" 18 | echo "always_populate_raw_post_data = -1" >> "${1}" 19 | 20 | # we disable xdebug for speed for both cli and web mode 21 | # @todo make this optional 22 | if which phpdismod >/dev/null 2>/dev/null; then 23 | phpdismod xdebug 24 | elif [ -f "/usr/local/php/$PHP_VERSION/etc/conf.d/20-xdebug.ini" ]; then 25 | mv "/usr/local/php/$PHP_VERSION/etc/conf.d/20-xdebug.ini" "/usr/local/php/$PHP_VERSION/etc/conf.d/20-xdebug.ini.bak" 26 | fi 27 | } 28 | 29 | # install php 30 | PHP_VERSION="$1" 31 | # `lsb-release` is not necessarily onboard. We parse /etc/os-release instead 32 | DEBIAN_VERSION=$(cat /etc/os-release | grep 'VERSION_CODENAME=' | sed 's/VERSION_CODENAME=//') 33 | if [ -z "${DEBIAN_VERSION}" ]; then 34 | # Example strings: 35 | # VERSION="14.04.6 LTS, Trusty Tahr" 36 | # VERSION="8 (jessie)" 37 | DEBIAN_VERSION=$(cat /etc/os-release | grep 'VERSION=' | grep 'VERSION=' | sed 's/VERSION=//' | sed 's/"[0-9.]\+ *(\?//' | sed 's/)\?"//' | tr '[:upper:]' '[:lower:]' | sed 's/lts, *//' | sed 's/ \+tahr//') 38 | fi 39 | 40 | # @todo use native packages if requested for a specific version and that is the same as available in the os repos 41 | 42 | if [ "${PHP_VERSION}" = default ]; then 43 | echo "Using native PHP packages..." 44 | 45 | if [ "${DEBIAN_VERSION}" = jessie -o "${DEBIAN_VERSION}" = precise -o "${DEBIAN_VERSION}" = trusty ]; then 46 | PHPSUFFIX=5 47 | else 48 | PHPSUFFIX= 49 | fi 50 | if [ "${DEBIAN_VERSION}" = bionic -o "${DEBIAN_VERSION}" = focal -o "${DEBIAN_VERSION}" = stretch -o "${DEBIAN_VERSION}" = buster -o "${DEBIAN_VERSION}" = bullseye ]; then 51 | PINBAPHP=php-pinba 52 | else 53 | PINBAPHP= 54 | fi 55 | # NB: the list of php extensions include some which are not used by our tests bt are required to unlock 56 | # installation of our -dev dependencies across phpunit 4/5/8 57 | # @todo check for mbstring presence in php5 (jessie) packages 58 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 59 | php${PHPSUFFIX} \ 60 | php${PHPSUFFIX}-cli \ 61 | php${PHPSUFFIX}-curl \ 62 | php${PHPSUFFIX}-fpm \ 63 | php${PHPSUFFIX}-mbstring \ 64 | php${PHPSUFFIX}-mysql \ 65 | ${PINBAPHP} \ 66 | php${PHPSUFFIX}-xdebug \ 67 | php${PHPSUFFIX}-xml 68 | else 69 | # on GHA runners ubuntu version, php 7.4 and 8.0 seem to be preinstalled. Remove them if found 70 | for PHP_CURRENT in $(dpkg -l | grep -E 'php.+-common' | awk '{print $2}'); do 71 | if [ "${PHP_CURRENT}" != "php${PHP_VERSION}-common" ]; then 72 | apt-get purge -y "${PHP_CURRENT}" 73 | fi 74 | done 75 | 76 | if [ "${PHP_VERSION}" = 5.3 -o "${PHP_VERSION}" = 5.4 -o "${PHP_VERSION}" = 5.5 ]; then 77 | echo "Using PHP from shivammathur/php5-ubuntu..." 78 | 79 | # @todo this set of packages has only been tested on Bionic, Focal and Jammy so far 80 | if [ "${DEBIAN_VERSION}" = jammy ]; then 81 | ENCHANTSUFFIX='-2' 82 | fi 83 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 84 | curl \ 85 | enchant${ENCHANTSUFFIX} \ 86 | imagemagick \ 87 | libc-client2007e \ 88 | libcurl3-gnutls \ 89 | libmcrypt4 \ 90 | libodbc1 \ 91 | libpq5 \ 92 | libqdbm14 \ 93 | libtinfo5 \ 94 | libxpm4 \ 95 | libxslt1.1 \ 96 | mysql-common \ 97 | zstd 98 | 99 | if [ ! -d /usr/include/php ]; then mkdir -p /usr/include/php; fi 100 | 101 | set +e 102 | curl -sSL https://github.com/shivammathur/php5-ubuntu/releases/latest/download/install.sh | bash -s "${PHP_VERSION}" 103 | set -e 104 | 105 | # we have to do this as the init script we get for starting/stopping php-fpm seems to be faulty... 106 | pkill php-fpm 107 | rm -rf "/usr/local/php/${PHP_VERSION}/var/run" 108 | ln -s "/var/run/php" "/usr/local/php/${PHP_VERSION}/var/run" 109 | # set up the minimal php-fpm config we need 110 | echo 'listen = /run/php/php-fpm.sock' >> "/usr/local/php/${PHP_VERSION}/etc/php-fpm.conf" 111 | # the user running apache will be different in GHA and local VMS. We just open fully perms on the fpm socket 112 | echo 'listen.mode = 0666' >> "/usr/local/php/${PHP_VERSION}/etc/php-fpm.conf" 113 | # as well as the conf to enable php-fpm in apache 114 | cp "$SCRIPT_DIR/../config/apache_phpfpm_proxyfcgi" "/etc/apache2/conf-available/php${PHP_VERSION}-fpm.conf" 115 | else 116 | echo "Using PHP packages from ondrej/php..." 117 | 118 | DEBIAN_FRONTEND=noninteractive apt-get install -y language-pack-en-base software-properties-common 119 | LC_ALL=en_US.UTF-8 add-apt-repository ppa:ondrej/php 120 | apt-get update 121 | 122 | # @todo check if there is a pinba module, taking care to install it from ondrej, not from the os / a different php version 123 | 124 | # NB: the list of php extensions include some which are not used by our tests bt are required to unlock 125 | # installation of our -dev dependencies across phpunit 4/5/8 126 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 127 | php${PHP_VERSION} \ 128 | php${PHP_VERSION}-cli \ 129 | php${PHP_VERSION}-curl \ 130 | php${PHP_VERSION}-fpm \ 131 | php${PHP_VERSION}-mbstring \ 132 | php${PHP_VERSION}-mysql \ 133 | php${PHP_VERSION}-xdebug \ 134 | php${PHP_VERSION}-xml 135 | 136 | update-alternatives --set php /usr/bin/php${PHP_VERSION} 137 | fi 138 | fi 139 | 140 | PHPVER=$(php -r 'echo implode(".",array_slice(explode(".",PHP_VERSION),0,2));' 2>/dev/null) 141 | 142 | service "php${PHPVER}-fpm" stop || true 143 | 144 | if [ -d "/etc/php/${PHPVER}/fpm" ]; then 145 | configure_php_ini "/etc/php/${PHPVER}/fpm/php.ini" 146 | elif [ -f "/usr/local/php/${PHPVER}/etc/php.ini" ]; then 147 | configure_php_ini "/usr/local/php/${PHPVER}/etc/php.ini" 148 | fi 149 | 150 | # use a nice name for the php-fpm service, so that it does not depend on php version running. Try to make that work 151 | # both for docker and VMs 152 | if [ -f "/etc/init.d/php${PHPVER}-fpm" ]; then 153 | ln -s "/etc/init.d/php${PHPVER}-fpm" /etc/init.d/php-fpm 154 | fi 155 | if [ -f "/lib/systemd/system/php${PHPVER}-fpm.service" ]; then 156 | ln -s "/lib/systemd/system/php${PHPVER}-fpm.service" /lib/systemd/system/php-fpm.service 157 | if [ ! -f /.dockerenv ]; then 158 | systemctl daemon-reload 159 | fi 160 | fi 161 | 162 | # @todo shall we configure php-fpm? 163 | 164 | service php-fpm start 165 | 166 | # reconfigure apache (if installed). Sadly, php will switch on mod-php and mpm_prefork at install time... 167 | if [ -n "$(dpkg --list | grep apache)" ]; then 168 | echo "Reconfiguring Apache..." 169 | if [ -n "$(ls /etc/apache2/mods-enabled/php* 2>/dev/null)" ]; then 170 | rm /etc/apache2/mods-enabled/php* 171 | fi 172 | a2dismod mpm_prefork 173 | a2enmod mpm_event 174 | a2enconf "php${PHPVER}-fpm" 175 | service apache2 restart 176 | fi 177 | 178 | echo "Done installing PHP" 179 | -------------------------------------------------------------------------------- /tests/ci/images/php/setup/setup_pinba.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # @todo test also anchorfree/pinba2 6 | docker pull tony2001/pinba:latest 7 | docker run tony2001/pinba & 8 | -------------------------------------------------------------------------------- /tests/ci/images/php/setup/setup_pinboard.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | git clone https://github.com/intaro/pinboard.git 6 | cd pinboard 7 | composer install 8 | 9 | # @todo setup vhosts, create db schema 10 | -------------------------------------------------------------------------------- /tests/ci/images/pinba/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tony2001/pinba 2 | 3 | # @todo test: can't we just alter mysql config to allow network access with `root` account? 4 | 5 | # Copy all the required config files 6 | COPY setup/* /root/build/ 7 | 8 | RUN apt-get update && \ 9 | #DEBIAN_FRONTEND=noninteractive apt-get -y upgrade && \ 10 | DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-client 11 | 12 | COPY entrypoint.sh /root/ 13 | RUN chmod 755 /root/entrypoint*.sh 14 | 15 | ENTRYPOINT ["/root/entrypoint.sh"] 16 | CMD ["mysqld"] 17 | -------------------------------------------------------------------------------- /tests/ci/images/pinba/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "[$(date)] Bootstrapping MySQL..." 4 | 5 | clean_up() { 6 | # Perform program exit housekeeping 7 | echo "[$(date)] Stopping the service..." 8 | kill -s TERM "$pid" 9 | wait "$pid" 10 | echo "[$(date)] Exiting" 11 | exit 12 | } 13 | 14 | trap clean_up TERM 15 | 16 | # cmd line from the original image. The original dockerfile and entrypoints are unknown... 17 | /local/mysql/bin/mysqld --basedir=/local/mysql --datadir=/local/mysql/data --plugin-dir=/local/mysql/lib/plugin --user=mysql --log-error=/local/mysql/var/mysqld.log --pid-file=/local/mysql/data/mysqld.pid --socket=/local/mysql/var/mysql.sock & 18 | pid=$! 19 | 20 | # wait until mysql is ready to accept connections over the network before saying bootstrap is finished 21 | which mysqladmin 2>/dev/null 22 | if [ $? -eq 0 ]; then 23 | while ! mysqladmin ping -h 127.0.0.1 --silent; do 24 | sleep 1 25 | done 26 | fi 27 | 28 | # add a mysql user we can use from other containers 29 | # @todo move this to the build step 30 | mysql -h 127.0.0.1 < /root/build/mysql_init.sql 31 | 32 | echo "[$(date)] Bootstrap finished" 33 | 34 | tail -f /dev/null & 35 | child=$! 36 | wait "$child" 37 | -------------------------------------------------------------------------------- /tests/ci/images/pinba/setup/mysql_init.sql: -------------------------------------------------------------------------------- 1 | CREATE USER 'pinba'@'%' IDENTIFIED BY 'pinba'; 2 | GRANT ALL ON *.* TO 'pinba'@'%' WITH GRANT OPTION; 3 | DELETE FROM mysql.user WHERE user=''; 4 | FLUSH PRIVILEGES; 5 | -------------------------------------------------------------------------------- /tests/ci/images/pinba2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM anchorfree/pinba2 2 | 3 | # source of the base image: https://github.com/badoo/pinba2/blob/master/Dockerfile 4 | 5 | # Copy all the required config files 6 | COPY setup/* /root/build/ 7 | 8 | RUN cd /root/build/ && \ 9 | chmod 755 *.sh && \ 10 | ./setup_mysql.sh 11 | 12 | COPY entrypoint.sh /root/ 13 | RUN chmod 755 /root/entrypoint*.sh 14 | 15 | ENTRYPOINT ["/root/entrypoint.sh"] 16 | #CMD ["mysqld"] 17 | -------------------------------------------------------------------------------- /tests/ci/images/pinba2/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "[$(date)] Bootstrapping MySQL..." 4 | 5 | clean_up() { 6 | # Perform program exit housekeeping 7 | echo "[$(date)] Stopping the service..." 8 | kill -s TERM "$pid" 9 | wait "$pid" 10 | echo "[$(date)] Exiting" 11 | exit 12 | } 13 | 14 | echo "[$(date)] Starting mysqld..." 15 | 16 | trap clean_up TERM 17 | 18 | mysqld -u mysql & 19 | pid=$! 20 | 21 | echo "[$(date)] Bootstrap finished" 22 | 23 | tail -f /dev/null & 24 | child=$! 25 | wait "$child" 26 | -------------------------------------------------------------------------------- /tests/ci/images/pinba2/setup/mysql_init.sql: -------------------------------------------------------------------------------- 1 | CREATE USER 'pinba'@'%' IDENTIFIED BY 'pinba'; 2 | GRANT ALL ON *.* TO 'pinba'@'%' WITH GRANT OPTION; 3 | FLUSH PRIVILEGES; 4 | 5 | USE pinba; 6 | 7 | CREATE TABLE `report_by_script_name` ( 8 | `script` varchar(64) NOT NULL, 9 | `req_count` int(10) unsigned NOT NULL, 10 | `req_per_sec` float NOT NULL, 11 | `req_percent` float, 12 | `req_time_total` float NOT NULL, 13 | `req_time_per_sec` float NOT NULL, 14 | `req_time_percent` float, 15 | `ru_utime_total` float NOT NULL, 16 | `ru_utime_per_sec` float NOT NULL, 17 | `ru_utime_percent` float, 18 | `ru_stime_total` float NOT NULL, 19 | `ru_stime_per_sec` float NOT NULL, 20 | `ru_stime_percent` float, 21 | `traffic_total` bigint(20) unsigned NOT NULL, 22 | `traffic_per_sec` float NOT NULL, 23 | `traffic_percent` float, 24 | `memory_footprint` bigint(20) NOT NULL, 25 | `memory_per_sec` float NOT NULL, 26 | `memory_percent` float 27 | ) ENGINE=PINBA DEFAULT CHARSET=utf8 COMMENT='v2/request/60/~script/no_percentiles/no_filters'; 28 | -------------------------------------------------------------------------------- /tests/ci/images/pinba2/setup/setup_mysql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Let the original code set up mysql, but not leave it running 6 | sed -i '$ d' /usr/local/bin/docker-entrypoint.sh 7 | 8 | if grep -F -q MYSQL_INIT_PATH /usr/local/bin/docker-entrypoint.sh >/dev/null 2>/dev/null; then 9 | export MYSQL_INIT_PATH=/root/build/mysql_init.sql 10 | /usr/local/bin/docker-entrypoint.sh mysqld 11 | else 12 | /usr/local/bin/docker-entrypoint.sh mysqld 13 | 14 | # Now add our own stuff: start mysql again 15 | mysqld --skip-networking -umysql & 16 | # legen.... wait for it (original comment ;-) 17 | for i in {10..0}; do 18 | if echo 'SELECT 1' | mysql &>/dev/null; then 19 | break 20 | fi 21 | #echo 'MySQL init process in progress...' 22 | sleep 1 23 | done 24 | if [ "$i" = 0 ]; then 25 | echo >&2 'MySQL init process failed.' 26 | exit 1 27 | fi 28 | 29 | # Execute extra sql 30 | mysql --protocol=socket -uroot < /root/build/mysql_init.sql 31 | 32 | # Shut it down 33 | pkill --signal term mysqld 34 | fi 35 | -------------------------------------------------------------------------------- /tests/ci/vm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Manage the whole set of containers and run tests without having to learn Docker 4 | 5 | # @todo when running test w. codecov, enable xdebug on the fly 6 | # @todo simplify/verify cli options, esp. -d and -f 7 | # @todo reimplement resetdb (is it useful? pinba tables are read-only anyway...) 8 | 9 | # vars 10 | DOCKER_COMPOSE=docker-compose 11 | INTERACTIVE= 12 | REBUILD=false 13 | RECREATE=false 14 | COVERAGE_OPTION= 15 | SILENT=false 16 | TTY= 17 | VERBOSITY= 18 | WEB_USER=docker 19 | WEB_CONTAINER=${COMPOSE_PROJECT_NAME:-pinbapolyfill}-php 20 | 21 | help() { 22 | printf "Usage: vm.sh [OPTIONS] COMMAND [OPTARGS] 23 | 24 | Manages the Test Environment Docker Stack 25 | 26 | Commands: 27 | build build or rebuild the complete set of containers and set up eZ. Leaves the stack running 28 | cleanup WHAT remove temporary data/logs/caches/etc... CATEGORY can be any of: 29 | - containers removes all the project's containers and their images 30 | - dead-images removes unused docker images. Can be quite beneficial to free up space 31 | - docker-logs NB: for this to work, you'll need to run this script as root, eg. with sudo -E 32 | - vendors removes composers vendors and locks file 33 | enter run a shell in the test container 34 | exec \$cmd execute a single shell command in the test container 35 | images [\$svc] list container images 36 | kill [\$svc] kill containers 37 | logs [\$svc] view output from containers 38 | pause [\$svc] pause the containers 39 | ps [\$svc] show the status of running containers 40 | runtests [\$suite] execute the test suite using the test container (or a single test scenario eg. Tests/phpunit/01GatherTest.php) 41 | services list docker-compose services 42 | start [\$svc] start the complete set of containers 43 | stop [\$svc] stop the complete set of containers 44 | top [\$svc] display the running container processes 45 | unpause [\$svc] unpause the containers 46 | 47 | Options: 48 | -h print help 49 | -v verbose mode 50 | 51 | Advanced Options: 52 | -d discard existing containers and force them to rebuild from scratch - when running 'build' 53 | -f freshen: force app set up via resetting containers to clean-build status besides updating them if needed - when running 'build', 'start' 54 | -i interactive - when running 'exec' 55 | -o FOLDER generate code coverage reports in FOLDER - when running 'runtests'. Needs xdebug to be enabled in code-coverage mode 56 | -t allocate a pseudo-TTY - when running 'exec' 57 | 58 | Env vars: TESTSTACK_UBUNTU_VERSION (focal), TESTSTACK_PHP_VERSION (default), TESTSTACK_WEB_PORT (80) 59 | " 60 | } 61 | 62 | create_compose_command() { 63 | DOCKER_TESTSTACK_QUIET=${DOCKER_COMPOSE/ --verbose/} 64 | } 65 | 66 | build() { 67 | echo "[$(date)] Stopping running Containers..." 68 | 69 | ${DOCKER_COMPOSE} stop 70 | 71 | if [ ${REBUILD} = 'true' ]; then 72 | echo "[$(date)] Removing existing Containers..." 73 | 74 | ${DOCKER_COMPOSE} rm -f 75 | fi 76 | 77 | echo "[$(date)] Building Containers..." 78 | 79 | ${DOCKER_COMPOSE} build || exit $? 80 | 81 | echo "[$(date)] Starting Containers..." 82 | 83 | if [ ${RECREATE} = 'true' ]; then 84 | ${DOCKER_COMPOSE} up -d --force-recreate 85 | else 86 | ${DOCKER_COMPOSE} up -d 87 | fi 88 | RETCODE=$? 89 | 90 | if [ $RETCODE -eq 0 ]; then 91 | echo "[$(date)] Build finished" 92 | else 93 | echo "[$(date)] Build finished. Exit code: ${RETCODE}" 94 | fi 95 | 96 | exit ${RETCODE} 97 | } 98 | 99 | check_requirements() { 100 | which docker >/dev/null 2>&1 101 | if [ $? -ne 0 ]; then 102 | printf "\n\e[31mPlease install docker & add it to \$PATH\e[0m\n\n" >&2 103 | exit 1 104 | fi 105 | 106 | which docker-compose >/dev/null 2>&1 107 | if [ $? -ne 0 ]; then 108 | printf "\n\e[31mPlease install docker-compose & add it to \$PATH\e[0m\n\n" >&2 109 | exit 1 110 | fi 111 | } 112 | 113 | # @todo loop over all args instead of allowing just one 114 | cleanup() { 115 | case "${1}" in 116 | containers) 117 | if [ ${SILENT} != true ]; then 118 | echo "Do you really want to delete all project containers and their images?" 119 | select yn in "Yes" "No"; do 120 | case $yn in 121 | Yes ) break ;; 122 | No ) exit 1 ;; 123 | esac 124 | done 125 | fi 126 | 127 | ${DOCKER_COMPOSE} down --rmi all 128 | ;; 129 | docker-images | dead-images) 130 | cleanup_dead_docker_images 131 | ;; 132 | docker-logs) 133 | for CONTAINER in $(${DOCKER_TESTSTACK_QUIET} ps -q) 134 | do 135 | LOGFILE=$(docker inspect --format='{{.LogPath}}' ${CONTAINER}) 136 | if [ -n "${LOGFILE}" ]; then 137 | echo "" > ${LOGFILE} 138 | fi 139 | done 140 | ;; 141 | vendors) 142 | # we are executing in the project's root, thanks to a cd call at the start 143 | if [ -f composer.lock ]; then rm composer.lock; fi 144 | if [ -d vendors ]; then rm -rf vendors; fi 145 | ;; 146 | *) 147 | printf "\n\e[31mERROR:\e[0m unknown cleanup target ${1}\n\n" >&2 148 | help 149 | exit 1 150 | ;; 151 | esac 152 | } 153 | 154 | cleanup_dead_docker_images() { 155 | echo "[$(date)] Removing unused Docker images from disk..." 156 | DEAD_IMAGES=$(docker images | grep "" | awk "{print \$3}") 157 | if [ -n "${DEAD_IMAGES}" ]; then 158 | docker rmi ${DEAD_IMAGES} 159 | fi 160 | } 161 | 162 | load_config() { 163 | export CONTAINER_USER_UID="$(id -u)" 164 | export CONTAINER_USER_GID="$(id -g)" 165 | create_compose_command 166 | } 167 | 168 | # @todo move to a function 169 | # @todo allow parsing of cli options after args -- see fe. https://medium.com/@Drew_Stokes/bash-argument-parsing-54f3b81a6a8f 170 | while getopts ":dfhio:tvy" opt 171 | do 172 | case $opt in 173 | d) 174 | REBUILD=true 175 | ;; 176 | f) 177 | RECREATE=true 178 | ;; 179 | h) 180 | help 181 | exit 0 182 | ;; 183 | i) 184 | INTERACTIVE='-i' 185 | ;; 186 | o) 187 | COVERAGE_OPTION="--coverage-html=${OPTARG}" 188 | ;; 189 | t) 190 | TTY='-t' 191 | ;; 192 | v) 193 | VERBOSITY=-v 194 | DOCKER_COMPOSE="${DOCKER_COMPOSE} --verbose" 195 | ;; 196 | y) 197 | SILENT=true 198 | ;; 199 | \?) 200 | printf "\n\e[31mERROR:\e[0m unknown option '-${OPTARG}'\n\n" >&2 201 | help 202 | exit 1 203 | ;; 204 | esac 205 | done 206 | shift $((OPTIND-1)) 207 | 208 | COMMAND=$1 209 | 210 | check_requirements 211 | 212 | load_config 213 | 214 | cd "$(dirname -- "${BASH_SOURCE[0]}"})" 215 | 216 | case "${COMMAND}" in 217 | build) 218 | build 219 | ;; 220 | 221 | cleanup) 222 | # @todo allow to pass in many cleanup targets in one go 223 | cleanup "${2}" 224 | ;; 225 | 226 | config) 227 | ${DOCKER_COMPOSE} config ${2} 228 | ;; 229 | 230 | 231 | # courtesy command alias - same as 'ps' 232 | containers) 233 | ${DOCKER_COMPOSE} ps ${2} 234 | ;; 235 | 236 | enter | shell | cli) 237 | docker exec -ti "${WEB_CONTAINER}" su "${WEB_USER}" 238 | ;; 239 | 240 | exec) 241 | # scary line ? found it at https://stackoverflow.com/questions/12343227/escaping-bash-function-arguments-for-use-by-su-c 242 | shift 243 | docker exec $INTERACTIVE $TTY "${WEB_CONTAINER}" su "${WEB_USER}" -c '"$0" "$@"' -- exec "$@" 244 | ;; 245 | 246 | images) 247 | ${DOCKER_COMPOSE} images ${2} 248 | ;; 249 | 250 | kill) 251 | ${DOCKER_COMPOSE} kill ${2} 252 | ;; 253 | 254 | logs) 255 | ${DOCKER_COMPOSE} logs ${2} 256 | ;; 257 | 258 | pause) 259 | ${DOCKER_COMPOSE} pause ${2} 260 | ;; 261 | 262 | ps) 263 | ${DOCKER_COMPOSE} ps ${2} 264 | ;; 265 | 266 | #resetdb) 267 | # # @todo allow this to be run from within the test container too 268 | # # q: do we need -ti ? 269 | # docker exec "${WEB_CONTAINER}" su "${WEB_USER}" -c "../teststack/bin/create-db.sh" 270 | #;; 271 | 272 | runtests) 273 | shift 274 | # q: do we need -ti ? 275 | docker exec "${WEB_CONTAINER}" su "${WEB_USER}" -c '"$0" "$@"' -- ./vendor/bin/phpunit ${COVERAGE_OPTION} ${VERBOSITY} tests 276 | ;; 277 | 278 | services) 279 | ${DOCKER_COMPOSE} config --services | sort 280 | ;; 281 | 282 | start) 283 | if [ ${RECREATE} = 'true' ]; then 284 | ${DOCKER_COMPOSE} up -d --force-recreate 285 | else 286 | ${DOCKER_COMPOSE} up -d ${2} 287 | fi 288 | ;; 289 | 290 | stop) 291 | ${DOCKER_COMPOSE} stop ${2} 292 | ;; 293 | 294 | top) 295 | ${DOCKER_COMPOSE} top ${2} 296 | ;; 297 | 298 | unpause) 299 | ${DOCKER_COMPOSE} unpause ${2} 300 | ;; 301 | 302 | *) 303 | printf "\n\e[31mERROR:\e[0m unknown command '${COMMAND}'\n\n" >&2 304 | help 305 | exit 1 306 | ;; 307 | esac 308 | -------------------------------------------------------------------------------- /tests/phpunit_coverage.php: -------------------------------------------------------------------------------- 1 |