Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.7] Add getSocketResource method to enhance socket management #17

Merged
merged 9 commits into from
Mar 18, 2024

Conversation

divinity76
Copy link

@divinity76 divinity76 commented Mar 17, 2024

This method is a prerequisite for eventually resolving chrome-php/chrome#606

Here is my investigation notes leading up to this PR:
class \HeadlessChromium\Communication\Connection has a protected \HeadlessChromium\Communication\Socket\SocketInterface $wsClient;

\HeadlessChromium\Communication\Socket\SocketInterface has no getSocketResource().

\HeadlessChromium\Communication\Socket\Wrench must then create its own getSocketResource(),
returning the resource from protected \Wrench\Client $client;
\Wrench\Client needs a getSocketResource(), returning it's socket resource from protected \Wrench\Socket\ClientSocket $socket;

\Wrench\Socket\ClientSocket inherits $this->socket property from UriSocket...

\Wrench\Socket\UriSocket inherits $this->socket property from \Wrench\Socket\AbstractSocket...

\Wrench\Socket\AbstractSocket has a public function getResource()

that means UriSocket has it too... that means ClientSocket also has it.. \Wrench\Client needs to expose it publicly (or at least protectedly)
it needs a
public function getSocketResource(){return $this->socket->getResource();}

@GrahamCampbell GrahamCampbell changed the base branch from 1.5 to 1.6 March 17, 2024 21:14
@GrahamCampbell GrahamCampbell changed the title Add getSocketResource method to enhance socket management [1.6] Add getSocketResource method to enhance socket management Mar 17, 2024
@GrahamCampbell
Copy link
Member

Exposing this underlying resource feels kinda wrong. What did you need to do with the resource? I wonder if it'd be better to expose a couple of methods here to deal with that, rather than leaking the internals.

@divinity76
Copy link
Author

divinity76 commented Mar 17, 2024

@GrahamCampbell it's a long story, and the TL;DR is that in https://github.com/chrome-php/chrome/blob/1.11/src/Communication/Connection.php I want to replace

    /**
     * Wait before sending next message.
     */
    private function waitForDelay(): void
    {
        if ($this->lastMessageSentTime) {
            $currentTime = (int) (\hrtime(true) / 1000 / 1000);
            // if not enough time was spent until last message was sent, wait
            if ($this->lastMessageSentTime + $this->delay > $currentTime) {
                $timeToWait = ($this->lastMessageSentTime + $this->delay) - $currentTime;
                \usleep($timeToWait * 1000);
            }
        }

        $this->lastMessageSentTime = (int) (\hrtime(true) / 1000 / 1000);
    }

with

    /**
     * Wait before sending next message.
     */
    private function waitForDelay(): void
    {
        if ($this->lastMessageSentTime) {
            $currentTime = (int) (\hrtime(true) / 1000 / 1000);
            // if not enough time was spent until last message was sent, wait
            if ($this->lastMessageSentTime + $this->delay > $currentTime) {
                $timeToWait = ($this->lastMessageSentTime + $this->delay) - $currentTime;
                $seconds = (int)floor($timeToWait);
                $nanoseconds = (int)(($timeToWait - $seconds) * 1e9);
                $null = null;
                $read = [$this->wsClient->getSocketResource()];
                socket_select($read, $null, $null, $seconds, $nanoseconds);
            }
        }
        $this->lastMessageSentTime = (int) (\hrtime(true) / 1000 / 1000);
    }
  • or something to that effect. Could make a select() method instead of a getSocketResource() method instead.

That's the short version anyway... The full version is: I wanted to make video recordings with chrome-php/chrome, and at first I tried just spamming screenshot(), and while that kindof works, it is way too slow and resource-intensive to make smooth video recordings of animations. Then I tried using Page.startScreencast + method:Page.screencastFrame instead, and that was a huge improvement over spamming screenshot() ! While much better than screenshot() spam, it still didn't produce 100% smooth recordings of animations.

I started investigating why, and I tried replacing stuff like

$navigate->waitForNavigation(\HeadlessChromium\Page::NETWORK_IDLE);

with

$target_time = microtime(true) + 5;
while (microtime(true) < $target_time) {
    // calling readData() fast causes smooth video recordings.
    // if there is too much delay in sending Page.screencastFrameAck, chromium will drop frames.
    $page->getSession()->getConnection()->readData();
}

and Yeah! Now i could make completely smooth video recordings! but it was a huge waste of CPU to busy loop like this.

Then I started investigating why I needed that loop, and it turns out that if you don't respond to a Page.screencastFrame with a Page.screencastFrameAck fast enough, Chrome will start dropping frames until you respond. That causes un-smooth choppy video recordings.

I investigated why waitForNavigation was too slow, and eventually concluded that the culprit is the usleep() in waitForDelay(), and concluded that the correct fix is to replace the usleep() with a socket_select(), and thus this PR.

@divinity76
Copy link
Author

Perhaps we can make a PHP-alternative to NodeJS' timecut :)

@GrahamCampbell
Copy link
Member

I wonder if it would be better to add a waitOnData() method instead of getSocketResource().

    public function waitOnData(float $maxSeconds): void
    {
        $seconds = (int)floor($timeToWait);
        $nanoseconds = (int)(($timeToWait - $seconds) * 1e9);
        $null = null;
        socket_select($this->socket, $null, $null, $seconds, $nanoseconds);
    }
    private function waitForDelay(): void
    {
        if ($this->lastMessageSentTime) {
            $currentTime = \hrtime(true) / 1000 / 1000;
            $timeToWait = (float) ($this->lastMessageSentTime + $this->delay) - $currentTime;
            // if not enough time was spent until last message was sent, wait
            if ($timeToWait > PHP_FLOAT_EPSILON)
                $this->wsClient->waitForData($timeToWait);
            }
        }
        $this->lastMessageSentTime = (int) (\hrtime(true) / 1000 / 1000);
    }

That way we're not breaking the abstraction and calling socket_select over here.

@GrahamCampbell GrahamCampbell changed the title [1.6] Add getSocketResource method to enhance socket management [1.7] Add getSocketResource method to enhance socket management Mar 18, 2024
@divinity76
Copy link
Author

divinity76 commented Mar 18, 2024

I wonder if it would be better to add a waitOnData() method

sounds good! Where would it be appropriate to be defined tho? \Wrench\Client or \Wrench\Socket\ClientSocket or \Wrench\Socket\UriSocket or \Wrench\Socket\AbstractSocket ? 🤔

Edit: Just realized a mistake in my example code above, select() takes $seconds + $MICROseconds , not $NANOseconds, so the correct code is

    $seconds = (int)floor($timeToWait);
    $microseconds = (int)(($timeToWait - $seconds) * 1e6);
(...)
    socket_select($read, $null, $null, $seconds, $microseconds);

@divinity76
Copy link
Author

@GrahamCampbell added a public function waitForData(float $maxSeconds): bool in \Wrench\Client, please re-review.

@GrahamCampbell GrahamCampbell changed the base branch from 1.6 to 1.7 March 18, 2024 11:36
@GrahamCampbell GrahamCampbell merged commit 47aa368 into chrome-php:1.7 Mar 18, 2024
6 of 7 checks passed
@GrahamCampbell
Copy link
Member

Thanks.

@divinity76
Copy link
Author

divinity76 commented Mar 18, 2024

@GrahamCampbell Any ETA for a 1.7 release? kinda itching to make progress on chrome-php/chrome#606

@GrahamCampbell
Copy link
Member

You can make progress by using ^1.7@dev for now. ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants