1
0
mirror of https://github.com/Seldaek/monolog.git synced 2025-07-31 18:30:15 +02:00

Simplify memoryIniValueToBytes, tweak code to use less memory overall

This commit is contained in:
Jordi Boggiano
2021-09-14 13:44:02 +02:00
parent 0b22036ab6
commit 70fe092867
4 changed files with 54 additions and 97 deletions

View File

@@ -26,9 +26,7 @@ use Monolog\Utils;
class StreamHandler extends AbstractProcessingHandler class StreamHandler extends AbstractProcessingHandler
{ {
/** @const int */ /** @const int */
const SAFE_MEMORY_OFFSET = 1024; protected const MAX_CHUNK_SIZE = 100 * 1024 * 1024;
/** @const int */
const MAX_CHUNK_SIZE = 2147483647;
/** @var int */ /** @var int */
protected $streamChunkSize = self::MAX_CHUNK_SIZE; protected $streamChunkSize = self::MAX_CHUNK_SIZE;
/** @var resource|null */ /** @var resource|null */
@@ -55,25 +53,21 @@ class StreamHandler extends AbstractProcessingHandler
{ {
parent::__construct($level, $bubble); parent::__construct($level, $bubble);
if ($phpMemoryLimit = ini_get('memory_limit')) { if (($phpMemoryLimit = Utils::expandIniShorthandBytes(ini_get('memory_limit'))) !== false) {
if (($memoryInByes = Utils::memoryIniValueToBytes($phpMemoryLimit))) { if ($phpMemoryLimit > 0) {
$memoryUsage = memory_get_usage(true); // use max 10% of allowed memory for the chunk size
if (($memoryInByes - $memoryUsage) < $this->streamChunkSize) { $this->streamChunkSize = max((int) ($phpMemoryLimit / 10), 10*1024);
$this->streamChunkSize = $memoryInByes - $memoryUsage - self::SAFE_MEMORY_OFFSET;
}
} }
// else memory is unlimited, keep the buffer to the default 100MB
} else {
// no memory limit information, use a conservative 10MB
$this->streamChunkSize = 10*10*1024;
} }
if (is_resource($stream)) { if (is_resource($stream)) {
$this->stream = $stream; $this->stream = $stream;
try { stream_set_chunk_size($this->stream, $this->streamChunkSize);
stream_set_chunk_size($this->stream, $this->streamChunkSize);
} catch (\Exception $exception) {
throw new \RuntimeException('Impossible to set the stream chunk size.'
.PHP_EOL.'Error: '.$exception->getMessage()
.PHP_EOL.'Trace: '.$exception->getTraceAsString());
}
} elseif (is_string($stream)) { } elseif (is_string($stream)) {
$this->url = Utils::canonicalizePath($stream); $this->url = Utils::canonicalizePath($stream);
} else { } else {
@@ -119,7 +113,7 @@ class StreamHandler extends AbstractProcessingHandler
/** /**
* @return int * @return int
*/ */
public function getStreamChunkSize() : int public function getStreamChunkSize(): int
{ {
return $this->streamChunkSize; return $this->streamChunkSize;
} }

View File

@@ -229,41 +229,27 @@ final class Utils
/** /**
* Converts a string with a valid 'memory_limit' format, to bytes. * Converts a string with a valid 'memory_limit' format, to bytes.
* Reference: Function code from https://www.php.net/manual/en/function.ini-get.php *
* @param string|int $val * @param string|false $val
* @return int|false Returns an integer representing bytes. Returns FALSE in case of error. * @return int|false Returns an integer representing bytes. Returns FALSE in case of error.
*/ */
public static function memoryIniValueToBytes($val) public static function expandIniShorthandBytes($val)
{ {
if (!is_string($val) && !is_integer($val)) { if (!is_string($val)) {
return false; return false;
} }
$val = trim((string)$val); // support -1
if ((int) $val < 0) {
return (int) $val;
}
if (empty($val)) { if (!preg_match('/^\s*(?<val>\d+)(?:\.\d+)?\s*(?<unit>[gmk]?)\s*$/i', $val, $match)) {
return false; return false;
} }
$valLen = strlen($val); $val = (int) $match['val'];
$last = strtolower($val[$valLen - 1]); switch (strtolower($match['unit'] ?? '')) {
if (preg_match('/[a-zA-Z]/', $last)) {
if ($valLen == 1) {
return false;
}
$val = substr($val, 0, -1);
}
if (!is_numeric($val) || $val < 0) {
return false;
}
//Lets be explicit here
$val = (int)($val);
switch ($last) {
case 'g': case 'g':
$val *= 1024; $val *= 1024;
case 'm': case 'm':

View File

@@ -225,15 +225,15 @@ class StreamHandlerTest extends TestCase
public function provideMemoryValues() public function provideMemoryValues()
{ {
return [ return [
['1M', true], ['1M', (int) (1024*1024/10)],
['10M', true], ['10M', (int) (1024*1024)],
['1024M', true], ['1024M', (int) (1024*1024*1024/10)],
['1G', true], ['1G', (int) (1024*1024*1024/10)],
['2000M', true], ['2000M', (int) (2000*1024*1024/10)],
['2050M', true], ['2050M', (int) (2050*1024*1024/10)],
['2048M', true], ['2048M', (int) (2048*1024*1024/10)],
['3G', false], ['3G', (int) (3*1024*1024*1024/10)],
['2560M', false], ['2560M', (int) (2560*1024*1024/10)],
]; ];
} }
@@ -241,52 +241,28 @@ class StreamHandlerTest extends TestCase
* @dataProvider provideMemoryValues * @dataProvider provideMemoryValues
* @return void * @return void
*/ */
public function testPreventOOMError($phpMemory, $chunkSizeDecreased) public function testPreventOOMError($phpMemory, $expectedChunkSize)
{ {
$memoryLimit = ini_set('memory_limit', $phpMemory); $previousValue = ini_set('memory_limit', $phpMemory);
if ($memoryLimit === false) { if ($previousValue === false) {
/* $this->markTestSkipped('We could not set a memory limit that would trigger the error.');
* We could not set a memory limit that would trigger the error.
* There is no need to continue with this test.
*/
$this->assertTrue(true);
return;
} }
$stream = tmpfile(); $stream = tmpfile();
if ($stream === false) { if ($stream === false) {
/* $this->markTestSkipped('We could not create a temp file to be use as a stream.');
* We could create a temp file to be use as a stream.
* There is no need to continue with this test.
*/
$this->assertTrue(true);
return;
} }
$exceptionRaised = false; $exceptionRaised = false;
try { $handler = new StreamHandler($stream);
$handler = new StreamHandler($stream); stream_get_contents($stream, 1024);
stream_get_contents($stream, 1024);
} catch (\RuntimeException $exception) {
/*
* At this point, stream_set_chunk_size() failed in the constructor.
* Probably because not enough memory.
* The rest of th test depends on the success pf stream_set_chunk_size(), that is why
* if exception is raised (which is true at this point), the rest of assertions will not be executed.
*/
$exceptionRaised = true;
}
ini_set('memory_limit', $memoryLimit); ini_set('memory_limit', $previousValue);
$this->assertEquals($memoryLimit, ini_get('memory_limit'));
if (!$exceptionRaised) { $this->assertEquals($expectedChunkSize, $handler->getStreamChunkSize());
$this->assertEquals($chunkSizeDecreased, $handler->getStreamChunkSize() < StreamHandler::MAX_CHUNK_SIZE);
}
} }
/** /**
@@ -297,8 +273,7 @@ class StreamHandlerTest extends TestCase
$previousValue = ini_set('memory_limit', '2048M'); $previousValue = ini_set('memory_limit', '2048M');
if ($previousValue === false) { if ($previousValue === false) {
$this->assertTrue(true); $this->markTestSkipped('We could not set a memory limit that would trigger the error.');
return;
} }
$stream = tmpfile(); $stream = tmpfile();

View File

@@ -142,16 +142,18 @@ class UtilsTest extends \PHPUnit_Framework_TestCase
]; ];
} }
public function provideMemoryIniValuesToConvertToBytes() public function provideIniValuesToConvertToBytes()
{ {
return [ return [
['1', 1], ['1', 1],
['2', 2], ['2', 2],
['2.5', 2], ['2.5', 2],
['2.9', 2], ['2.9', 2],
['1B', 1], ['1B', false],
['1X', 1], ['1X', false],
['1K', 1024], ['1K', 1024],
['1 K', 1024],
[' 5 M ', 5*1024*1024],
['1G', 1073741824], ['1G', 1073741824],
['', false], ['', false],
[null, false], [null, false],
@@ -161,11 +163,11 @@ class UtilsTest extends \PHPUnit_Framework_TestCase
['BB', false], ['BB', false],
['G', false], ['G', false],
['GG', false], ['GG', false],
['-1', false], ['-1', -1],
['-123', false], ['-123', -123],
['-1A', false], ['-1A', -1],
['-1B', false], ['-1B', -1],
['-123G', false], ['-123G', -123],
['-B', false], ['-B', false],
['-A', false], ['-A', false],
['-', false], ['-', false],
@@ -175,13 +177,13 @@ class UtilsTest extends \PHPUnit_Framework_TestCase
} }
/** /**
* @dataProvider provideMemoryIniValuesToConvertToBytes * @dataProvider provideIniValuesToConvertToBytes
* @param mixed $input * @param mixed $input
* @param int|false $expected * @param int|false $expected
*/ */
public function testMemoryIniValueToBytes($input, $expected) public function testExpandIniShorthandBytes($input, $expected)
{ {
$result = Utils::memoryIniValueToBytes($input); $result = Utils::expandIniShorthandBytes($input);
$this->assertEquals($expected, $result); $this->assertEquals($expected, $result);
} }
} }