From 7570909516d6aa6e59230bd55dce10f8e17357fa Mon Sep 17 00:00:00 2001 From: David Findley Date: Sat, 26 Mar 2022 19:09:18 -0500 Subject: [PATCH 1/2] Implement auto dump on out-of-memory Two new INI settings have been added called `meminfo.dump_on_limit` and `meminfo.dump_dir`. When dump_on_limit is enabled, meminfo will attempt to create a heap dump when an OOM error is detected in the `dump_dir` directory. OOM errors are detected by registering an error handler, then checking for a prefix of "Allowed memory size of" on the error message. --- README.md | 43 +++++++--- extension/meminfo.c | 105 ++++++++++++++++++++++--- extension/php_meminfo.h | 39 ++++++++- extension/tests/00-dump-oom.phpt | 18 +++++ extension/tests/dump-oom-confirm.phpt | 18 +++++ extension/tests/dump-private-prop.phpt | 70 +++++++++++++++++ 6 files changed, 270 insertions(+), 23 deletions(-) create mode 100644 extension/tests/00-dump-oom.phpt create mode 100644 extension/tests/dump-oom-confirm.phpt create mode 100644 extension/tests/dump-private-prop.phpt diff --git a/README.md b/README.md index 96a5dfb..42444f4 100644 --- a/README.md +++ b/README.md @@ -44,20 +44,28 @@ $ cd analyzer $ composer install ``` -Usage ------ -## Dumping memory content +## Usage +### Dumping memory content ```php meminfo_dump(fopen('/tmp/my_dump_file.json', 'w')); - ``` This function generates a dump of the PHP memory in a JSON format. This dump can be later analyzed by the provided analyzers. This function takes a stream handle as a parameter. It allows you to specify a file (ex `fopen('/tmp/file.txt', 'w')`, as well as to use standard output with the `php://stdout` stream. -## Displaying a summary of items in memory +### Enable dump on limit +The ini settings `dump_on_limit` and `dump_dir` can be used to enable automatic heap dumps on OOM. + +```ini +meminfo.dump_on_limit = On; Defaults Off +meminfo.dump_dir = /tmp; Will write a file /tmp/php_heap_.json +``` + +Note: xdebug may interfere with the error callback used to detect an OOM error. + +### Displaying a summary of items in memory ```bash $ bin/analyzer summary @@ -65,7 +73,7 @@ Arguments: dump-file PHP Meminfo Dump File in JSON format ``` -### Example +#### Example ```bash $ bin/analyzer summary /tmp/my_dump_file.json +----------+-----------------+-----------------------------+ @@ -80,7 +88,7 @@ $ bin/analyzer summary /tmp/my_dump_file.json +----------+-----------------+-----------------------------+ ``` -## Displaying a list of objects with the largest number of children +### Displaying a list of objects with the largest number of children ```bash $ bin/analyzer top-children [options] [--] @@ -91,7 +99,7 @@ Options: -l, --limit[=LIMIT] limit [default: 5] ``` -### Example +#### Example ```bash $ bin/analyzer top-children /tmp/my_dump_file.json +-----+----------------+----------+ @@ -103,10 +111,21 @@ $ bin/analyzer top-children /tmp/my_dump_file.json | 4 | 0x7fffeab63ca0 | 3605 | | 5 | 0x7fffd3161400 | 2400 | +-----+----------------+----------+ +``` +### Visualizing The Heap as a Treemap +[php-meminfo-treemap](https://gitlab.com/findley/php-meminfo-treemap) can be +used to generate a browser based treemap visualization powered by google +charts. The caveat is that the heap dump is a graph, so you must select a root +node to render a treemap. + +#### Example +```bash +php-meminfo-treemap heap.json 0x7fe7d2d65020 -o treemap.html ``` +![](https://gitlab.com/findley/php-meminfo-treemap/-/raw/master/docs/meminfo-treechart.png) -## Querying the memory dump to find specific objects +### Querying the memory dump to find specific objects ```bash $ bin/analyzer query [options] [--] @@ -119,7 +138,7 @@ Options: -v Increase the verbosity ``` -### Example +#### Example ```bash $ bin/analyzer query -v -f "class=MyClassA" -f "is_root=0" /tmp/php_mem_dump.json @@ -144,7 +163,7 @@ $ bin/analyzer query -v -f "class=MyClassA" -f "is_root=0" /tmp/php_mem_dump.jso ``` -## Displaying the reference path +### Displaying the reference path The reference path is the path between a specific item in memory (identified by its pointer address) and all the intermediary items up to the one item that is attached to a variable still alive in the program. @@ -163,7 +182,7 @@ Options: -v Increase the verbosity ``` -### Example +#### Example ```bash $ bin/analyzer ref-path -v 0x7f94a1877068 /tmp/php_mem_dump.json diff --git a/extension/meminfo.c b/extension/meminfo.c index b91b5b5..2859453 100644 --- a/extension/meminfo.c +++ b/extension/meminfo.c @@ -2,8 +2,8 @@ #include "config.h" #endif -#include "php.h" #include "php_meminfo.h" +#include "php_ini.h" #include "ext/standard/info.h" #include "ext/standard/php_string.h" @@ -36,15 +36,80 @@ zend_module_entry meminfo_module_entry = { STANDARD_MODULE_HEADER, "meminfo", meminfo_functions, + PHP_MINIT(meminfo), + PHP_MSHUTDOWN(meminfo), NULL, NULL, + PHP_MINFO(meminfo), + MEMINFO_VERSION, + PHP_MODULE_GLOBALS(meminfo), + PHP_GINIT(meminfo), NULL, NULL, - NULL, - MEMINFO_VERSION, - STANDARD_MODULE_PROPERTIES + STANDARD_MODULE_PROPERTIES_EX }; +PHP_GINIT_FUNCTION(meminfo) +{ + meminfo_globals->dump_on_limit = 0; +} + +PHP_MINFO_FUNCTION(meminfo) +{ + DISPLAY_INI_ENTRIES(); +} + +#if PHP_VERSION_ID < 70200 /* PHP 7.1 */ +static void meminfo_zend_error_cb(int type, const char* error_filename, const uint error_lineno, const char* format, va_list args) +#elif PHP_VERSION_ID < 80000 /* PHP 7.2 - 7.4 */ +static void meminfo_zend_error_cb(int type, const char* error_filename, const uint32_t error_lineno, const char* format, va_list args) +#elif PHP_VERSION_ID < 80100 /* PHP 8.0 */ +static void meminfo_zend_error_cb(int type, const char* error_filename, const uint32_t error_lineno, zend_string* message) +#else /* PHP 8.1 */ +static void meminfo_zend_error_cb(int type, zend_string* error_filename, const uint32_t error_lineno, zend_string* message) +#endif +{ +#if PHP_VERSION_ID < 80000 + const char* msg = format; +#else + const char* msg = ZSTR_VAL(message); +#endif + + if (EXPECTED(!should_autodump(type, msg))) { + original_zend_error_cb(MEMINFO_ZEND_ERROR_CB_ARGS_PASSTHRU); + return; + } + + zend_set_memory_limit((size_t)Z_L(-1) >> (size_t)Z_L(1)); + + char outfile[500]; + sprintf(outfile, "%s/php_heap_%d.json", INI_STR("meminfo.dump_dir"), (int)time(NULL)); + + php_stream* stream = php_stream_fopen(outfile, "w", NULL); + perform_dump(stream); + + zend_set_memory_limit(PG(memory_limit)); + original_zend_error_cb(MEMINFO_ZEND_ERROR_CB_ARGS_PASSTHRU); +} + +PHP_MINIT_FUNCTION(meminfo) +{ + REGISTER_INI_ENTRIES(); + + original_zend_error_cb = zend_error_cb; + zend_error_cb = meminfo_zend_error_cb; + + return SUCCESS; +} + +PHP_MSHUTDOWN_FUNCTION(meminfo) +{ + UNREGISTER_INI_ENTRIES(); + + zend_error_cb = original_zend_error_cb; + + return SUCCESS; +} /** * Generate a JSON output of the list of items in memory (objects, arrays, string, etc...) @@ -53,21 +118,24 @@ zend_module_entry meminfo_module_entry = { PHP_FUNCTION(meminfo_dump) { zval *zval_stream; - - int first_element = 1; - php_stream *stream; - HashTable visited_items; if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &zval_stream) == FAILURE) { return; } + php_stream_from_zval(stream, zval_stream); + + perform_dump(stream); +} +void perform_dump(php_stream* stream) +{ + int first_element = 1; + + HashTable visited_items; zend_hash_init(&visited_items, 1000, NULL, NULL, 0); - php_stream_from_zval(stream, zval_stream); php_stream_printf(stream, "{\n"); - php_stream_printf(stream, " \"header\" : {\n"); php_stream_printf(stream, " \"memory_usage\" : %zd,\n", zend_memory_usage(0)); php_stream_printf(stream, " \"memory_usage_real\" : %zd,\n", zend_memory_usage(1)); @@ -552,6 +620,23 @@ zend_string * meminfo_escape_for_json(const char *s) return s3; } +#define MEMORY_LIMIT_ERROR_PREFIX "Allowed memory size of" +static zend_bool should_autodump(int error_type, const char* message) { + if (EXPECTED(error_type != E_ERROR)) { + return 0; + } + + if (EXPECTED(!MEMINFO_G(dump_on_limit))) { + return 0; + } + + if (EXPECTED(strncmp(MEMORY_LIMIT_ERROR_PREFIX, message, strlen(MEMORY_LIMIT_ERROR_PREFIX)) != 0)) { + return 0; + } + + return 1; +} + #ifdef COMPILE_DL_MEMINFO #ifdef ZTS ZEND_TSRMLS_CACHE_DEFINE(); diff --git a/extension/php_meminfo.h b/extension/php_meminfo.h index 5b3e399..01f309f 100644 --- a/extension/php_meminfo.h +++ b/extension/php_meminfo.h @@ -1,20 +1,39 @@ #ifndef PHP_MEMINFO_H #define PHP_MEMINFO_H 1 +#include "php.h" + extern zend_module_entry meminfo_module_entry; #define phpext_meminfo_ptr &meminfo_module_entry #define MEMINFO_NAME "PHP Meminfo" #define MEMINFO_VERSION "2.0.0-beta1" #define MEMINFO_AUTHOR "Benoit Jacquemont" -#define MEMINFO_COPYRIGHT "Copyright (c) 2010-2021 by Benoit Jacquemont & contributors" +#define MEMINFO_COPYRIGHT "Copyright (c) 2010-2021 by Benoit Jacquemont & contributors" #define MEMINFO_COPYRIGHT_SHORT "Copyright (c) 2010-2021" +ZEND_BEGIN_MODULE_GLOBALS(meminfo) + zend_bool dump_on_limit; +ZEND_END_MODULE_GLOBALS(meminfo) + +static ZEND_DECLARE_MODULE_GLOBALS(meminfo) +#define MEMINFO_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(meminfo, v) + PHP_FUNCTION(meminfo_dump); +PHP_MSHUTDOWN_FUNCTION(meminfo); +PHP_MINIT_FUNCTION(meminfo); +PHP_MINFO_FUNCTION(meminfo); +PHP_GINIT_FUNCTION(meminfo); + +PHP_INI_BEGIN() +STD_PHP_INI_ENTRY("meminfo.dump_on_limit", "Off", PHP_INI_ALL, OnUpdateBool, dump_on_limit, zend_meminfo_globals, meminfo_globals) +PHP_INI_ENTRY("meminfo.dump_dir", "/tmp", PHP_INI_ALL, NULL) +PHP_INI_END() zend_ulong meminfo_get_element_size(zval* z); // Functions to browse memory parts to record item +void perform_dump(php_stream* stream); void meminfo_browse_exec_frames(php_stream *stream, HashTable *visited_items, int *first_element); void meminfo_browse_class_static_members(php_stream *stream, HashTable *visited_items, int *first_element); @@ -28,6 +47,24 @@ void meminfo_build_frame_label(char * frame_label, int frame_label_len, zend_exe zend_string * meminfo_escape_for_json(const char *s); +static zend_bool should_autodump(int error_type, const char* message); + +// Function pointer to original error handler +// See https://www.phpinternalsbook.com/php7/extensions_design/hooks.html +#if PHP_VERSION_ID < 70200 /* PHP 7.1 */ +static void (*original_zend_error_cb)(int type, const char* error_filename, const uint error_lineno, const char* format, va_list args); +#define MEMINFO_ZEND_ERROR_CB_ARGS_PASSTHRU type, error_filename, error_lineno, format, args +#elif PHP_VERSION_ID < 80000 /* PHP 7.2 - 7.4 */ +static void (*original_zend_error_cb)(int type, const char* error_filename, const uint32_t error_lineno, const char* format, va_list args); +#define MEMINFO_ZEND_ERROR_CB_ARGS_PASSTHRU type, error_filename, error_lineno, format, args +#elif PHP_VERSION_ID < 80100 /* PHP 8.0 */ +static void (*original_zend_error_cb)(int type, const char* error_filename, const uint32_t error_lineno, zend_string* message); +#define MEMINFO_ZEND_ERROR_CB_ARGS_PASSTHRU type, error_filename, error_lineno, message +#else /* PHP 8.1 */ +static void (*original_zend_error_cb)(int type, zend_string* error_filename, const uint32_t error_lineno, zend_string* message); +#define MEMINFO_ZEND_ERROR_CB_ARGS_PASSTHRU type, error_filename, error_lineno, message +#endif + extern zend_module_entry meminfo_entry; #endif diff --git a/extension/tests/00-dump-oom.phpt b/extension/tests/00-dump-oom.phpt new file mode 100644 index 0000000..5d05ac1 --- /dev/null +++ b/extension/tests/00-dump-oom.phpt @@ -0,0 +1,18 @@ +--TEST-- +Trigger PHP OOM +--FILE-- + +--FILE-- +pub_str = $pub; + $this->priv_str = $priv; + } + } + + $my_class_instance = new MyClass('public', 'private'); + + meminfo_dump($dump); + + rewind($dump); + $meminfoData = json_decode(stream_get_contents($dump), true); + fclose($dump); + + $myArrayDump = []; + + foreach ($meminfoData['items'] as $item) { + if (isset($item['symbol_name']) && $item['symbol_name'] == 'my_class_instance') { + $myArrayDump = $item; + } + } + + echo "Symbol: ".$myArrayDump['symbol_name']."\n"; + echo " Frame:".$myArrayDump['frame']."\n"; + echo " Type:".$myArrayDump['type']."\n"; + echo " Is root:".$myArrayDump['is_root']."\n"; + + echo " Children:\n"; + + foreach ($myArrayDump['children'] as $key => $child) { + echo " Key: ".$key."\n"; + echo " Type:"; + $type = $meminfoData['items'][$child]['type']; + if ('int' === $type) { + echo "integer"; + } elseif ('bool' === $type) { + echo "boolean"; + } else { + echo $type; + } + echo "\n"; + echo " Is root:".$meminfoData['items'][$child]['is_root']."\n"; + } +?> +--EXPECT-- +Symbol: my_class_instance + Frame: + Type:object + Is root:1 + Children: + Key: pub_str + Type:string + Is root: + Key: priv_str + Type:string + Is root: From 5e94543a229403d638d90e4e35c4f7c77a1c69bd Mon Sep 17 00:00:00 2001 From: David Findley Date: Tue, 11 Oct 2022 22:34:33 -0500 Subject: [PATCH 2/2] Convert errant tab intention into spaces --- extension/meminfo.c | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/extension/meminfo.c b/extension/meminfo.c index 2859453..c1c1e76 100644 --- a/extension/meminfo.c +++ b/extension/meminfo.c @@ -51,7 +51,7 @@ zend_module_entry meminfo_module_entry = { PHP_GINIT_FUNCTION(meminfo) { - meminfo_globals->dump_on_limit = 0; + meminfo_globals->dump_on_limit = 0; } PHP_MINFO_FUNCTION(meminfo) @@ -70,15 +70,15 @@ static void meminfo_zend_error_cb(int type, zend_string* error_filename, const u #endif { #if PHP_VERSION_ID < 80000 - const char* msg = format; + const char* msg = format; #else - const char* msg = ZSTR_VAL(message); + const char* msg = ZSTR_VAL(message); #endif - if (EXPECTED(!should_autodump(type, msg))) { - original_zend_error_cb(MEMINFO_ZEND_ERROR_CB_ARGS_PASSTHRU); - return; - } + if (EXPECTED(!should_autodump(type, msg))) { + original_zend_error_cb(MEMINFO_ZEND_ERROR_CB_ARGS_PASSTHRU); + return; + } zend_set_memory_limit((size_t)Z_L(-1) >> (size_t)Z_L(1)); @@ -622,19 +622,19 @@ zend_string * meminfo_escape_for_json(const char *s) #define MEMORY_LIMIT_ERROR_PREFIX "Allowed memory size of" static zend_bool should_autodump(int error_type, const char* message) { - if (EXPECTED(error_type != E_ERROR)) { - return 0; - } + if (EXPECTED(error_type != E_ERROR)) { + return 0; + } - if (EXPECTED(!MEMINFO_G(dump_on_limit))) { - return 0; - } + if (EXPECTED(!MEMINFO_G(dump_on_limit))) { + return 0; + } - if (EXPECTED(strncmp(MEMORY_LIMIT_ERROR_PREFIX, message, strlen(MEMORY_LIMIT_ERROR_PREFIX)) != 0)) { - return 0; - } + if (EXPECTED(strncmp(MEMORY_LIMIT_ERROR_PREFIX, message, strlen(MEMORY_LIMIT_ERROR_PREFIX)) != 0)) { + return 0; + } - return 1; + return 1; } #ifdef COMPILE_DL_MEMINFO