├── .gitignore ├── .php_cs.dist ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src └── BlockDetector.php └── test └── BlockTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | /.php_cs.cache -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 5 | ->setRules([ 6 | "@PSR1" => true, 7 | "@PSR2" => true, 8 | "braces" => [ 9 | "allow_single_line_closure" => true, 10 | "position_after_functions_and_oop_constructs" => "same", 11 | ], 12 | "array_syntax" => ["syntax" => "short"], 13 | "cast_spaces" => true, 14 | "combine_consecutive_unsets" => true, 15 | "function_to_constant" => true, 16 | "no_multiline_whitespace_before_semicolons" => true, 17 | "no_unused_imports" => true, 18 | "no_useless_else" => true, 19 | "no_useless_return" => true, 20 | "no_whitespace_before_comma_in_array" => true, 21 | "no_whitespace_in_blank_line" => true, 22 | "non_printable_character" => true, 23 | "normalize_index_brace" => true, 24 | "ordered_imports" => true, 25 | "php_unit_construct" => true, 26 | "php_unit_dedicate_assert" => true, 27 | "php_unit_fqcn_annotation" => true, 28 | "phpdoc_summary" => true, 29 | "phpdoc_types" => true, 30 | "psr4" => true, 31 | "return_type_declaration" => ["space_before" => "none"], 32 | "short_scalar_cast" => true, 33 | "single_blank_line_before_namespace" => true, 34 | ]) 35 | ->setFinder( 36 | PhpCsFixer\Finder::create() 37 | ->in(__DIR__ . "/src") 38 | ->in(__DIR__ . "/test") 39 | ); 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | php: 6 | - 7.0 7 | - 7.1 8 | - nightly 9 | 10 | matrix: 11 | allow_failures: 12 | - php: nightly 13 | fast_finish: true 14 | 15 | before_install: 16 | - phpenv config-rm xdebug.ini 17 | 18 | install: 19 | # --ignore-platform-reqs, because https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/2722 20 | - composer update -n --prefer-dist --ignore-platform-reqs 21 | - composer require satooshi/php-coveralls dev-master --ignore-platform-reqs 22 | 23 | - vendor/amphp/amp/travis/install-uv.sh 24 | - vendor/amphp/amp/travis/install-ev.sh 25 | - vendor/amphp/amp/travis/install-event.sh 26 | 27 | - mkdir -p coverage/cov coverage/bin 28 | - wget https://phar.phpunit.de/phpcov.phar -O coverage/bin/phpcov 29 | - chmod +x coverage/bin/phpcov 30 | 31 | - composer show 32 | 33 | script: 34 | - phpdbg -qrr vendor/bin/phpunit --verbose --exclude-group memoryleak --coverage-php coverage/cov/main.cov 35 | - PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer --diff --dry-run -v fix 36 | 37 | after_script: 38 | - phpdbg -qrr coverage/bin/phpcov merge --clover build/logs/clover.xml coverage/cov 39 | - php vendor/bin/coveralls -v 40 | 41 | cache: 42 | directories: 43 | - $HOME/.composer/cache/files -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Niklas Keller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PHP_BIN := php 2 | COMPOSER_BIN := composer 3 | 4 | COVERAGE = coverage 5 | SRCS = lib test 6 | 7 | find_php_files = $(shell find $(1) -type f -name "*.php") 8 | src = $(foreach d,$(SRCS),$(call find_php_files,$(d))) 9 | 10 | .PHONY: test 11 | test: setup phpunit code-style 12 | 13 | .PHONY: clean 14 | clean: clean-coverage clean-vendor 15 | 16 | .PHONY: clean-coverage 17 | clean-coverage: 18 | test ! -e coverage || rm -r coverage 19 | 20 | .PHONY: clean-vendor 21 | clean-vendor: 22 | test ! -e vendor || rm -r vendor 23 | 24 | .PHONY: setup 25 | setup: vendor/autoload.php 26 | 27 | .PHONY: deps-update 28 | deps-update: 29 | $(COMPOSER_BIN) update 30 | 31 | .PHONY: phpunit 32 | phpunit: setup 33 | $(PHP_BIN) vendor/bin/phpunit 34 | 35 | .PHONY: code-style 36 | code-style: setup 37 | PHP_CS_FIXER_IGNORE_ENV=1 $(PHP_BIN) vendor/bin/php-cs-fixer --diff -v fix 38 | 39 | composer.lock: composer.json 40 | $(COMPOSER_BIN) install 41 | touch $@ 42 | 43 | vendor/autoload.php: composer.lock 44 | $(COMPOSER_BIN) install 45 | touch $@ 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loop-block 2 | 3 | Detect blocking ticks in an event loop based on the PHP [event loop standard](https://github.com/async-interop/event-loop). 4 | 5 | ## Installation 6 | 7 | ``` 8 | $ composer install kelunik/loop-block 9 | ``` 10 | 11 | ## Usage 12 | 13 | You can instantiate a new `BlockDetector`. Its constructor takes a callback to be executed on blocks, a threshold in milliseconds, and an interval to configure how often the check is executed. 14 | 15 | Once the loop runs, you can call `BlockDetector::start` to start the detection. `BlockDetector::stop` stops the detection again. 16 | 17 | ## License 18 | 19 | [MIT](./LICENSE). 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kelunik/loop-block", 3 | "description": "Detect blocking ticks in the Amp event loop.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Niklas Keller", 9 | "email": "me@kelunik.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Kelunik\\LoopBlock\\": "src" 15 | } 16 | }, 17 | "require": { 18 | "amphp/amp": "^2" 19 | }, 20 | "require-dev": { 21 | "amphp/phpunit-util": "^1", 22 | "friendsofphp/php-cs-fixer": "^2.3", 23 | "phpunit/phpunit": "^6" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | test 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | src 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/BlockDetector.php: -------------------------------------------------------------------------------- 1 | onBlock = $onBlock; 25 | $this->blockThreshold = $blockThreshold; 26 | $this->checkInterval = $checkInterval; 27 | 28 | $this->measure = function ($watcherId, $time) { 29 | $timeDiff = microtime(true) - $time; 30 | $timeDiff *= 1000; 31 | 32 | if ($timeDiff > $this->blockThreshold && $this->watcher !== null) { 33 | $onBlock = $this->onBlock; 34 | $onBlock($timeDiff); 35 | } 36 | }; 37 | 38 | $this->check = function () { 39 | $time = microtime(1); 40 | 41 | Loop::unreference(Loop::defer($this->measure, $time)); 42 | }; 43 | } 44 | 45 | /** 46 | * Start the detector. 47 | */ 48 | public function start() { 49 | if ($this->watcher !== null) { 50 | return; 51 | } 52 | 53 | $this->watcher = Loop::repeat($this->checkInterval, function () { 54 | // Use double defer to calculate complete tick time 55 | // instead of timer → defer time. 56 | Loop::unreference(Loop::defer($this->check)); 57 | }); 58 | 59 | Loop::unreference($this->watcher); 60 | } 61 | 62 | /** 63 | * Stop the detector. 64 | */ 65 | public function stop() { 66 | if ($this->watcher === null) { 67 | return; 68 | } 69 | 70 | Loop::cancel($this->watcher); 71 | $this->watcher = null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/BlockTest.php: -------------------------------------------------------------------------------- 1 | start(); 30 | 31 | Loop::repeat(0, function () { 32 | usleep(100 * 1000); 33 | }); 34 | 35 | Loop::delay(300, function () { 36 | Loop::stop(); 37 | }); 38 | }); 39 | 40 | $this->assertGreaterThanOrEqual($expectedCallCount, $callCount); 41 | } 42 | 43 | public function provideArgs() { 44 | return [ 45 | [10, 0, 2], 46 | [90, 0, 2], 47 | [110, 0, 0], 48 | [300, 0, 0], 49 | ]; 50 | } 51 | } 52 | --------------------------------------------------------------------------------