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();
}