2019-01-21 21:36:59 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Amp\Loop\Internal;
|
|
|
|
|
|
|
|
use Amp\Loop\Watcher;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Uses a binary tree stored in an array to implement a heap.
|
|
|
|
*/
|
2019-10-30 15:41:46 +01:00
|
|
|
final class TimerQueue
|
2019-01-21 21:36:59 +01:00
|
|
|
{
|
2020-11-14 17:44:06 +01:00
|
|
|
/** @var Watcher[] */
|
2020-09-25 05:17:13 +02:00
|
|
|
private array $data = [];
|
2019-01-21 21:36:59 +01:00
|
|
|
|
2019-10-30 15:41:46 +01:00
|
|
|
/** @var int[] */
|
2020-09-25 05:17:13 +02:00
|
|
|
private array $pointers = [];
|
2019-10-30 15:41:46 +01:00
|
|
|
|
2020-11-03 17:23:45 +01:00
|
|
|
/**
|
|
|
|
* @param int $node Rebuild the data array from the given node upward.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
2020-11-04 00:27:49 +01:00
|
|
|
private function heapifyUp(int $node): void
|
2020-11-03 17:23:45 +01:00
|
|
|
{
|
|
|
|
$entry = $this->data[$node];
|
|
|
|
while ($node !== 0 && $entry->expiration < $this->data[$parent = ($node - 1) >> 1]->expiration) {
|
2020-11-14 17:44:06 +01:00
|
|
|
$this->swap($node, $parent);
|
2020-11-03 17:23:45 +01:00
|
|
|
$node = $parent;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int $node Rebuild the data array from the given node downward.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
2020-11-04 00:27:49 +01:00
|
|
|
private function heapifyDown(int $node): void
|
2020-11-03 17:23:45 +01:00
|
|
|
{
|
|
|
|
$length = \count($this->data);
|
|
|
|
while (($child = ($node << 1) + 1) < $length) {
|
|
|
|
if ($this->data[$child]->expiration < $this->data[$node]->expiration
|
|
|
|
&& ($child + 1 >= $length || $this->data[$child]->expiration < $this->data[$child + 1]->expiration)
|
|
|
|
) {
|
|
|
|
// Left child is less than parent and right child.
|
|
|
|
$swap = $child;
|
|
|
|
} elseif ($child + 1 < $length && $this->data[$child + 1]->expiration < $this->data[$node]->expiration) {
|
|
|
|
// Right child is less than parent and left child.
|
|
|
|
$swap = $child + 1;
|
|
|
|
} else { // Left and right child are greater than parent.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2020-11-14 17:44:06 +01:00
|
|
|
$this->swap($node, $swap);
|
|
|
|
$node = $swap;
|
|
|
|
}
|
|
|
|
}
|
2020-11-03 17:23:45 +01:00
|
|
|
|
2020-11-14 17:52:36 +01:00
|
|
|
private function swap(int $left, int $right): void
|
2020-11-14 17:44:06 +01:00
|
|
|
{
|
|
|
|
$temp = $this->data[$left];
|
2020-11-03 17:23:45 +01:00
|
|
|
|
2020-11-14 17:44:06 +01:00
|
|
|
$this->data[$left] = $this->data[$right];
|
|
|
|
$this->pointers[$this->data[$right]->id] = $left;
|
2020-11-03 17:23:45 +01:00
|
|
|
|
2020-11-14 17:44:06 +01:00
|
|
|
$this->data[$right] = $temp;
|
|
|
|
$this->pointers[$temp->id] = $right;
|
2020-11-03 17:23:45 +01:00
|
|
|
}
|
|
|
|
|
2019-01-21 21:36:59 +01:00
|
|
|
/**
|
|
|
|
* Inserts the watcher into the queue. Time complexity: O(log(n)).
|
|
|
|
*
|
|
|
|
* @param Watcher $watcher
|
2019-10-30 15:41:46 +01:00
|
|
|
*
|
2020-03-28 21:55:44 +01:00
|
|
|
* @psalm-param Watcher<int> $watcher
|
|
|
|
*
|
2019-10-30 15:41:46 +01:00
|
|
|
* @return void
|
2019-01-21 21:36:59 +01:00
|
|
|
*/
|
2020-10-03 05:26:10 +02:00
|
|
|
public function insert(Watcher $watcher): void
|
2019-01-21 21:36:59 +01:00
|
|
|
{
|
2020-07-14 21:45:35 +02:00
|
|
|
\assert($watcher->expiration !== null);
|
|
|
|
\assert(!isset($this->pointers[$watcher->id]));
|
|
|
|
|
2019-01-21 21:36:59 +01:00
|
|
|
$node = \count($this->data);
|
2020-11-14 17:44:06 +01:00
|
|
|
$this->data[$node] = $watcher;
|
2019-10-30 15:41:46 +01:00
|
|
|
$this->pointers[$watcher->id] = $node;
|
2019-01-21 21:36:59 +01:00
|
|
|
|
2020-11-03 17:23:45 +01:00
|
|
|
$this->heapifyUp($node);
|
2019-01-21 21:36:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-10-31 21:41:07 +01:00
|
|
|
* Removes the given watcher from the queue. Time complexity: O(log(n)).
|
2019-01-21 21:36:59 +01:00
|
|
|
*
|
|
|
|
* @param Watcher $watcher
|
2019-10-30 15:41:46 +01:00
|
|
|
*
|
2020-03-28 21:55:44 +01:00
|
|
|
* @psalm-param Watcher<int> $watcher
|
|
|
|
*
|
2019-10-30 15:41:46 +01:00
|
|
|
* @return void
|
2019-01-21 21:36:59 +01:00
|
|
|
*/
|
2020-10-03 05:26:10 +02:00
|
|
|
public function remove(Watcher $watcher): void
|
2019-01-21 21:36:59 +01:00
|
|
|
{
|
2019-10-30 15:41:46 +01:00
|
|
|
$id = $watcher->id;
|
|
|
|
|
|
|
|
if (!isset($this->pointers[$id])) {
|
|
|
|
return;
|
2019-01-21 21:36:59 +01:00
|
|
|
}
|
2019-10-30 15:41:46 +01:00
|
|
|
|
|
|
|
$this->removeAndRebuild($this->pointers[$id]);
|
2019-01-21 21:36:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-10-30 15:41:46 +01:00
|
|
|
* Deletes and returns the Watcher on top of the heap if it has expired, otherwise null is returned.
|
|
|
|
* Time complexity: O(log(n)).
|
|
|
|
*
|
|
|
|
* @param int $now Current loop time.
|
2019-01-21 21:36:59 +01:00
|
|
|
*
|
2019-10-30 15:41:46 +01:00
|
|
|
* @return Watcher|null Expired watcher at the top of the heap or null if the watcher has not expired.
|
2020-03-28 21:55:44 +01:00
|
|
|
*
|
|
|
|
* @psalm-return Watcher<int>|null
|
2019-01-21 21:36:59 +01:00
|
|
|
*/
|
2020-10-03 05:26:10 +02:00
|
|
|
public function extract(int $now): ?Watcher
|
2019-01-21 21:36:59 +01:00
|
|
|
{
|
2019-10-30 15:41:46 +01:00
|
|
|
if (empty($this->data)) {
|
2019-10-31 21:41:07 +01:00
|
|
|
return null;
|
2019-01-21 21:36:59 +01:00
|
|
|
}
|
|
|
|
|
2020-11-14 17:44:06 +01:00
|
|
|
$watcher = $this->data[0];
|
2019-01-21 21:36:59 +01:00
|
|
|
|
2020-11-14 17:44:06 +01:00
|
|
|
if ($watcher->expiration > $now) {
|
2019-10-30 15:41:46 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->removeAndRebuild(0);
|
|
|
|
|
2020-11-14 17:44:06 +01:00
|
|
|
return $watcher;
|
2019-01-21 21:36:59 +01:00
|
|
|
}
|
|
|
|
|
2019-10-31 21:41:07 +01:00
|
|
|
/**
|
|
|
|
* Returns the expiration time value at the top of the heap. Time complexity: O(1).
|
|
|
|
*
|
|
|
|
* @return int|null Expiration time of the watcher at the top of the heap or null if the heap is empty.
|
|
|
|
*/
|
2020-10-03 05:26:10 +02:00
|
|
|
public function peek(): ?int
|
2019-10-31 21:41:07 +01:00
|
|
|
{
|
2019-11-11 20:23:14 +01:00
|
|
|
return isset($this->data[0]) ? $this->data[0]->expiration : null;
|
2019-10-31 21:41:07 +01:00
|
|
|
}
|
|
|
|
|
2019-01-21 21:36:59 +01:00
|
|
|
/**
|
2020-11-03 17:23:45 +01:00
|
|
|
* @param int $node Remove the given node and then rebuild the data array.
|
2019-01-21 21:36:59 +01:00
|
|
|
*
|
2019-10-30 15:41:46 +01:00
|
|
|
* @return void
|
2019-01-21 21:36:59 +01:00
|
|
|
*/
|
2020-10-03 05:26:10 +02:00
|
|
|
private function removeAndRebuild(int $node): void
|
2019-01-21 21:36:59 +01:00
|
|
|
{
|
|
|
|
$length = \count($this->data) - 1;
|
2020-11-14 17:44:06 +01:00
|
|
|
$id = $this->data[$node]->id;
|
2019-10-30 15:41:46 +01:00
|
|
|
$left = $this->data[$node] = $this->data[$length];
|
2020-11-14 17:44:06 +01:00
|
|
|
$this->pointers[$left->id] = $node;
|
2019-10-30 15:41:46 +01:00
|
|
|
unset($this->data[$length], $this->pointers[$id]);
|
2019-01-21 21:36:59 +01:00
|
|
|
|
2020-11-03 17:23:45 +01:00
|
|
|
if ($node < $length) { // don't need to do anything if we removed the last element
|
|
|
|
$parent = ($node - 1) >> 1;
|
|
|
|
if ($parent >= 0 && $this->data[$node]->expiration < $this->data[$parent]->expiration) {
|
|
|
|
$this->heapifyUp($node);
|
|
|
|
} else {
|
|
|
|
$this->heapifyDown($node);
|
2019-01-21 21:36:59 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|