Don't Wait; Generate!

PHPBenelux 2019

Ian Littman / @iansltx

follow along at https://ian.im/gen19bnl

function translate(string $texan) : string

switch ($texan) {
    case "y'all":
        return "some of you";
    case "all y'all":
        return "all of you";
    default:
        return $texan;
}

We'll Answer

  • What's a Generator?
  • What's a Coroutine?
  • Why async/non-blocking I/O && event loops?
  • Why not?
  • What a Generator + Coroutine based app looks like

This is a generator

function gRange($min, $max, $step): \Generator
{
    for ($i = $min; $i < $max; $i += $step) {
        yield $i;
    }
}

foreach (gRange(12, 82, 5) as $n) {
    echo "$n\n"; // 12, 17, 22, etc.
}

What's going on here?

  • Resumable function
  • Uses yield rather than (or in addition to) return
  • Incremental, iterable results
  • Behaves a bit like an Iterator
  • Values and Exceptions can be sent/thrown in
    (more on this in a moment)

<spooky>This is a generator</spooky>

function spooky(): \Generator
{
    while (true) {
        yield random_int(0, 1) ? 'O' : 'o';
    }
}

foreach (spooky() as $chr) {
    echo $chr; usleep(100000);
}

When can I use it?

  • PHP 5.5+, but 7.0+ gets you...
    • return
    • yield from
    • Throwables
  • Was on HHVM and predecessors before PHP
  • ES2015 (aka ES6) via function*
  • C# (.NET 4.5+)
  • Python 2.2+

Before we look at our next example...

If you call a function that yields, you will not execute that function. You'll get back a Generator object. To execute the function, you need to call methods on the Generator.

 

Also, "return" means something different for a Generator than for a normal function.

Generator

Parent

$g = gen(1);

$a = $g->current();

2

$b = yield $arg1 + 1;

$c = $g->send($a + 1);

$d = yield $b + 2;

5

3

function gen($arg1)

$e = $g->send($c + 1);

6

return $d + 2;

null

echo $g->getReturn();

8

We just did a coroutine.

  • Yield: stop execution until caller restarts it via send()
  • Yield from: pass through execution until yielded-from object has nothing else to yield
  • Return: use just like normal
  • Cooperative multitasking!

Standard PHP-FPM Request Model

Client (or Load Balancer)

Web Server (nginx or Apache)

FastCGI Daemon (php-fpm) + Workers

HTTP

FastCGI

Pros

  • Common
  • Multicore
  • Shared-nothing (safe)
  • Fast for static resources
  • Library support
    • Slim\Http Req\Res
    • Aura.Sql
    • ...basically anything else
  • Don't worry (much) about blocking the thread

Cons

  • No in-request parallelism*
  • Blocking I/O
  • Not memory-efficient
  • Startup penalty on every req
  • Not 12-factor
    • Process manager (runit)
    • nginx
    • php-fpm + workers

* Ignoring curl_multi and wrappers (e.g. Guzzle)

DEMO TIME: ngx + FPM + SLIM 3 APP

Event Loops!

T_PARADIGM_SHIFT

  • Async I/O
  • For compute-heavy operations...
    • Don't do in-process if you can avoid it
    • Don't do all at once if it must be in-process

Callbacks/Promises

  • Very common (used in e.g. ReactPHP), but...
  • Hard to follow execution flow
    • Error callback convention (vs. Exceptions)
    • Messages only at function borders
    • Callback Pyramid of Doom

Why a Generator?

  • Easier to follow
  • Cleaner error handling
  • Pass control inside functions
  • Can still do async!

Generators in an Event Loop

  1. Run until blocking I/O
  2. Yield promise representing blocking I/O
  3. Event loop skips coroutine until promise is resolved (can use yield from inside a coroutine when you want to call another coroutine and wait for it to complete)
  4. Event loop send()s promise result to coroutine
  5. Repeat from 1 until coroutine is complete (return)

Use Node (or Hack)? This should look familar.

  • async functions -> Generators
  • await -> yield

Event Loop Extensions

Amphp http-server Request Model

Client (or Load Balancer)

Application Server (AMPHP)

HTTP

var_dump(is_12_factor()); // bool(true)

Pros

  • No per-request bootstrap time
  • Fewer moving parts (12F app)
  • Async execution
  • Generator based (!pyramid)
  • Async database access
  • Lower memory use per request
  • Fast!

Cons

  • A bit fragile*
  • Requires port match
  • Single-threaded**
  • Plenty to refactor

 

* Throwables to the rescue!
** amphp/cluster to the rescue!

Benchmarks!

  • System under test
    • Vultr 4GB server (2 vCPUs), Amsterdam
    • Ubuntu 18.10, Docker 18.06ce via snap, HTTP-only
    • Current docker-compose setups from GitHub
  • Load generation system
    • Vultr 512MB server, same data center
    • Siege 4.0.4 on Ubuntu 18.10
    • Benchmarked a few times to allow for warm-up
    • Hitting raffle entrants URL w\cookie, one "blank" raffle
    • siege -c 15 -t 30S -b <url> <cookie header> 

Benchmarks! Nginx + PHP-FPM (7.2)

Transactions:               17029 hits
Availability:               100.00 %
Elapsed time:               29.25 secs
Data transferred:           0.68 MB
Response time:              0.03 secs
Transaction rate:           582.19 trans/sec
Throughput:                 0.02 MB/sec
Concurrency:                14.95
Successful transactions:    17029
Failed transactions:        0
Longest transaction:        0.07
Shortest transaction:       0.00

 

~45MB RAM, ~120% CPU

Benchmarks! AMPHP (7.3)

Transactions:               20724 hits
Availability:               99.57 %
Elapsed time:               29.79 secs
Data transferred:           0.96 MB
Response time:              0.02 secs
Transaction rate:           695.67 trans/sec
Throughput:                 0.03 MB/sec
Concurrency:                14.84
Successful transactions:    20724
Failed transactions:        90
Longest transaction:        1.04
Shortest transaction:       0.00

           

~42MB RAM, ~120% CPU

Benchmark Caveats

  • In favor of amphp
    • Relatively low concurrency
    • Didn't turn opcache revalidation completely off on FPM
    • Failing requests didn't automatically disqualify
  • In favor of nginx + fpm
    • Very little I/O (a couple very quick DB calls)
    • Very little I/O latency (DB was on-server)

Thanks! Questions?

Don't Wait; Generate! - PHPBenelux 2019

By Ian Littman

Don't Wait; Generate! - PHPBenelux 2019

Generators, which have been around since PHP 5.5 and got a lot better with PHP 7, take a lot of the angst out of asynchronous programming in PHP. In this talk I'll explain the basic concepts that you'll need to grok generators, then apply our new-found knowledge to turn an I/O-bottlenecked web app into a concurrent, performant one via the AMPHP family of libraries.

  • 1,896