Skip to content

Snapshot #11

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

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft

Snapshot #11

wants to merge 2 commits into from

Conversation

arnaud-lb
Copy link
Owner

@arnaud-lb arnaud-lb commented Mar 7, 2025

This implements snapshotting of the process's state. The snapshot can be restored in a subsequent request, to save initialization time.

This adds two functions:

function snapshot_state(): void;

/* Return true if the state was restored */
function restore_state(): bool;

Example usage:

if (!restore_state()) {
    $kernel = new Kernel();
    $kernel->boot();
    snapshot_state();
}

$kernel->handle();

What is restored

  • Declared classes, functions, constants
  • Global variables, static properties, static variables
  • Error and exception handlers
  • Autoloaders
  • List of included files (for require_once)

Internal objects

Internal objects don't need special support as long as they use only the zend heap (e.g. no native allocations, kernel resources). However they need to be marked as such. The current branch marks a few internal classes as safe because these are used in the symfony-demo benchmark. snapshot_state() will throw if an unsupported object is found.

Snapshots are private to a php instance / process, so it may be possible to support objects with native allocations and kernel resources. In some case it may be acceptable that the state of the native allocation / kernel resource changes after the snapshot (e.g. for streams or database connection), but for others it might not (e.g. DOM objects), in which case we may need to separate/clone the resource.

Design

The basic idea is that we copy the heap to a separate buffer. Later, we restore the state by copying back the buffer to the old location. We don't have to copy/relocate individual zvals and other state: We just copy heap chunks. We then update the global symbol table.

Snapshotting

We make a copy of every heap chunk (2MiB each), and prevent the heap from releasing these chunks later (to reserve the address space).

copies := []
for chunk in heap.chunks:
    copies[] := (chunk, memdup(chunk, MM_CHUNK_SIZE))
    chunk.preserve := true

We also copy symbol tables (variables, classes, functions, constants). Symbols are allocated either in the heap or in opcache SHM, so we only copy the hashtables, not the symbols themselves.

Restoring

When restoring, we just restore chunk copies to their old location, and add the old chunks back to the heap. We then restore the symbol tables.

for (chunk, copy) in copies:
    memcpy(chunk, copy, MM_CHUNK_SIZE)
    heap.chunks[] = chunk

Thoughts

Snapshots are private to each php instance, as the location of the heap is specific to each instance (this is important because when we restore a snapshot, we want to do so at the original memory location).

Sharing snapshots is possible if a fixed memory region is reserved in the parent process, like the opcache SHM. But this wouldn't work for ZTS, as each php instance needs separate regions anyway. Also, instance-private snapshots are convenient to support snapshotting of internal objects (see above).

One issue with raw-copying heap chunks is that some slots will be reported as leaks after a restore, in debug builds, but this is fixable.

A less brute-force approach would copy/relocate every individual zval/object/etc to a new heap separately, in a similar fashion as zend_persist.c. CoW objects such as arrays and strings could be moved to the native heap, to reduce restoring costs further. It would result in a smaller heap (reduces restoring cost). However, relocating would increase maintenance, as every internal object needs to know how to clone itself to a new location.

Performance

Benchmark

  • Request /en/blog/ on the Symfony Demo app
  • With Nginx+PHP-FPM vs FrankenPHP
  • 100 concurrent requests
  • nprocs*2 threads/processes

Results

PHP-FPM with snapshotting is slower than FrankenPHP. How much slower depends on the snapshot size and of when it's taken. Best results were obtained when taking a snapshot just after the first request:

Request duration (95 percentile; lower is better)
base:       87.090000; +0.00%
snapshot:   75.020000; -13.86%
frankenphp: 71.280000; -18.15%

Requests per second (higher is better)
base:       1420.509395; +0.00%
snapshot:   1655.731823; +16.56%
frankenphp: 1698.379295; +19.56%

Restoring the state takes about 14% of the time in this benchmark. This could possibly be reduced by a few percent.

Taking the snapshot just after booting the kernel has smaller improvements compared to baseline:

Request duration (95 percentile; lower is better)
base:       86.830000; +0.00%
snapshot:   84.630000; -2.53%
frankenphp: 71.620000; -17.52%

Requests per second (higher is better)
base:       1420.183333; +0.00%
snapshot:   1472.085336; +3.65%
frankenphp: 1695.029720; +19.35%

Initializing all container services before snapshot is slower than baseline due to the cost of restoring state.

More benchmarking / analysis is needed.

Eliminate snapshot restoration cost by using a MAP_PRIVATE / copy-on-write
mapping.

To prevent object destruction from faulting most of our mapping, we skip
snapshotted objects in zend_objects_store_call_destructors(), unless they have
destructors.

Credit for these ideas belong to Bob.

We also increase the refcount of snapshotted global variables and objects to
prevent freeing them, which would also generate a lot of page faults.

Co-authored-by: Bob Weinand <[email protected]>
arnaud-lb pushed a commit that referenced this pull request Mar 31, 2025
```
ext/gd/libgd/gd.c:2275:14: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
    #0 0x5d6a2103e1db in php_gd_gdImageCopy /home/dcarlier/Contribs/php-src/ext/gd/libgd/gd.c:2275
    #1 0x5d6a210a2b63 in gdImageCrop /home/dcarlier/Contribs/php-src/ext/gd/libgd/gd_crop.c:57
    #2 0x5d6a21018ca4 in zif_imagecrop /home/dcarlier/Contribs/php-src/ext/gd/gd.c:3575
    #3 0x5d6a21e46e7a in ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER /home/dcarlier/Contribs/php-src/Zend/zend_vm_execute.h:1337
    #4 0x5d6a221188da in execute_ex /home/dcarlier/Contribs/php-src/Zend/zend_vm_execute.h:57246
    #5 0x5d6a221366bd in zend_execute /home/dcarlier/Contribs/php-src/Zend/zend_vm_execute.h:61634
    #6 0x5d6a21d107a6 in zend_execute_scripts /home/dcarlier/Contribs/php-src/Zend/zend.c:1895
    #7 0x5d6a21a63409 in php_execute_script /home/dcarlier/Contribs/php-src/main/main.c:2529
    #8 0x5d6a22516d5e in do_cli /home/dcarlier/Contribs/php-src/sapi/cli/php_cli.c:966
    #9 0x5d6a2251981d in main /home/dcarlier/Contribs/php-src/sapi/cli/php_cli.c:1341
    #10 0x7f10d002a3b7 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #11 0x7f10d002a47a in __libc_start_main_impl ../csu/libc-start.c:360
    php#12 0x5d6a20a06da4 in _start (/home/dcarlier/Contribs/php-src/sapi/cli/php+0x2806da4) (BuildId: d9a79c7e0e4872311439d7313cb3a81fe04190a2)
```

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

Successfully merging this pull request may close these issues.

1 participant