From 953312bf44de805a007687a626557d5b3fb67944 Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Sat, 7 Nov 2026 12:44:57 +0100 Subject: [PATCH] Read files for a backup in pieces of 4 MiB instead of 8 KiB PHP reads a file 8 KiB at a time by default. On a disk where every request waits, such as a network or cloud volume, that made reading five times slower: on the test server 5 MiB/s instead of 27 MiB/s. Without PHP's read buffer every read of a blob is one request to the disk. The same for packs on a local location. --- CHANGELOG.md | 3 +++ lib/Backend/LocalBackend.php | 3 +++ lib/Job/BackupRun.php | 2 +- lib/Repository/Repository.php | 22 +++++++++++++++++++++- psalm-baseline.xml | 1 - 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eab931b..b2f5509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ shown under 1.0.0. The git tag of a release candidate goes after the date. - The key page warns against creating a new key when you are restoring a server. ### Changed +- Reading files for a backup is much faster on disks where every request waits, such as network and + cloud volumes: files are read in pieces of 4 MiB instead of 8 KiB. On a test server the disk + delivered 27 MiB/s instead of 5 MiB/s. - Locations are backed up one after the other: a backup that has to wait shows as queued. A restore point has the time its backup really began, not the time it was queued. - Choosing a time no longer starts a backup right away when that time has already passed today. diff --git a/lib/Backend/LocalBackend.php b/lib/Backend/LocalBackend.php index 266b517..f7cc96a 100644 --- a/lib/Backend/LocalBackend.php +++ b/lib/Backend/LocalBackend.php @@ -67,6 +67,9 @@ final class LocalBackend implements IBackend { if ($fh === false) { throw new BackendException('Not found: ' . $path); } + // Packs are read in large pieces: one request each, not 8 KiB at a time (see Repository::openForReading()). + stream_set_read_buffer($fh, 0); + stream_set_chunk_size($fh, 4 * 1048576); return $fh; } diff --git a/lib/Job/BackupRun.php b/lib/Job/BackupRun.php index fc4921a..19dbc51 100644 --- a/lib/Job/BackupRun.php +++ b/lib/Job/BackupRun.php @@ -120,7 +120,7 @@ final class BackupRun { if ($cur === null || $cur['path'] !== $logical) { $cur = ['path' => $logical, 'offset' => 0, 'blobs' => [], 'version' => $version]; } - $fh = @fopen($path, 'rb'); + $fh = Repository::openForReading($path); if ($fh === false) { self::recordUnreadable($state, $stats, $logical); $state['cur'] = null; diff --git a/lib/Repository/Repository.php b/lib/Repository/Repository.php index 8f05b2b..f485746 100644 --- a/lib/Repository/Repository.php +++ b/lib/Repository/Repository.php @@ -193,6 +193,23 @@ final class Repository { return $config; } + /** + * Open a local file to read it in whole blobs. PHP reads a file in pieces of 8 KiB by default; + * on a disk where each request waits (a network or cloud volume) that made reading five times + * slower: 5 MiB/s instead of 27 MiB/s on nc01. Without PHP's read buffer every fread() of a + * blob is one request to the disk. + * + * @return resource|false + */ + public static function openForReading(string $path) { + $fh = @fopen($path, 'rb'); + if ($fh !== false) { + stream_set_read_buffer($fh, 0); + stream_set_chunk_size($fh, self::BLOB_SIZE); + } + return $fh; + } + private static function configMatches(array $config, KeyRing $keys): bool { try { return isset($config['check'], $config['id']) @@ -1148,7 +1165,10 @@ final class Repository { for ($attempt = 1; $attempt <= 3; $attempt++) { $before = self::fileVersion($path); $blobs = []; - $fh = fopen($path, 'rb'); + $fh = self::openForReading($path); + if ($fh === false) { + throw new RepositoryException('Cannot read ' . $path); + } while (($data = self::readFull($fh, self::BLOB_SIZE)) !== '') { $blobs[] = $this->storeBlob($data, $packs, $stats); } diff --git a/psalm-baseline.xml b/psalm-baseline.xml index cbdc470..0bacdb4 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -367,7 +367,6 @@ -