我最近遇到了这个问题,以测试单元测试中的某些函数是否具有内存效率。
它只需要 Xdebug 扩展,因为它在底层使用 Xdebug Profiler,生成和解析与 Kachegrind 相同的跟踪文件。它使用 Xdebug 函数以编程方式根据被测函数按需启动 Profile,然后解析跟踪文件,找到该函数内部的峰值内存执行并将其返回。
/**
* Returns the peak memory usage in bytes during the execution of a given function.
*
* @param callable $callback A callable to the function to check the memory usage.
* @param mixed ...$parameters The arguments to the callable function.
*
* @throws RuntimeException When Xdebug is not available, or
*
* @return int The peak memory usage in bytes that this function consumed during execution.
*/
protected function get_function_memory_usage( callable $callback, ...$parameters ) {
if ( ! function_exists( 'xdebug_stop_trace' ) ) {
throw new RuntimeException('Xdebug is required for this test.');
}
$trace_file = xdebug_start_trace( null, XDEBUG_TRACE_COMPUTERIZED );
call_user_func_array( $callback, $parameters );
xdebug_stop_trace();
$trace_file = new SplFileObject( $trace_file );
$start_context_memory_usage = 0;
$highest_memory_usage = 0;
/*
* A small Xdebug Tracefile analyser that looks for the highest memory allocation
* during the execution of the function
*
* @link https://github.com/xdebug/xdebug/blob/master/contrib/tracefile-analyser.php
*/
while ( $trace_file->valid() ) {
$line = $trace_file->fgets();
if (
preg_match( '@^Version: (.*)@', $line, $matches ) ||
preg_match( '@^File format: (.*)@', $line, $matches ) ||
preg_match( '@^TRACE.*@', $line, $matches )
) {
continue;
}
$trace_entry = explode( "\t", $line );
if ( count( $trace_entry ) < 5 ) {
continue;
}
$memory = (int) $trace_entry[4];
if ( $memory > $highest_memory_usage ) {
$highest_memory_usage = $memory;
}
if ( empty( $start_context_memory_usage ) ) {
$start_context_memory_usage = $memory;
}
}
$memory_used = $highest_memory_usage - $start_context_memory_usage;
return $memory_used;
}
示例结果:
$function_uses_memory = static function( $mbs ) {
$a = str_repeat('a', 1024 * 1024 * $mbs);
};
$memory_usage_5 = get_function_memory_usage( $function_uses_memory, 5 );
$memory_usage_40 = get_function_memory_usage( $function_uses_memory, 40 );
$memory_usage_62 = get_function_memory_usage( $function_uses_memory, 62 );
$memory_usage_30 = get_function_memory_usage( $function_uses_memory, 30 );
// Result:
(
[$memory_usage_5] => 5247000
[$memory_usage_40] => 41947160
[$memory_usage_62] => 65015832
[$memory_usage_30] => 31461400
)
或者,您可以通过仅更改两行来返回使用的内存和结果,以断言两者:
// Store the result after calling the callback
$result = call_user_func_array( $callback, $parameters );
// Return the result along with the memory usage
return [ $result, $memory_used ];
通过这种方式,您可以断言预期的内存使用量,同时给出预期的结果。
如果你打算在测试中使用它,你也可以将这个有用的方法添加到内存特征中:
protected function assertMemoryUsedWithinTolerance( $expected, $equal, $tolerance_percentage ) {
$is_within_tolerance = static function ( $expected, $actual ) use ( $tolerance_percentage ) {
$upper_limit = $expected + ( ( $expected / 100 ) * $tolerance_percentage );
return $actual <= $upper_limit;
};
$failure_message = static function ( $expected, $actual ) use ( $tolerance_percentage ) {
return sprintf( 'Failed to assert that a function uses up to %s bytes (%s) of memory (+%s%%). Actual usage: %s bytes (%s).', $expected, size_format( $expected, 4 ), $tolerance_percentage, $actual, size_format( $actual, 4 ) );
};
$this->assertTrue( $is_within_tolerance( $expected, $equal ), $failure_message( $expected, $equal ) );
}
示例用法:
public function test_memory_usage_parsing_long_string() {
$long_input = str_repeat('a', 1024 * 1024); // 1mb string
[ $result, $memory_used ] = $this->get_function_memory_usage( [$this, 'parse_long_string'], $long_input );
// If it exceeds 2x the size of the input it should fail.
$expected_memory_peak = 1024 * 1024 * 2; // 2mb
$this->assertEquals('foo', $result);
$this->assertMemoryUsedWithinTolerance($expected_memory_peak, $memory_used, 1); // 1% tolerance
}
为什么不register_ticks_function?
在我写完这个答案之后,我还测试了这个问题的另一个答案提到的register_ticks_function,但是,我发现它需要declare(ticks=1) 在被测函数所在的文件以及函数所在的所有文件上under test 使用来自的代码,否则不会触发滴答声。 Xdebug 方法在没有声明的情况下工作,因此它适用于所有文件并跟踪嵌套在被测函数调用深处的内存使用情况。