From fccfff12411a5603973ddaac700b6481a5b4f63e Mon Sep 17 00:00:00 2001 From: Timur Shagiakhmetov Date: Thu, 10 Jan 2019 17:27:23 +0000 Subject: [PATCH] added project files --- .gitignore | 3 + .scrutinizer.yml | 34 ++ .travis.yml | 20 + Dockerfile | 13 + DockerfileHHVM | 10 + DockerfileTidyWays | 18 + DockerfileUprofiler | 19 + LICENSE | 21 + README.md | 179 ++++++ bin/example.php | 71 +++ bin/install.php | 15 + bin/install_data/mysqli/source.sql | 12 + bin/install_data/pdo_mysql/source.sql | 12 + bin/install_data/pdo_pgsql/source.sql | 11 + bin/install_data/pdo_sqlite/source.sql | 10 + composer.json | 29 + images/liveprof_logo.png | Bin 0 -> 20703 bytes phpunit.xml | 26 + src/Badoo/LiveProfiler/DataPacker.php | 29 + .../LiveProfiler/DataPackerInterface.php | 22 + src/Badoo/LiveProfiler/LiveProfiler.php | 439 ++++++++++++++ src/Badoo/LiveProfiler/Logger.php | 61 ++ tests/bootstrap.php | 2 + tests/unit/Badoo/BaseTestCase.php | 55 ++ .../Badoo/LiveProfiler/DataPackerTest.php | 34 ++ .../Badoo/LiveProfiler/LiveProfilerTest.php | 544 ++++++++++++++++++ tests/unit/Badoo/LiveProfiler/LoggerTest.php | 33 ++ 27 files changed, 1722 insertions(+) create mode 100644 .gitignore create mode 100644 .scrutinizer.yml create mode 100644 .travis.yml create mode 100644 Dockerfile create mode 100644 DockerfileHHVM create mode 100644 DockerfileTidyWays create mode 100644 DockerfileUprofiler create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bin/example.php create mode 100644 bin/install.php create mode 100644 bin/install_data/mysqli/source.sql create mode 100644 bin/install_data/pdo_mysql/source.sql create mode 100644 bin/install_data/pdo_pgsql/source.sql create mode 100644 bin/install_data/pdo_sqlite/source.sql create mode 100644 composer.json create mode 100644 images/liveprof_logo.png create mode 100644 phpunit.xml create mode 100644 src/Badoo/LiveProfiler/DataPacker.php create mode 100644 src/Badoo/LiveProfiler/DataPackerInterface.php create mode 100644 src/Badoo/LiveProfiler/LiveProfiler.php create mode 100644 src/Badoo/LiveProfiler/Logger.php create mode 100644 tests/bootstrap.php create mode 100644 tests/unit/Badoo/BaseTestCase.php create mode 100644 tests/unit/Badoo/LiveProfiler/DataPackerTest.php create mode 100644 tests/unit/Badoo/LiveProfiler/LiveProfilerTest.php create mode 100644 tests/unit/Badoo/LiveProfiler/LoggerTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f38912 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +vendor +composer.lock diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..0ac8039 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,34 @@ +before_commands: + - "composer install --no-dev --prefer-source" + +tools: + external_code_coverage: + timeout: 1200 + php_code_coverage: + enabled: true + php_code_sniffer: + enabled: true + config: + standard: PSR2 + filter: + paths: ["src/*"] + php_cpd: + enabled: true + excluded_dirs: ["ide-stubs", "tests", 'vendor'] + php_cs_fixer: + enabled: true + config: + level: all + filter: + paths: ["src/*"] + php_loc: + enabled: true + excluded_dirs: ["ide-stubs", "tests", 'vendor'] + php_pdepend: + enabled: true + excluded_dirs: ["ide-stubs", "tests", 'vendor'] + php_analyzer: + enabled: true + filter: + paths: ["src/*"] + sensiolabs_security_checker: true diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f970a93 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: php +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + - hhvm + +before_script: composer install +script: vendor/bin/phpunit --verbose --colors --coverage-clover=coverage.xml + +after_success: + - bash <(curl -s https://codecov.io/bash) + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover coverage.xml + +notifications: + email: "timur.shagiakhmetov@corp.badoo.com" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..889b46a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM php:5.4 +MAINTAINER Timur Shagiakhmetov + +COPY . /app +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends git unzip \ +&& curl --silent --show-error https://getcomposer.org/installer | php \ +&& ./composer.phar -q install \ +&& echo 'date.timezone = Europe/London' >> /usr/local/etc/php/php.ini \ +&& pecl install xhprof-0.9.4 && docker-php-ext-enable xhprof # xhprof profiler installation + +CMD ["php", "/app/bin/example.php"] diff --git a/DockerfileHHVM b/DockerfileHHVM new file mode 100644 index 0000000..afb79ae --- /dev/null +++ b/DockerfileHHVM @@ -0,0 +1,10 @@ +FROM hhvm/hhvm:latest +MAINTAINER Timur Shagiakhmetov + +COPY . /app +WORKDIR /app + +RUN curl -sS https://getcomposer.org/installer | hhvm --php \ +&& hhvm ./composer.phar -q install + +CMD ["hhvm", "/app/bin/example.php"] diff --git a/DockerfileTidyWays b/DockerfileTidyWays new file mode 100644 index 0000000..f5a8e1a --- /dev/null +++ b/DockerfileTidyWays @@ -0,0 +1,18 @@ +FROM php:7.0 +MAINTAINER Timur Shagiakhmetov + +COPY . /app +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends git unzip \ +&& curl --silent --show-error https://getcomposer.org/installer | php \ +&& ./composer.phar -q install + +# tideways profiler installation +RUN git clone https://github.com/tideways/php-profiler-extension.git /tmp/xhptof +WORKDIR /tmp/xhptof +RUN phpize && ./configure && make && make install \ +&& echo "extension=tideways_xhprof.so" >> /usr/local/etc/php/conf.d/20-xhprof.ini \ +&& echo "xhprof.output_dir='/usr/src/myapp/xhprof'" >> /usr/local/etc/php/conf.d/20-xhprof.ini + +CMD ["php", "/app/bin/example.php"] diff --git a/DockerfileUprofiler b/DockerfileUprofiler new file mode 100644 index 0000000..1c4708b --- /dev/null +++ b/DockerfileUprofiler @@ -0,0 +1,19 @@ +FROM php:5.4 +MAINTAINER Timur Shagiakhmetov + +COPY . /app +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends git unzip \ +&& curl --silent --show-error https://getcomposer.org/installer | php \ +&& ./composer.phar -q install \ +&& echo 'date.timezone = Europe/London' >> /usr/local/etc/php/php.ini + +# uprofiler installation +RUN git clone https://github.com/FriendsOfPHP/uprofiler.git /tmp/uprofiler +WORKDIR /tmp/uprofiler/extension +RUN phpize && ./configure && make && make install \ +&& echo "extension=uprofiler.so" >> /usr/local/etc/php/conf.d/20-uprofiler.ini \ +&& echo "uprofiler.output_dir='/usr/src/myapp/uprofiler'" >> /usr/local/etc/php/conf.d/20-uprofiler.ini + +CMD ["php", "/app/bin/example.php"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67af520 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Badoo Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9cebed --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +Live Profiler +================ + +![logo](images/liveprof_logo.png "logo") + +Live Profiler is a system-wide performance monitoring system in use at Badoo that is built on top of [XHProf](http://pecl.php.net/package/xhprof) or its forks ([Uprofiler](https://github.com/FriendsOfPHP/uprofiler) or [Tideways](https://github.com/tideways/php-profiler-extension)). +Live Profiler continually gathers function-level profiler data from production tier by running a sample of page requests under XHProf. + +[Live profiler UI](https://github.com/badoo/liveprof-ui) then aggregates the profile data corresponding to individual requests by various dimensions such a time, page type, and can help answer a variety of questions such as: +What is the function-level profile for a specific page? +How expensive is function "foo" across all pages, or on a specific page? +What functions regressed most in the last day/week/month? +What is the historical trend for execution time of a page/function? and so on. + +System Requirements +=================== + +* PHP version 5.4 or later / hhvm version 3.25.0 or later +* One of [XHProf](http://pecl.php.net/package/xhprof), + [Uprofiler](https://github.com/FriendsOfPHP/uprofiler) or + [Tideways](https://github.com/tideways/php-profiler-extension) to actually profile the data. + You can use other profiler which returns data in the follow format: + ```php + $data = [ + [ + 'parent_method==>child_method' => [ + 'param' => 'value' + ] + ] + ]; + ``` +* Database extension to save profiling results + +Installation +============ + +* You can install Live Profiler via [Composer](https://getcomposer.org/): + +```bash +php composer.phar require badoo/liveprof +``` + +* Prepare a database server. You can use any driver described [here](https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/configuration.html#configuration) or implement the custom one +* Run a script to configure database. This script creates "details" table + +```bash +LIVE_PROFILER_CONNECTION_URL=mysql://db_user:db_password@db_mysql:3306/Profiler?charset=utf8 php vendor/badoo/liveprof/bin/install.php +``` + +* Init a profiler before working code in the project entry point (usually public/index.php). + +You should add a profiler call before your code to start profiling with default parameters: +```php +start(); +// Code is here +``` + +There is a full list of methods you can use to change options: +```php +setConnectionString('mysql://db_user:db_password@db_mysql:3306/Profiler?charset=utf8') // optional, you can also set the connection url in the environment variable LIVE_PROFILER_CONNECTION_URL + ->setApp('Site1') // optional, current app name to use one profiler in several apps, "Default" by default + ->setLabel('users') // optional, the request name, by default the url path or script name in cli + ->setDivider(700) // optional, profiling starts for 1 of 700 requests with the same app and label, 1000 by default + ->setTotalDivider(7000) // optional, profiling starts for 1 of 7000 requests with forces label "All", 10000 by default + ->setLogger($Logger) // optional, a custom logger implemented \Psr\Log\LoggerInterface + ->setConnection($Connection) // optional, a custom instance of \Doctrine\DBAL\Connection if you can't use the connection url + ->setDataPacker($DatePacker) // optional, a class implemented \Badoo\LiveProfiler\DataPackerInterface to convert array into string + ->setStartCallback($profiler_start_callback) // optional, set it if you use custom profiler + ->setEndCallback($profiler_profiler_callback) // optional, set it if you use custom profiler + ->start(); +``` + +If you want to change the Label during running (for instance, after you got some information in the router or controller) you can call: +```php +getLabel(); +\Badoo\LiveProfiler\LiveProfiler::getInstance()->setLabel($current_label . $number); +``` + +if you don't want to save profiling result you can reset it anytime: +```php +reset(); +``` + +After script ends it will call `\Badoo\LiveProfiler\LiveProfiler::getInstance()->end();` on shutdown, but you can call it explicitly after working code. + +Environment Variables +===================== + +`LIVE_PROFILER_CONNECTION_URL`: [url](https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/configuration.html#configuration) for the database connection + +Work flow +========= + +Live profiler allows to run profiling with custom frequency (for instance 1 of 1000 requests) grouped by app name ('Default' by default) and custom label (by default it's the url path or script name). + +It's important to calculate **the request divider** properly to have enough data for aggregation. You should divide daily request count to have approximately 1 profile per minute. +For instance, if you have 1M requests a day for the page /users the divider should be 1000000/(60*24) = 694, so divider = 700 is enough. + +Also you have to calculate **a total divider** for all request profiling. It's important to control the whole system health. It can be calculated the same way as a divider calculation for particularly request, +but in this case you should use count of all daily requests. For instance, if you have 10M requests a day - total_divider=10000000/(60*24) = 6940, so total_divider = 7000 is enough. + +The profiler automatically detects which profiler extension you have (xhprof, uprofiler or tidyways). You have to set profiler callbacks if you use other profiler. + +You can run the test script in the docker container with **xhprof** extension and look at the example of the server configuration in Dockerfile: +```bash +docker build -t badoo/liveprof . +docker run badoo/liveprof +``` + +or you can build a docker container with **tideways** extension: +```bash +docker build -f DockerfileTidyWays -t badoo/liveprof . +``` + +or **uprofiler** extension: +```bash +docker build -f DockerfileUprofiler -t badoo/liveprof . +``` + +or latest hhvm with included **xhprof** extension: +```bash +docker build -f DockerfileHHVM -t badoo/liveprof . +``` + +If your server has php version 7.0 or later it's better to use [Tideways](https://github.com/tideways/php-profiler-extension) as profiler. + +Steps to install tideways extension: +```bash +git clone https://github.com/tideways/php-profiler-extension.git +cd php-profiler-extension +phpize +./configure +make +make install +echo "extension=tideways_xhprof.so" >> /usr/local/etc/php/conf.d/20-tideways_xhprof.ini +echo "xhprof.output_dir='/tmp/xhprof'" >> /usr/local/etc/php/conf.d/20-tideways_xhprof.ini +``` + +Steps to install uprofiler: +```bash +git clone https://github.com/FriendsOfPHP/uprofiler.git +cd uprofiler/extension/ +phpize +./configure +make +make install +echo "extension=uprofiler.so" >> /usr/local/etc/php/conf.d/20-uprofiler.ini +echo "uprofiler.output_dir='/tmp/uprofiler'" >> /usr/local/etc/php/conf.d/20-uprofiler.ini +``` + +Tests +===== + +Install Live Profiler with dev requirements: +```bash +php composer.phar require --dev badoo/liveprof +``` + +In the project directory, run: +```bash +vendor/bin/phpunit +``` + +License +======= + +This project is licensed under the MIT open source license. \ No newline at end of file diff --git a/bin/example.php b/bin/example.php new file mode 100644 index 0000000..af1a5c8 --- /dev/null +++ b/bin/example.php @@ -0,0 +1,71 @@ + + */ + +if (file_exists(__DIR__ . '/../vendor/autoload.php')) { + require_once __DIR__ . '/../vendor/autoload.php'; +} elseif (file_exists(__DIR__ . '/../../../../vendor/autoload.php')) { + require_once __DIR__ . '/../../../../vendor/autoload.php'; +} + +use Badoo\LiveProfiler\LiveProfiler; + +// Create source database in the memory +$Profiler = new LiveProfiler('sqlite:///:memory:'); +$Profiler->setDivider(1); +$Profiler->createTable(); + +// Run this method before profiled code +$Profiler->start(); + +// Start of profiled code +testCode(1); +// End of profiled code + +// Run this method after profiled code +$profiling_result = $Profiler->end(); +print_r($Profiler->getLastProfileData()); + +echo $profiling_result ? "Prof;ling successfully finished\n" : "Error in profiling\n"; + +/** + * Test functions + * @param int $level + */ +function testCode($level = 2) +{ + getSlower($level); + getFaster($level); +} + +/** + * @param int $level + * @return float + */ +function getSlower($level) +{ + $result = 0; + for ($i = 0; $i < $level; $i++) { + for ($j = 0; $j < 1000000; $j++) { + $result = $j * (1000000 - $j); + } + } + return $result; +} + +/** + * @param int $level + * @return float + */ +function getFaster($level) +{ + $result = 0; + for ($i = 0; $i < (10 - $level); $i++) { + for ($j = 0; $j < 1000000; $j++) { + $result = $i * (1000000 - $i); + } + } + return $result; +} diff --git a/bin/install.php b/bin/install.php new file mode 100644 index 0000000..8cf3222 --- /dev/null +++ b/bin/install.php @@ -0,0 +1,15 @@ + + */ + +if (file_exists(__DIR__ . '/../vendor/autoload.php')) { + require_once __DIR__ . '/../vendor/autoload.php'; +} elseif (file_exists(__DIR__ . '/../../../../vendor/autoload.php')) { + require_once __DIR__ . '/../../../../vendor/autoload.php'; +} + +use Badoo\LiveProfiler\LiveProfiler; + +$Profiler = new LiveProfiler(); +$Profiler->createTable(); diff --git a/bin/install_data/mysqli/source.sql b/bin/install_data/mysqli/source.sql new file mode 100644 index 0000000..d638ea0 --- /dev/null +++ b/bin/install_data/mysqli/source.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS `details` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `app` varchar(32) DEFAULT NULL, + `label` varchar(64) DEFAULT NULL, + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `perfdata` mediumblob, + PRIMARY KEY (`id`), + KEY `timestamp` (`timestamp`), + KEY `app` (`app`), + KEY `label` (`label`), + KEY `timestamp_label_idx` (`timestamp`,`label`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; diff --git a/bin/install_data/pdo_mysql/source.sql b/bin/install_data/pdo_mysql/source.sql new file mode 100644 index 0000000..d638ea0 --- /dev/null +++ b/bin/install_data/pdo_mysql/source.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS `details` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `app` varchar(32) DEFAULT NULL, + `label` varchar(64) DEFAULT NULL, + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `perfdata` mediumblob, + PRIMARY KEY (`id`), + KEY `timestamp` (`timestamp`), + KEY `app` (`app`), + KEY `label` (`label`), + KEY `timestamp_label_idx` (`timestamp`,`label`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; diff --git a/bin/install_data/pdo_pgsql/source.sql b/bin/install_data/pdo_pgsql/source.sql new file mode 100644 index 0000000..6af8c2e --- /dev/null +++ b/bin/install_data/pdo_pgsql/source.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS details ( + id SERIAL NOT NULL PRIMARY KEY, + app CHAR(32) DEFAULT NULL, + label CHAR(64) DEFAULT NULL, + timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + perfdata text +); +CREATE INDEX IF NOT EXISTS timestamp_idx ON details (timestamp); +CREATE INDEX IF NOT EXISTS app_idx ON details (app); +CREATE INDEX IF NOT EXISTS label_idx ON details (label); +CREATE INDEX IF NOT EXISTS timestamp_label_idx ON details (timestamp,label); diff --git a/bin/install_data/pdo_sqlite/source.sql b/bin/install_data/pdo_sqlite/source.sql new file mode 100644 index 0000000..e5c5310 --- /dev/null +++ b/bin/install_data/pdo_sqlite/source.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS details ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + app TEXT DEFAULT NULL, + label TEXT DEFAULT NULL, + timestamp TEXT NOT NULL, + perfdata BLOB +); +CREATE INDEX IF NOT EXISTS app ON details (app); +CREATE INDEX IF NOT EXISTS label ON details (label); +CREATE INDEX IF NOT EXISTS timestamp_label_idx ON details (timestamp,label); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..16634df --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "badoo/liveprof", + "type": "library", + "description": "Live profiler for auto running with custom frequency", + "minimum-stability": "stable", + "license": "MIT", + "authors": [ + { + "name": "Badoo Development" + } + ], + "require": { + "php": ">=5.4", + "doctrine/dbal": "<2.9", + "psr/log": "~1.0", + "ext-json": "*", + "ext-zlib": "*" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "autoload": { + "psr-4": { + "Badoo\\LiveProfiler\\": "src/Badoo/LiveProfiler"} + }, + "autoload-dev": { + "psr-4": {"unit\\Badoo\\": "tests/unit/Badoo"} + } +} diff --git a/images/liveprof_logo.png b/images/liveprof_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..32d9d024ce18ec4c19b904f33123594682574180 GIT binary patch literal 20703 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 Tzr2b-00000NkvXXu0mjf8(+PP literal 0 HcmV?d00001 diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..0ffefc6 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,26 @@ + + + + tests/unit + + + + + src/Badoo + + + diff --git a/src/Badoo/LiveProfiler/DataPacker.php b/src/Badoo/LiveProfiler/DataPacker.php new file mode 100644 index 0000000..e29066a --- /dev/null +++ b/src/Badoo/LiveProfiler/DataPacker.php @@ -0,0 +1,29 @@ + + */ + +namespace Badoo\LiveProfiler; + +class DataPacker implements DataPackerInterface +{ + /** + * @param array $data + * @return string + */ + public function pack(array $data) + { + return json_encode($data); + } + + /** + * @param string $data + * @return array + */ + public function unpack($data) + { + return json_decode($data, true); + } +} diff --git a/src/Badoo/LiveProfiler/DataPackerInterface.php b/src/Badoo/LiveProfiler/DataPackerInterface.php new file mode 100644 index 0000000..c805491 --- /dev/null +++ b/src/Badoo/LiveProfiler/DataPackerInterface.php @@ -0,0 +1,22 @@ + + */ + +namespace Badoo\LiveProfiler; + +interface DataPackerInterface +{ + /** + * @param array $data + * @return string + */ + public function pack(array $data); + + /** + * @param string $data + * @return array + */ + public function unpack($data); +} diff --git a/src/Badoo/LiveProfiler/LiveProfiler.php b/src/Badoo/LiveProfiler/LiveProfiler.php new file mode 100644 index 0000000..75b9eb3 --- /dev/null +++ b/src/Badoo/LiveProfiler/LiveProfiler.php @@ -0,0 +1,439 @@ + + */ + +namespace Badoo\LiveProfiler; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\DBALException; +use Psr\Log\LoggerInterface; + +class LiveProfiler +{ + /** @var LiveProfiler */ + protected static $instance; + /** @var Connection */ + protected $Conn; + /** @var LoggerInterface */ + protected $Logger; + /** @var DataPackerInterface */ + protected $DataPacker; + /** @var string */ + protected $connection_string; + /** @var string */ + protected $app; + /** @var string */ + protected $label; + /** @var string */ + protected $datetime; + /** @var int */ + protected $divider = 1000; + /** @var int */ + protected $total_divider = 10000; + /** @var callable callback to start profiling */ + protected $start_callback; + /** @var callable callback to end profiling */ + protected $end_callback; + /** @var bool */ + protected $is_enabled = false; + /** @var array */ + protected $last_profile_data = []; + + /** + * LiveProfiler constructor. + * @param string $connection_string + */ + public function __construct($connection_string = '') + { + if ($connection_string) { + $this->connection_string = $connection_string; + } else { + $this->connection_string = getenv('LIVE_PROFILER_CONNECTION_URL'); + } + + $this->app = 'Default'; + $this->label = $this->getAutoLabel(); + $this->datetime = date('Y-m-d H:i:s'); + + + $this->detectProfiler(); + $this->Logger = new Logger(); + $this->DataPacker = new DataPacker(); + } + + public static function getInstance($connection_string = '') + { + if (self::$instance === null) { + self::$instance = new static($connection_string); + } + + return self::$instance; + } + + public function start() + { + if ($this->is_enabled) { + return true; + } + + if (null === $this->start_callback) { + return true; + } + + if ($this->needToStart($this->divider)) { + $this->is_enabled = true; + } elseif ($this->needToStart($this->total_divider)) { + $this->is_enabled = true; + $this->label = 'All'; + } + + if ($this->is_enabled) { + register_shutdown_function([$this, 'end']); + call_user_func($this->start_callback); + } + + return true; + } + + /** + * @return bool + */ + public function end() + { + if (!$this->is_enabled) { + return true; + } + + $this->is_enabled = false; + + if (null === $this->end_callback) { + return true; + } + + $data = call_user_func($this->end_callback); + if (!is_array($data)) { + $this->Logger->warning('Invalid profiler data: ' . var_export($data, true)); + return false; + } + + $this->last_profile_data = $data; + $packed_data = $this->DataPacker->pack($data); + + $result = false; + try { + $result = $this->save($this->app, $this->label, $this->datetime, $packed_data); + } catch (DBALException $Ex) { + $this->Logger->error('Error in insertion profile data: ' . $Ex->getMessage()); + } + + if (!$result) { + $this->Logger->warning('Can\'t insert profile data'); + } + + return $result; + } + + public function detectProfiler() + { + if (function_exists('xhprof_enable')) { + return $this->useXhprof(); + } + + if (function_exists('tideways_xhprof_enable')) { + return $this->useTidyWays(); + } + + if (function_exists('uprofiler_enable')) { + return $this->useUprofiler(); + } + + return false; + } + + public function useXhprof() + { + if ($this->is_enabled) { + $this->Logger->warning('can\'t change profiler after profiling started'); + return false; + } + + $this->start_callback = function () { + xhprof_enable(XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_CPU); + }; + + $this->end_callback = function () { + return xhprof_disable(); + }; + + return true; + } + + public function useTidyWays() + { + if ($this->is_enabled) { + $this->Logger->warning('can\'t change profiler after profiling started'); + return false; + } + + $this->start_callback = function () { + tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_MEMORY | TIDEWAYS_XHPROF_FLAGS_CPU); + }; + + $this->end_callback = function () { + return tideways_xhprof_disable(); + }; + + return true; + } + + public function useUprofiler() + { + if ($this->is_enabled) { + $this->Logger->warning('can\'t change profiler after profiling started'); + return false; + } + + $this->start_callback = function () { + uprofiler_enable(UPROFILER_FLAGS_CPU | UPROFILER_FLAGS_MEMORY); + }; + + $this->end_callback = function () { + return uprofiler_disable(); + }; + + return true; + } + + /** + * @return bool + */ + public function reset() + { + if ($this->is_enabled) { + call_user_func($this->end_callback); + $this->is_enabled = false; + } + + return true; + } + + /** + * @param string $app + * @return $this + */ + public function setApp($app) + { + $this->app = $app; + return $this; + } + + /** + * @return string + */ + public function getApp() + { + return $this->app; + } + + /** + * @param string $label + * @return $this + */ + public function setLabel($label) + { + $this->label = $label; + return $this; + } + + /** + * @return string + */ + public function getLabel() + { + return $this->label; + } + + /** + * @param string $datetime + * @return $this + */ + public function setDateTime($datetime) + { + $this->datetime = $datetime; + return $this; + } + + /** + * @return string + */ + public function getDateTime() + { + return $this->datetime; + } + + /** + * @param int $divider + * @return $this + */ + public function setDivider($divider) + { + $this->divider = $divider; + return $this; + } + + /** + * @param int $total_divider + * @return $this + */ + public function setTotalDivider($total_divider) + { + $this->total_divider = $total_divider; + return $this; + } + + /** + * @param \Closure $start_callback + * @return $this + */ + public function setStartCallback(\Closure $start_callback) + { + $this->start_callback = $start_callback; + return $this; + } + + /** + * @param \Closure $end_callback + * @return $this + */ + public function setEndCallback(\Closure $end_callback) + { + $this->end_callback = $end_callback; + return $this; + } + + /** + * @param LoggerInterface $Logger + * @return $this + */ + public function setLogger(LoggerInterface $Logger) + { + $this->Logger = $Logger; + return $this; + } + + /** + * @param DataPackerInterface $DataPacker + * @return $this + */ + public function setDataPacker($DataPacker) + { + $this->DataPacker = $DataPacker; + return $this; + } + + /** + * @return array + */ + public function getLastProfileData() + { + return $this->last_profile_data; + } + + /** + * @return Connection + * @throws DBALException + */ + protected function getConnection() + { + if (null === $this->Conn) { + $config = new \Doctrine\DBAL\Configuration(); + $connectionParams = ['url' => $this->connection_string]; + $this->Conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config); + } + + return $this->Conn; + } + + /** + * @param Connection $Conn + * @return $this + */ + public function setConnection(Connection $Conn) + { + $this->Conn = $Conn; + return $this; + } + + /** + * @param string $connection_string + * @return $this + */ + public function setConnectionString($connection_string) + { + $this->connection_string = $connection_string; + return $this; + } + + /** + * @param string $app + * @param string $label + * @param string $datetime + * @param string $data + * @return bool + * @throws DBALException + */ + protected function save($app, $label, $datetime, $data) + { + return (bool)$this->getConnection()->insert( + 'details', + [ + 'app' => $app, + 'label' => $label, + 'perfdata' => $data, + 'timestamp' => $datetime + ] + ); + } + + /** + * @throws DBALException + */ + public function createTable() + { + $driver_name = $this->getConnection()->getDriver()->getName(); + $sql_path = __DIR__ . '/../../../bin/install_data/' . $driver_name . '/source.sql'; + if (!file_exists($sql_path)) { + $this->Logger->error('Invalid sql path:' . $sql_path); + return false; + } + + $sql = file_get_contents($sql_path); + + $this->getConnection()->exec($sql); + return true; + } + + /** + * @return string + */ + protected function getAutoLabel() + { + if (!empty($_SERVER['REQUEST_URI'])) { + $label = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + return $label ?: $_SERVER['REQUEST_URI']; + } + + return $_SERVER['SCRIPT_NAME']; + } + + /** + * @param int $divider + * @return bool + */ + protected function needToStart($divider) + { + return mt_rand(1, $divider) === 1; + } +} diff --git a/src/Badoo/LiveProfiler/Logger.php b/src/Badoo/LiveProfiler/Logger.php new file mode 100644 index 0000000..da946bc --- /dev/null +++ b/src/Badoo/LiveProfiler/Logger.php @@ -0,0 +1,61 @@ + + */ + +namespace Badoo\LiveProfiler; + +use Psr\Log\LoggerInterface; +use Psr\Log\LoggerTrait; + +class Logger implements LoggerInterface +{ + use LoggerTrait; + + /** @var string */ + protected $logfile; + + /** + * Logger constructor. + */ + public function __construct() + { + $this->logfile = __DIR__ . '/../../../live.profiler.log'; + } + + public function setLogFile($logfile) + { + $this->logfile = $logfile; + } + + /** + * @param mixed $level + * @param string $message + * @param array $context + */ + public function log($level, $message, array $context = array()) + { + $log_string = $this->getLogMsg($level, $message, $context); + file_put_contents($this->logfile, $log_string, FILE_APPEND); + } + + /** + * @param string $level + * @param string $message + * @param array $context + * @return string + */ + protected function getLogMsg($level, $message, array $context = array()) + { + $log_string = sprintf("%s\t%s\t%s", date('Y-m-d H:i:s'), $level, $message); + + if (!empty($context)) { + $log_string .= "\t" . json_encode($context, true); + } + + $log_string .= "\n"; + + return $log_string; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..6e0bc63 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,2 @@ + + */ + +namespace unit\Badoo; + +class BaseTestCase extends \PHPUnit\Framework\TestCase +{ + /** + * Call protected/private method of a class. + * @param object &$object Instantiated object that we will run method on. + * @param string $methodName Method name to call + * @param array $parameters Array of parameters to pass into method. + * @return mixed Method return. + * @throws \ReflectionException + */ + public function invokeMethod(&$object, $methodName, array $parameters = array()) + { + $reflection = new \ReflectionClass(\get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } + + /** + * @param $object + * @param $property + * @param $value + * @throws \ReflectionException + */ + public function setProtectedProperty(&$object, $property, $value) + { + $reflection = new \ReflectionClass(\get_class($object)); + $reflection_property = $reflection->getProperty($property); + $reflection_property->setAccessible(true); + $reflection_property->setValue($object, $value); + } + + /** + * @param $object + * @param $property + * @return mixed + * @throws \ReflectionException + */ + public function getProtectedProperty(&$object, $property) + { + $reflection = new \ReflectionClass(\get_class($object)); + $reflection_property = $reflection->getProperty($property); + $reflection_property->setAccessible(true); + return $reflection_property->getValue($object); + } +} diff --git a/tests/unit/Badoo/LiveProfiler/DataPackerTest.php b/tests/unit/Badoo/LiveProfiler/DataPackerTest.php new file mode 100644 index 0000000..3ecc3fd --- /dev/null +++ b/tests/unit/Badoo/LiveProfiler/DataPackerTest.php @@ -0,0 +1,34 @@ + + */ + +namespace unit\Badoo\LiveProfiler; + +class DataPackerTest extends \unit\Badoo\BaseTestCase +{ + public function testPack() + { + $data = ['a' => 1]; + $Packer = new \Badoo\LiveProfiler\DataPacker(); + + $result = $Packer->pack($data); + + static::assertEquals(json_encode($data), $result); + } + + /** + * @depends testPack + */ + public function testUnPack() + { + $data = ['a' => 1]; + $Packer = new \Badoo\LiveProfiler\DataPacker(); + $packed_data = $Packer->pack($data); + + $result = $Packer->unpack($packed_data); + + static::assertEquals(json_decode($packed_data, true), $result); + } +} diff --git a/tests/unit/Badoo/LiveProfiler/LiveProfilerTest.php b/tests/unit/Badoo/LiveProfiler/LiveProfilerTest.php new file mode 100644 index 0000000..22963bd --- /dev/null +++ b/tests/unit/Badoo/LiveProfiler/LiveProfilerTest.php @@ -0,0 +1,544 @@ + + */ + +namespace unit\Badoo\LiveProfiler; + +class LiveProfilerTest extends \unit\Badoo\BaseTestCase +{ + public function providerGetAutoLabel() + { + return [ + [ + 'request_uri' => '', + 'script_name' => 'index.php', + 'expected' => 'index.php', + ], + [ + 'request_uri' => '/test/test?param=value', + 'script_name' => '', + 'expected' => '/test/test', + ] + ]; + } + + /** + * @dataProvider providerGetAutoLabel + * @param $request_uri + * @param $script_name + * @param $expected + * @throws \ReflectionException + */ + public function testGetAutoLabel($request_uri, $script_name, $expected) + { + $_SERVER['REQUEST_URI'] = $request_uri; + $_SERVER['SCRIPT_NAME'] = $script_name; + $Profiler = new \Badoo\LiveProfiler\LiveProfiler('sqlite:///:memory:'); + + $label = $this->invokeMethod($Profiler, 'getAutoLabel', []); + self::assertEquals($expected, $label); + } + + public function testStart() + { + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['end']) + ->getMock(); + $ProfilerMock->method('end')->willReturn(true); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $ProfilerMock + ->setDivider(1) + ->setStartCallback(function () { + }); + + $result = $ProfilerMock->start(); + self::assertTrue($result); + } + + /** + * @throws \ReflectionException + */ + public function testAlreadyStarted() + { + $Profiler = new \Badoo\LiveProfiler\LiveProfiler(); + + $this->setProtectedProperty($Profiler, 'is_enabled', true); + + $result = $Profiler->start(); + self::assertTrue($result); + } + + public function testStartWithoutCallback() + { + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['end']) + ->getMock(); + $ProfilerMock->method('end')->willReturn(true); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $result = $ProfilerMock->start(); + + self::assertTrue($result); + } + + public function testStartTotal() + { + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['end']) + ->getMock(); + $ProfilerMock->method('end')->willReturn(true); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $ProfilerMock + ->setTotalDivider(1) + ->setStartCallback(function () { + }); + + $result = $ProfilerMock->start(); + self::assertTrue($result); + } + + /** + * @depends testStart + * @throws \ReflectionException + */ + public function testEnd() + { + $DataPacker = new \Badoo\LiveProfiler\DataPacker(); + + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['save']) + ->getMock(); + $ProfilerMock->method('save')->willReturn(true); + + $this->setProtectedProperty($ProfilerMock, 'is_enabled', true); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $ProfilerMock + ->setDataPacker($DataPacker) + ->setEndCallback(function () { + return ['end result']; + }); + + $result = $ProfilerMock->end(); + self::assertTrue($result); + } + + /** + * @throws \ReflectionException + */ + public function testReset() + { + $Profiler = new \Badoo\LiveProfiler\LiveProfiler(); + $Profiler->setEndCallback(function () {}); + + $this->setProtectedProperty($Profiler, 'is_enabled', true); + + $result = $Profiler->reset(); + self::assertTrue($result); + + $is_enabled = $this->getProtectedProperty($Profiler, 'is_enabled'); + self::assertFalse($is_enabled); + } + + /** + * @throws \ReflectionException + */ + public function testEndWithoutCallback() + { + $Profiler = new \Badoo\LiveProfiler\LiveProfiler(); + + $this->setProtectedProperty($Profiler, 'is_enabled', true); + $this->setProtectedProperty($Profiler, 'end_callback', null); + + $result = $Profiler->end(); + self::assertTrue($result); + } + + /** + * @depends testStart + */ + public function testEndErrorInProfilerData() + { + $LoggerMock = $this->getMockBuilder('\Badoo\LiveProfiler\Logger') + ->disableOriginalConstructor() + ->setMethods(['warning']) + ->getMock(); + $LoggerMock->method('warning')->willReturn(true); + /** @var \Psr\Log\LoggerInterface $LoggerMock */ + + $Profiler = new \Badoo\LiveProfiler\LiveProfiler('sqlite:///:memory:'); + $Profiler->setLogger($LoggerMock); + $Profiler->setDivider(1); + $Profiler->setStartCallback(function () { + return true; + }); + $Profiler->setEndCallback(function () { + return null; + }); + $Profiler->start(); + + $result = $Profiler->end(); + self::assertFalse($result); + } + + /** + * @depends testStart + */ + public function testEndSaveFalse() + { + $LoggerMock = $this->getMockBuilder('\Badoo\LiveProfiler\Logger') + ->disableOriginalConstructor() + ->setMethods(['warning']) + ->getMock(); + $LoggerMock->method('warning')->willReturn(true); + /** @var \Psr\LOg\LoggerInterface $LoggerMock */ + + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->setConstructorArgs(['sqlite:///:memory:']) + ->setMethods(['save']) + ->getMock(); + $ProfilerMock->method('save')->willReturn(false); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $ProfilerMock->setLogger($LoggerMock); + $ProfilerMock->setDivider(1); + $ProfilerMock->setStartCallback(function () { + return true; + }); + $ProfilerMock->setEndCallback(function () { + return ['end result']; + }); + $ProfilerMock->start(); + + $result = $ProfilerMock->end(); + self::assertFalse($result); + } + + /** + * @depends testStart + */ + public function testEndErrorInSaving() + { + $LoggerMock = $this->getMockBuilder('\Badoo\LiveProfiler\Logger') + ->disableOriginalConstructor() + ->setMethods(['warning', 'error']) + ->getMock(); + $LoggerMock->method('warning')->willReturn(true); + $LoggerMock->method('error')->willReturn(true); + /** @var \Psr\LOg\LoggerInterface $LoggerMock */ + + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->setConstructorArgs(['sqlite:///:memory:']) + ->setMethods(['save']) + ->getMock(); + $ProfilerMock->method('save')->willReturnCallback(function () { + throw new \Doctrine\DBAL\DBALException('Error in insertion'); + }); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $ProfilerMock->setLogger($LoggerMock); + $ProfilerMock->setDivider(1); + $ProfilerMock->setStartCallback(function () { + return true; + }); + $ProfilerMock->setEndCallback(function () { + return ['end result']; + }); + $ProfilerMock->start(); + + $result = $ProfilerMock->end(); + self::assertFalse($result); + } + + public function testEndWithoutStartProfiling() + { + $Profiler = new \Badoo\LiveProfiler\LiveProfiler('sqlite:///:memory:'); + + $result = $Profiler->end(); + self::assertTrue($result); + } + + /** + * @throws \ReflectionException + */ + public function testSettersGetters() + { + /** @var \Psr\Log\LoggerInterface $LoggerMock */ + $LoggerMock = $this->getMockBuilder('\Badoo\LiveProfiler\Logger') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $DataPacker = new \Badoo\LiveProfiler\DataPacker(); + + /** @var \Doctrine\DBAL\Connection $ConnectionMock */ + $ConnectionMock = $this->getMockBuilder('\Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $test_app = 'test_app'; + $test_label = 'test_label'; + $test_datetime = 'test_datetime'; + $test_divider = 1; + $test_total_divider = 2; + $test_connection_string = 'test_connection_string'; + + $Profiler = new \Badoo\LiveProfiler\LiveProfiler('sqlite:///:memory:'); + + $Profiler + ->setApp($test_app) + ->setLabel($test_label) + ->setDateTime($test_datetime) + ->setDivider($test_divider) + ->setTotalDivider($test_total_divider) + ->setLogger($LoggerMock) + ->setDataPacker($DataPacker) + ->setConnection($ConnectionMock) + ->setConnectionString($test_connection_string); + + $app = $this->getProtectedProperty($Profiler, 'app'); + $label = $this->getProtectedProperty($Profiler, 'label'); + $datetime = $this->getProtectedProperty($Profiler, 'datetime'); + $divider = $this->getProtectedProperty($Profiler, 'divider'); + $total_divider = $this->getProtectedProperty($Profiler, 'total_divider'); + $Logger = $this->getProtectedProperty($Profiler, 'Logger'); + $DataPackerNew = $this->getProtectedProperty($Profiler, 'DataPacker'); + $Connection = $this->getProtectedProperty($Profiler, 'Conn'); + $connection_string = $this->getProtectedProperty($Profiler, 'connection_string'); + + self::assertEquals($test_app, $app); + self::assertEquals($test_app, $Profiler->getApp()); + self::assertEquals($test_label, $label); + self::assertEquals($test_label, $Profiler->getLabel()); + self::assertEquals($test_datetime, $datetime); + self::assertEquals($test_datetime, $Profiler->getDateTime()); + self::assertEquals($test_divider, $divider); + self::assertEquals($test_total_divider, $total_divider); + self::assertSame($LoggerMock, $Logger); + self::assertSame($DataPacker, $DataPackerNew); + self::assertSame($ConnectionMock, $Connection); + self::assertSame([], $Profiler->getLastProfileData()); + self::assertSame($test_connection_string, $connection_string); + } + + public function testGetInstance() + { + $Profiler1 = \Badoo\LiveProfiler\LiveProfiler::getInstance(); + $Profiler2 = \Badoo\LiveProfiler\LiveProfiler::getInstance(); + + static::assertSame($Profiler1, $Profiler2); + } + + /** + * @throws \Doctrine\DBAL\DBALException + */ + public function testCreateTable() + { + $Profiler = new \Badoo\LiveProfiler\LiveProfiler('sqlite:///:memory:'); + + $result = $Profiler->createTable(); + + static::assertTrue($result); + } + + /** + * @throws \Doctrine\DBAL\DBALException + */ + public function testCreateTableError() + { + $LoggerMock = $this->getMockBuilder('\Badoo\LiveProfiler\Logger') + ->disableOriginalConstructor() + ->setMethods(['error']) + ->getMock(); + $LoggerMock->method('error')->willReturn(true); + /** @var \Psr\Log\LoggerInterface $LoggerMock */ + + $Profiler = new \Badoo\LiveProfiler\LiveProfiler('drizzle-pdo-mysql://localhost:4486/foo?charset=UTF-8'); + $Profiler->setLogger($LoggerMock); + $result = $Profiler->createTable(); + + static::assertFalse($result); + } + + /** + * @throws \Doctrine\DBAL\DBALException + * @throws \ReflectionException + */ + public function testSave() + { + $Profiler = new \Badoo\LiveProfiler\LiveProfiler('sqlite:///:memory:'); + $Profiler->createTable(); + + $result = $this->invokeMethod($Profiler, 'save', ['app', 'label', 'datetime', 'data']); + self::assertTrue($result); + } + + /** + * @throws \ReflectionException + */ + public function testUseXhprof() + { + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['__construct']) + ->getMock(); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $result = $ProfilerMock->useXhprof(); + + self::assertTrue($result); + $start_callback = $this->getProtectedProperty($ProfilerMock, 'start_callback'); + $end_callback = $this->getProtectedProperty($ProfilerMock, 'end_callback'); + self::assertNotEmpty($start_callback); + self::assertInternalType('callable', $start_callback); + self::assertNotEmpty($end_callback); + self::assertInternalType('callable', $end_callback); + } + + /** + * @throws \ReflectionException + */ + public function testUseXhprofAfterStart() + { + $LoggerMock = $this->getMockBuilder('\Badoo\LiveProfiler\Logger') + ->disableOriginalConstructor() + ->setMethods(['warning']) + ->getMock(); + $LoggerMock->method('warning')->willReturn(true); + /** @var \Psr\LOg\LoggerInterface $LoggerMock */ + + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['__construct']) + ->getMock(); + + $this->setProtectedProperty($ProfilerMock, 'is_enabled', true); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $result = $ProfilerMock + ->setLogger($LoggerMock) + ->useXhprof(); + + self::assertFalse($result); + } + + /** + * @throws \ReflectionException + */ + public function testUseUprofiler() + { + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['__construct']) + ->getMock(); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $result = $ProfilerMock->useUprofiler(); + + self::assertTrue($result); + $start_callback = $this->getProtectedProperty($ProfilerMock, 'start_callback'); + $end_callback = $this->getProtectedProperty($ProfilerMock, 'end_callback'); + self::assertNotEmpty($start_callback); + self::assertInternalType('callable', $start_callback); + self::assertNotEmpty($end_callback); + self::assertInternalType('callable', $end_callback); + } + + /** + * @throws \ReflectionException + */ + public function testUseUprofilerAfterStart() + { + $LoggerMock = $this->getMockBuilder('\Badoo\LiveProfiler\Logger') + ->disableOriginalConstructor() + ->setMethods(['warning']) + ->getMock(); + $LoggerMock->method('warning')->willReturn(true); + /** @var \Psr\LOg\LoggerInterface $LoggerMock */ + + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['__construct']) + ->getMock(); + + $this->setProtectedProperty($ProfilerMock, 'is_enabled', true); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $result = $ProfilerMock + ->setLogger($LoggerMock) + ->useUprofiler(); + + self::assertFalse($result); + } + + /** + * @throws \ReflectionException + */ + public function testUseTidyWays() + { + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['__construct']) + ->getMock(); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $result = $ProfilerMock->useTidyWays(); + + self::assertTrue($result); + $start_callback = $this->getProtectedProperty($ProfilerMock, 'start_callback'); + $end_callback = $this->getProtectedProperty($ProfilerMock, 'end_callback'); + self::assertNotEmpty($start_callback); + self::assertInternalType('callable', $start_callback); + self::assertNotEmpty($end_callback); + self::assertInternalType('callable', $end_callback); + } + + /** + * @throws \ReflectionException + */ + public function testUseTidyWaysAfterStart() + { + $LoggerMock = $this->getMockBuilder('\Badoo\LiveProfiler\Logger') + ->disableOriginalConstructor() + ->setMethods(['warning']) + ->getMock(); + $LoggerMock->method('warning')->willReturn(true); + /** @var \Psr\LOg\LoggerInterface $LoggerMock */ + + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['__construct']) + ->getMock(); + + $this->setProtectedProperty($ProfilerMock, 'is_enabled', true); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $result = $ProfilerMock + ->setLogger($LoggerMock) + ->useTidyWays(); + + self::assertFalse($result); + } + + public function testDetectProfiler() + { + $ProfilerMock = $this->getMockBuilder('\Badoo\LiveProfiler\LiveProfiler') + ->disableOriginalConstructor() + ->setMethods(['__construct']) + ->getMock(); + + /** @var \Badoo\LiveProfiler\LiveProfiler $ProfilerMock */ + $result = $ProfilerMock->detectProfiler(); + + self::assertInternalType('bool', $result); + } +} diff --git a/tests/unit/Badoo/LiveProfiler/LoggerTest.php b/tests/unit/Badoo/LiveProfiler/LoggerTest.php new file mode 100644 index 0000000..271ca8f --- /dev/null +++ b/tests/unit/Badoo/LiveProfiler/LoggerTest.php @@ -0,0 +1,33 @@ + + */ + +namespace unit\Badoo\LiveProfiler; + +class LoggerTest extends \unit\Badoo\BaseTestCase +{ + /** + * @throws \ReflectionException + */ + public function testGetLogMsg() + { + $Logger = new \Badoo\LiveProfiler\Logger(); + $log_msg = $this->invokeMethod($Logger, 'getLogMsg', ['error', 'Error msg', ['param' => 1]]); + self::assertEquals(date('Y-m-d H:i:s'). "\terror\tError msg\t{\"param\":1}\n", $log_msg); + } + + public function testLog() + { + $tmp_log_file = tempnam('/tmp', 'live.profiling'); + $Logger = new \Badoo\LiveProfiler\Logger(); + $Logger->setLogFile($tmp_log_file); + $Logger->log('error', 'Error msg'); + + $log_msg = file_get_contents($tmp_log_file); + unset($tmp_log_file); + + self::assertEquals(date('Y-m-d H:i:s'). "\terror\tError msg\n", $log_msg); + } +}