Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement auto dump on out-of-memory #123

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,28 +44,36 @@ $ 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_<timestamp>.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 <dump-file>

Arguments:
dump-file PHP Meminfo Dump File in JSON format
```

### Example
#### Example
```bash
$ bin/analyzer summary /tmp/my_dump_file.json
+----------+-----------------+-----------------------------+
Expand All @@ -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] [--] <dump-file>

Expand All @@ -91,7 +99,7 @@ Options:
-l, --limit[=LIMIT] limit [default: 5]
```

### Example
#### Example
```bash
$ bin/analyzer top-children /tmp/my_dump_file.json
+-----+----------------+----------+
Expand All @@ -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] [--] <dump-file>

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -163,7 +182,7 @@ Options:
-v Increase the verbosity
```

### Example
#### Example

```bash
$ bin/analyzer ref-path -v 0x7f94a1877068 /tmp/php_mem_dump.json
Expand Down
105 changes: 95 additions & 10 deletions extension/meminfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))) {
tortis marked this conversation as resolved.
Show resolved Hide resolved
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...)
Expand All @@ -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));
Expand Down Expand Up @@ -552,6 +620,23 @@ zend_string * meminfo_escape_for_json(const char *s)
return s3;
}

#define MEMORY_LIMIT_ERROR_PREFIX "Allowed memory size of"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no other way to check for memory exhaustion error?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this technique in memprof when I was researching how to do it. Unfortunately I didn't find any other options.

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();
Expand Down
39 changes: 38 additions & 1 deletion extension/php_meminfo.h
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BitOne Do you think we can just let this default to "/tmp" for now? It might not be the best for Windows, but I think most users will probably set the dump_dir if they are going to use this anyway right?

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);

Expand All @@ -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
18 changes: 18 additions & 0 deletions extension/tests/00-dump-oom.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
Trigger PHP OOM
--FILE--
<?php

ini_set("meminfo.dump_on_limit", true);
ini_set("meminfo.dump_dir", __DIR__);

$things = [];
for ($i = 0; $i < 3000; $i++) {
$things []= str_repeat("*", rand(50000, 100000));
}
--EXPECT--
--XFAIL--
This test triggers an OOM error which will write a heap dump into the test
directory. The test dump-oom-confirm.phpt will verify if the heap dump was
written. This test is prefixed with 00 to ensure that it runs before the
confirm test.
18 changes: 18 additions & 0 deletions extension/tests/dump-oom-confirm.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
Confirm heap dump on OOM
--FILE--
<?php

$files = glob(__DIR__ . '/php_heap*.json');
echo count($files);
--EXPECT--
1
--CLEAN--
<?php

$files = glob(__DIR__ . '/php_heap*.json');
foreach($files as $file) {
if(is_file($file)) {
unlink($file);
}
}
Loading