Skip to content

Commit ee9fe9e

Browse files
ondrejmirtesclaude
andcommitted
Stream the result cache to disk instead of building it in memory
Building the whole var_export()ed contents of the result cache file as one string took up roughly twice the size of the resulting file in the main process at the end of every analysis - the file can reach hundreds of megabytes on large projects. The big sections are now streamed to the file entry by entry. var_export() builds the entire export in memory even when asked to print it, so each entry is exported wrapped in a single-entry array whose "array (\n" prefix and "\n)" suffix are stripped, which yields output byte-identical to exporting the whole section at once. In self-analysis this lowers the main process peak during the result cache save from 276 MB to 190 MB, removing the spike above the size of the retained data entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DjgMHgfvwhD4ogdTaRtLp
1 parent d62655c commit ee9fe9e

1 file changed

Lines changed: 83 additions & 14 deletions

File tree

‎src/Analyser/ResultCache/ResultCacheManager.php‎

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
use PHPStan\DependencyInjection\GenerateFactory;
1818
use PHPStan\DependencyInjection\ProjectConfigHelper;
1919
use PHPStan\File\CouldNotReadFileException;
20+
use PHPStan\File\CouldNotWriteFileException;
2021
use PHPStan\File\FileFinder;
2122
use PHPStan\File\FileHelper;
22-
use PHPStan\File\FileWriter;
2323
use PHPStan\Internal\ArrayHelper;
2424
use PHPStan\Internal\ComposerHelper;
2525
use PHPStan\PhpDoc\StubFilesProvider;
@@ -36,7 +36,11 @@
3636
use function array_unique;
3737
use function array_values;
3838
use function count;
39+
use function error_get_last;
3940
use function explode;
41+
use function fclose;
42+
use function fopen;
43+
use function fwrite;
4044
use function get_loaded_extensions;
4145
use function getenv;
4246
use function hash_file;
@@ -49,6 +53,7 @@
4953
use function sort;
5054
use function sprintf;
5155
use function str_starts_with;
56+
use function substr;
5257
use function time;
5358
use function unlink;
5459
use function var_export;
@@ -1153,25 +1158,89 @@ private function save(
11531158

11541159
$file = $this->cacheFilePath;
11551160

1156-
FileWriter::write(
1157-
$file,
1158-
"<?php declare(strict_types = 1);
1161+
// streamed to the file section by section - building the whole
1162+
// var_export()ed contents in memory at once would take up roughly
1163+
// twice the size of the resulting file in the main process
1164+
$handle = @fopen($file, 'w');
1165+
if ($handle === false) {
1166+
$error = error_get_last();
1167+
throw new CouldNotWriteFileException($file, $error !== null ? $error['message'] : 'unknown cause');
1168+
}
1169+
1170+
try {
1171+
$this->writeToHandle($handle, $file, "<?php declare(strict_types = 1);
11591172
11601173
return [
11611174
'lastFullAnalysisTime' => " . var_export($lastFullAnalysisTime, true) . ",
11621175
'meta' => " . var_export($meta, true) . ",
11631176
'projectExtensionFiles' => " . var_export($projectExtensionFiles, true) . ",
1164-
'errorsCallback' => static function (): array { return " . var_export($errors, true) . "; },
1165-
'locallyIgnoredErrorsCallback' => static function (): array { return " . var_export($locallyIgnoredErrors, true) . "; },
1166-
'linesToIgnore' => " . var_export($linesToIgnore, true) . ",
1167-
'unmatchedLineIgnores' => " . var_export($unmatchedLineIgnores, true) . ",
1168-
'collectedDataCallback' => static function (): array { return " . var_export($collectedData, true) . "; },
1169-
'dependencies' => " . var_export($invertedDependencies, true) . ",
1170-
'packageDependencies' => " . var_export($packageDependencies, true) . ",
1171-
'exportedNodesCallback' => static function (): array { return " . var_export($exportedNodes, true) . '; },
1177+
'errorsCallback' => static function (): array { return ");
1178+
$this->streamArrayVarExportToHandle($handle, $file, $errors);
1179+
$this->writeToHandle($handle, $file, "; },
1180+
'locallyIgnoredErrorsCallback' => static function (): array { return ");
1181+
$this->streamArrayVarExportToHandle($handle, $file, $locallyIgnoredErrors);
1182+
$this->writeToHandle($handle, $file, "; },
1183+
'linesToIgnore' => ");
1184+
$this->streamArrayVarExportToHandle($handle, $file, $linesToIgnore);
1185+
$this->writeToHandle($handle, $file, ",
1186+
'unmatchedLineIgnores' => ");
1187+
$this->streamArrayVarExportToHandle($handle, $file, $unmatchedLineIgnores);
1188+
$this->writeToHandle($handle, $file, ",
1189+
'collectedDataCallback' => static function (): array { return ");
1190+
$this->streamArrayVarExportToHandle($handle, $file, $collectedData);
1191+
$this->writeToHandle($handle, $file, "; },
1192+
'dependencies' => ");
1193+
$this->streamArrayVarExportToHandle($handle, $file, $invertedDependencies);
1194+
$this->writeToHandle($handle, $file, ",
1195+
'packageDependencies' => ");
1196+
$this->streamArrayVarExportToHandle($handle, $file, $packageDependencies);
1197+
$this->writeToHandle($handle, $file, ",
1198+
'exportedNodesCallback' => static function (): array { return ");
1199+
$this->streamArrayVarExportToHandle($handle, $file, $exportedNodes);
1200+
$this->writeToHandle($handle, $file, '; },
11721201
];
1173-
',
1174-
);
1202+
');
1203+
} finally {
1204+
fclose($handle);
1205+
}
1206+
}
1207+
1208+
/**
1209+
* @param resource $handle
1210+
*/
1211+
private function writeToHandle($handle, string $file, string $contents): void
1212+
{
1213+
if (@fwrite($handle, $contents) === false) {
1214+
$error = error_get_last();
1215+
throw new CouldNotWriteFileException($file, $error !== null ? $error['message'] : 'unknown cause');
1216+
}
1217+
}
1218+
1219+
/**
1220+
* Streams the var_export() representation of an array to the file entry
1221+
* by entry, producing output byte-identical to var_export($values, true).
1222+
*
1223+
* var_export() builds the whole export in memory even when told to print it,
1224+
* so exporting a big section in one call would take up as much memory
1225+
* as the resulting file section itself.
1226+
*
1227+
* Each entry is exported wrapped in a single-entry array whose "array (\n"
1228+
* prefix and "\n)" suffix are stripped, yielding the same bytes (including
1229+
* indentation) the entry would get inside the full export. Indenting the lines
1230+
* of a standalone value export would corrupt multi-line string contents instead.
1231+
*
1232+
* @param resource $handle
1233+
* @param array<mixed> $values
1234+
*/
1235+
private function streamArrayVarExportToHandle($handle, string $file, array $values): void
1236+
{
1237+
$this->writeToHandle($handle, $file, 'array (');
1238+
foreach ($values as $key => $value) {
1239+
$entry = var_export([$key => $value], true);
1240+
$this->writeToHandle($handle, $file, "\n" . substr($entry, 8, -2));
1241+
}
1242+
1243+
$this->writeToHandle($handle, $file, "\n)");
11751244
}
11761245

11771246
/**

0 commit comments

Comments
 (0)