diff --git a/Coroutine/Core.php b/Coroutine/Core.php index 3356e1e..59d5730 100644 --- a/Coroutine/Core.php +++ b/Coroutine/Core.php @@ -24,6 +24,49 @@ \define('EOL', \PHP_EOL); \define('CRLF', "\r\n"); + /** + * Returns a random float between two numbers. + * + * Works similar to Python's `random.uniform()` + * @see https://docs.python.org/3/library/random.html#random.uniform + * + * @param int $min + * @param int $max + * @return float + */ + function random_uniform($min, $max) + { + return ($min + \lcg_value() * (\abs($max - $min))); + } + + /** + * Return the value (in fractional seconds) of a performance counter, i.e. a clock with the highest + * available resolution to measure a short duration. Using either `hrtime` or system's `microtime`. + * + * @param string $tag + * - A reference point used to set, to get the difference between the results of consecutive calls. + * - Will be cleared/unset on the next consecutive call. + * + * @return float|void + * + * @see https://docs.python.org/3/library/time.html#time.perf_counter + * @see https://nodejs.org/docs/latest-v11.x/api/console.html#console_console_time_label + */ + function timer_for(string $tag = 'perf_counter') + { + global $__timer__; + if (isset($__timer__[$tag])) { + $perf_counter = $__timer__[$tag]; + $__timer__[$tag] = null; + unset($GLOBALS['__timer__'][$tag]); + return (float) ($__timer__['hrtime'] + ? (\hrtime(true) / 1e+9) - $perf_counter + : \microtime(true) - $perf_counter); + } + + $__timer__[$tag] = (float) ($__timer__['hrtime'] ? \hrtime(true) / 1e+9 : \microtime(true)); + } + /** * Makes an resolvable function from label name that's callable with `away` * The passed in `function/callable/task` is wrapped to be `awaitAble` @@ -81,7 +124,8 @@ function result($value) * - This function needs to be prefixed with `yield` * * @param Generator|callable $awaitableFunction - * @param mixed $args - if `generator`, $args can hold `customState`, and `customData` + * @param mixed ...$args - if **$awaitableFunction** is `Generator`, $args can hold `customState`, and `customData` + * - for third party code integration. * * @return int $task id */ @@ -1481,11 +1525,12 @@ function coroutine_instance(): ?CoroutineInterface function coroutine_clear() { - global $__coroutine__; + global $__coroutine__, $__timer__; if ($__coroutine__ instanceof CoroutineInterface) { $__coroutine__->setup(false); - unset($GLOBALS['__coroutine__']); + unset($GLOBALS['__coroutine__'], $GLOBALS['__timer__']); $__coroutine__ = null; + $__timer__ = null; } } diff --git a/Coroutine/Coroutine.php b/Coroutine/Coroutine.php index 4e092d4..0ffd7ae 100644 --- a/Coroutine/Coroutine.php +++ b/Coroutine/Coroutine.php @@ -235,7 +235,7 @@ public function close() */ public function __construct() { - global $__coroutine__; + global $__coroutine__, $__timer__; $__coroutine__ = $this; $this->initSignals(); @@ -272,7 +272,7 @@ public function __construct() }; } - $this->isHighTimer = \function_exists('hrtime'); + $this->isHighTimer = $__timer__['hrtime'] = \function_exists('hrtime'); $this->parallel = new Parallel($this); $this->taskQueue = new \SplQueue(); } diff --git a/README.md b/README.md index 6d119f2..49aef3e 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![Coroutine](https://github.com/symplely/coroutine/workflows/Coroutine/badge.svg)](https://github.com/symplely/coroutine/actions)[![codecov](https://codecov.io/gh/symplely/coroutine/branch/master/graph/badge.svg)](https://codecov.io/gh/symplely/coroutine)[![Codacy Badge](https://api.codacy.com/project/badge/Grade/44a6f32f03194872b7d4cd6a2411ff79)](https://www.codacy.com/app/techno-express/coroutine?utm_source=github.com&utm_medium=referral&utm_content=symplely/coroutine&utm_campaign=Badge_Grade)[![Maintainability](https://api.codeclimate.com/v1/badges/1bfc3497fde67b111a04/maintainability)](https://codeclimate.com/github/symplely/coroutine/maintainability) -> For versions `1.5.x` onward, has features of an PHP extension [UV](https://github.com/bwoebi/php-uv), of **Node.js** [libuv](https://github.com/libuv/libuv) library, see the online [book](https://nikhilm.github.io/uvbook/index.html) for a full tutorial overview. +> For versions `1.5.x` onward, has features of an PHP extension [UV](https://github.com/amphp/ext-uv), of **Node.js** [libuv](https://github.com/libuv/libuv) library, see the online [book](https://nikhilm.github.io/uvbook/index.html) for a full tutorial overview. -> Currently all `libuv` [network](https://github.com/bwoebi/php-uv/issues) `socket/stream/udp/tcp` like features are broken on *Windows*, as such will not be implemented for *Windows*, will continue to use native `stream_select` instead. +> Currently all `libuv` [network](https://github.com/amphp/ext-uv/issues) `socket/stream/udp/tcp` like features are broken on *Windows*, as such will not be implemented for *Windows*, will continue to use native `stream_select` instead. ## Table of Contents @@ -52,6 +52,149 @@ This package follows a new paradigm [Behavioral Programming](http://www.wisdom.w The base overall usage of [Swoole Coroutine](https://www.swoole.co.uk/coroutine), and [FaceBook's Hhvm](https://docs.hhvm.com/hack/asynchronous-operations/introduction) **PHP** follows the same outline implementations as others and put forth here. +To illustrate further take this comparison between **NodeJS** and **Python** from [Intro to Async Concurrency in Python vs. Node.js](https://medium.com/@interfacer/intro-to-async-concurrency-in-python-and-node-js-69315b1e3e36). + +```js +// async_scrape.js (tested with node 11.3) +const sleep = ts => new Promise(resolve => setTimeout(resolve, ts * 1000)); + +async function fetchUrl(url) { + console.log(`~ executing fetchUrl(${url})`); + console.time(`fetchUrl(${url})`); + await sleep(1 + Math.random() * 4); + console.timeEnd(`fetchUrl(${url})`); + return `fake page html for ${url}`; +} + +async function analyzeSentiment(html) { + console.log(`~ analyzeSentiment("${html}")`); + console.time(`analyzeSentiment("${html}")`); + await sleep(1 + Math.random() * 4); + const r = { + positive: Math.random() + } + console.timeEnd(`analyzeSentiment("${html}")`); + return r; +} + +const urls = [ + "https://www.ietf.org/rfc/rfc2616.txt", + "https://en.wikipedia.org/wiki/Asynchronous_I/O", +] +const extractedData = {} + +async function handleUrl(url) { + const html = await fetchUrl(url); + extractedData[url] = await analyzeSentiment(html); +} + +async function main() { + console.time('elapsed'); + await Promise.all(urls.map(handleUrl)); + console.timeEnd('elapsed'); +} + +main() +``` + +```py +# async_scrape.py (requires Python 3.7+) +import asyncio, random, time + +async def fetch_url(url): + print(f"~ executing fetch_url({url})") + t = time.perf_counter() + await asyncio.sleep(random.randint(1, 5)) + print(f"time of fetch_url({url}): {time.perf_counter() - t:.2f}s") + return f"fake page html for {url}" + +async def analyze_sentiment(html): + print(f"~ executing analyze_sentiment('{html}')") + t = time.perf_counter() + await asyncio.sleep(random.randint(1, 5)) + r = {"positive": random.uniform(0, 1)} + print(f"time of analyze_sentiment('{html}'): {time.perf_counter() - t:.2f}s") + return r + +urls = [ + "https://www.ietf.org/rfc/rfc2616.txt", + "https://en.wikipedia.org/wiki/Asynchronous_I/O", +] +extracted_data = {} + +async def handle_url(url): + html = await fetch_url(url) + extracted_data[url] = await analyze_sentiment(html) + +async def main(): + t = time.perf_counter() + await asyncio.gather(*(handle_url(url) for url in urls)) + print("> extracted data:", extracted_data) + print(f"time elapsed: {time.perf_counter() - t:.2f}s") + +asyncio.run(main()) +``` + +**Using this package as setout, it's the same simplicity:** + +```php +// This is in the examples folder as "async_scrape.php" +include 'vendor/autoload.php'; + +function fetch_url($url) +{ + print("~ executing fetch_url($url)" . \EOL); + \timer_for($url); + yield \sleep_for(\random_uniform(1, 5)); + print("time of fetch_url($url): " . \timer_for($url) . 's' . \EOL); + return "fake page html for $url"; +}; + +function analyze_sentiment($html) +{ + print("~ executing analyze_sentiment('$html')" . \EOL); + \timer_for($html . '.url'); + yield \sleep_for(\random_uniform(1, 5)); + $r = "positive: " . \random_uniform(0, 1); + print("time of analyze_sentiment('$html'): " . \timer_for($html . '.url') . 's' . \EOL); + return $r; +}; + +function handle_url($url) +{ + yield; + $extracted_data = []; + $html = yield fetch_url($url); + $extracted_data[$url] = yield analyze_sentiment($html); + return yield $extracted_data; +}; + +function main() +{ + $urls = [ + "https://www.ietf.org/rfc/rfc2616.txt", + "https://en.wikipedia.org/wiki/Asynchronous_I/O" + ]; + $urlID = []; + + \timer_for(); + foreach ($urls as $url) + $urlID[] = yield \away(handle_url($url)); + + $result_data = yield \gather($urlID); + foreach ($result_data as $id => $extracted_data) { + echo "> extracted data:"; + \print_r($extracted_data); + } + + print("time elapsed: " . \timer_for() . 's'); +} + +\coroutine_run(main()); +``` + +Try recreating this with the other pure *PHP* async implementations, they would need an rewrite first to come close. + ------- A **Coroutine** here are specially crafted functions that are based on __generators__, with the use of `yield` and `yield from`. When used, they **control context**, meaning `capture/release` an application's execution flow. diff --git a/examples/async_scrape.php b/examples/async_scrape.php index cd71f1f..f75fc9b 100644 --- a/examples/async_scrape.php +++ b/examples/async_scrape.php @@ -41,9 +41,9 @@ } async function main() { - console.time('ellapsed'); + console.time('elapsed'); await Promise.all(urls.map(handleUrl)); - console.timeEnd('ellapsed'); + console.timeEnd('elapsed'); } main() @@ -89,29 +89,23 @@ asyncio.run(main()) ``` */ -function random_float($min, $max) -{ - return ($min + lcg_value() * (abs($max - $min))); -} function fetch_url($url) { print("~ executing fetch_url($url)" . \EOL); - $sleep = random_float(1, 5); - $t = \microtime(true); - yield \sleep_for($sleep); - print("time of fetch_url($url): " . (\microtime(true) - $t) . 's' . \EOL); - return "fake page html for $url" . \EOL; + \timer_for($url); + yield \sleep_for(\random_uniform(1, 5)); + print("time of fetch_url($url): " . \timer_for($url) . 's' . \EOL); + return "fake page html for $url"; }; function analyze_sentiment($html) { print("~ executing analyze_sentiment('$html')" . \EOL); - $sleep = random_float(1, 5); - $t = \microtime(true); - yield \sleep_for($sleep); - $r = "positive: " . \random_float(0, 1); - print("time of analyze_sentiment('$html'): " . (\microtime(true) - $t) . 's' . \EOL); + \timer_for($html . '.url'); + yield \sleep_for(\random_uniform(1, 5)); + $r = "positive: " . \random_uniform(0, 1); + print("time of analyze_sentiment('$html'): " . \timer_for($html . '.url') . 's' . \EOL); return $r; }; @@ -132,17 +126,18 @@ function main() ]; $urlID = []; - $t = \microtime(true); + + \timer_for(); foreach ($urls as $url) $urlID[] = yield \away(handle_url($url)); $result_data = yield \gather($urlID); foreach ($result_data as $id => $extracted_data) { echo "> extracted data:"; - print_r($extracted_data); + \print_r($extracted_data); } - print("time elapsed: " . ((\microtime(true) - $t)) . 's'); + print("time elapsed: " . \timer_for() . 's'); } \coroutine_run(main()); diff --git a/tests/CoroutineSignalerTest.php b/tests/CoroutineSignalerTest.php index 19d3b52..78b5405 100644 --- a/tests/CoroutineSignalerTest.php +++ b/tests/CoroutineSignalerTest.php @@ -60,10 +60,9 @@ public function testSignalsKeepTheLoopRunningAndRemovingItStopsTheLoop() private function assertRunFasterThan($maxInterval) { - $start = microtime(true); + \timer_for(); $this->loop->run(); - $end = microtime(true); - $interval = $end - $start; + $interval = \timer_for(); $this->assertLessThan($maxInterval, $interval); } } diff --git a/tests/KernelTest.php b/tests/KernelTest.php index 56ef138..d365186 100644 --- a/tests/KernelTest.php +++ b/tests/KernelTest.php @@ -224,11 +224,11 @@ public function lapse(int $taskId = null) public function taskSleepFor() { - $t0 = \microtime(true); - $done = yield Kernel::sleepFor(1, 'done sleeping'); - $t1 = \microtime(true); + \timer_for('true'); + $done = yield Kernel::sleepFor(\random_uniform(1, 1), 'done sleeping'); + $t1 = \timer_for('true'); $this->assertEquals('done sleeping', $done); - $this->assertGreaterThan(.9, (float) ($t1 - $t0)); + $this->assertGreaterThan(.9, $t1); yield \shutdown(); }