Skip to content

Commit 6bbf4fe

Browse files
committed
Merge pull request #20 from BitOne/frame_label
Adding frame and root information on items.
2 parents a9e0eef + e81732a commit 6bbf4fe

36 files changed

+949
-511
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
2016-03-15 Benoit Jacquemont <[email protected]>
2+
* Added frame and symbol information
3+
* Added analyzers to find path that kept references
4+
15
2016-01-26 Benoit Jacquemont <[email protected]>
26
* Added the object handle to uniquely identify objects
37

@@ -7,7 +11,5 @@
711
2014-02-08 Benoit Jacquemont <[email protected]>
812
* Migrating from Google Code to GitHub
913

10-
1114
2011-01-16 Benoit Jacquemont <[email protected]>
12-
1315
* Extension creation.

README.md

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ MEMINFO
22
=======
33
PHP Meminfo is a PHP extension that gives you insights on the PHP memory content.
44

5-
Its main goal is to help you understand memory leaks, but by looking at data present in memory, you can better understand your application behaviour.
5+
Its main goal is to help you understand memory leaks: by looking at data present in memory, you can better understand your application behaviour.
66

77
One of the main source of inspiration for this tool is the Java jmap tool with the -histo option (see `man jmap`).
88

@@ -13,6 +13,7 @@ Compiled and tested on:
1313
- PHP 5.4.4 (Debian 7)
1414
- PHP 5.5.8 (Ubuntu 12.04 LTS)
1515
- PHP 5.5.20 (CentOS 7)
16+
- PHP 5.6.17 (Debian 8)
1617

1718
Compilation instructions
1819
------------------------
@@ -24,7 +25,7 @@ $ apt-get install php5-dev
2425
Once you have this command, follow this steps:
2526

2627
## Compilation
27-
From the root of the extension directory:
28+
From the root of the `extension/` directory:
2829

2930
```bash
3031
$ phpize
@@ -40,31 +41,18 @@ Add the following line to your `php.ini`:
4041
extension=meminfo.so
4142
```
4243

43-
Usage
44-
-----
45-
All meminfo functions take a stream handle as a parameter. It allows you to specify a file, as well as to use standard output with the `php://stdout` stream.
46-
47-
## Exploring Memory usage
48-
The provided user interface allows you to explore the content of your memory. It will show you the items instanciated, the dependencies between items and the size of each of them.
49-
50-
### Summary Screen
51-
The main screen lists the top memory consumers, ordered by their total size.
52-
![summary screen](doc/images/ui_summary.png)
53-
54-
### Item details Screen
55-
Clicking on an item brings you to the Item Details Screen.
56-
![summary screen](doc/images/ui_item_details.png)
57-
58-
On this screen, you will see 5 parts:
59-
- title
60-
Item type (object, array, string, boolean, etc...) and its pointer address in memory at the time of the data extraction.
61-
This memory address is used as the unique identifier of the item
44+
Installing analyzers
45+
--------------------
46+
Analyzers allow to analyze a memory dump (see below).
6247

63-
- Information
64-
Small summary on the item itself, like the class name in case of object and memory size information.
48+
```bash
49+
$ cd analyzers
50+
$ composer update
51+
```
6552

66-
### How memory size is computed
67-
See [the dedicated documentation](/doc/memory_calculation.md).
53+
Usage
54+
-----
55+
All meminfo functions take 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.
6856

6957
## Object instances count per class
7058
Display the number of instances per class, ordered descending. Very useful to identify the content of a memory leak.
@@ -73,7 +61,7 @@ Display the number of instances per class, ordered descending. Very useful to id
7361
meminfo_objects_summary(fopen('php://stdout','w'));
7462
```
7563

76-
The result will provide something similar to the following example (generated at the end of the Symfony2 console launch)
64+
The result will provide something similar to the following example generated at the end of a Symfony2 console launch:
7765

7866
```
7967
Instances count by class:
@@ -98,6 +86,49 @@ The `examples/` directory at the root of the repository contains more detailed e
9886
$ php examples/objects_summary.php
9987
```
10088

89+
## Memory state dump
90+
This feature allow to dump the list of items present in memory at the time of the function execution. Each memory items (string, boolean, objects, array, etc...) are dumped in a JSON format, with the following information:
91+
- in memory address
92+
- type (object, array, int, string, ...)
93+
- class (only for objects)
94+
- object handle (only for objects.
95+
- self size (without the size of the linked objects)
96+
- is_root (tells if the item is directly linked to a variable)
97+
- symbol name (variable name, if linked to a variable)
98+
- execution frame (name of the method where the variable has been declared)
99+
- children: list of linked items with the key value if array or property name if object and the item address in memory
100+
101+
### Analyzing a memory dump
102+
The analyzer is available from the `analyzer/` directory. It will be invoked with:
103+
``` bash
104+
$ bin/analyzer
105+
```
106+
107+
#### Querying a memory dump
108+
The `query` command on the analyzer allows you to filter out some items from a memory dump. The `-f` option can be used several times, effectively *anding* the filters. The supported operators are exact match `=` and regexp match `~`.
109+
110+
The `-v`option display all the information of the items found.
111+
112+
##### Examples
113+
- finding array that are not directly linked to a variable
114+
```bash
115+
$ bin/analyzer query -f "type=array" -f "is_root=0" my_dump_file.json
116+
```
117+
- finding objects whose class name contains `Product` and linked to a variable
118+
```bash
119+
$ bin/analyzer query -f "class~Product" -f "is_root=1" -v my_dump_file.json
120+
```
121+
122+
#### Finding out why an object has not been removed from memory
123+
When you are tracking down a memory leak, it's very interesting to understand why an object is still in memory.
124+
125+
The analyzer provides the `ref-path` command that load the memory dump as a graph in memory and findout all paths linking an item to a root (a variable define in an execution frame).
126+
127+
Without the `-v` option, the output will contains only item memory adress and key/property name. Adding the `-v` option will display all the information of the linked items.
128+
129+
```bash
130+
$ bin/analyzer ref-path my_dump_file.json 0x12345678
131+
```
101132

102133
## List of items in memory
103134
Provides a list of items in memory (objects, arrays, string, etc.) with their sizes.

analyzer/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor/
2+
/composer.lock

analyzer/bin/analyzer

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
require_once __DIR__ . '/../vendor/autoload.php';
5+
6+
$analyzer = new BitOne\PhpMemInfo\Console\Application();
7+
$analyzer->run();

analyzer/composer.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "bitone/php-meminfo-analyzer",
3+
"license": "MIT",
4+
"type": "project",
5+
"description": "Tools to analyze meminfo dump files",
6+
"require": {
7+
"symfony/console" : "^2.8.3",
8+
"symfony/filesystem" : "^2.8.3",
9+
"symfony/serializer" : "^2.8.3",
10+
"clue/graph": "^0.9.0",
11+
"graphp/algorithms": "^0.8.1"
12+
},
13+
"autoload": {
14+
"psr-4": { "": "src/" }
15+
},
16+
"require-dev": {
17+
"squizlabs/php_codesniffer": "^2.5",
18+
"phpspec/phpspec": "^2.4",
19+
"fabpot/php-cs-fixer": "^1.11"
20+
}
21+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
namespace BitOne\PhpMemInfo\Analyzer;
4+
5+
use Fhaculty\Graph\Graph;
6+
use Graphp\Algorithms\ShortestPath\BreadthFirst;
7+
8+
/**
9+
* Analyzer to load the data as a graph and analyze the graph.
10+
*
11+
* @author Benoit Jacquemont <[email protected]>
12+
* @copyright 2016 Benoit Jacquemont
13+
* @license http://opensource.org/licenses/MIT MIT
14+
*/
15+
class GraphBrowser
16+
{
17+
/** @var array */
18+
protected $items;
19+
20+
/** @var Graph */
21+
protected $graph;
22+
23+
/**
24+
* @param array $items
25+
*/
26+
public function __construct(array $items)
27+
{
28+
$this->items = $items;
29+
}
30+
31+
/**
32+
* Find all references coming from roots.
33+
*
34+
* @param string $itemid
35+
*
36+
* @return array
37+
*/
38+
public function findReferencePaths($itemId)
39+
{
40+
$graph = $this->getGraph();
41+
42+
$to = $graph->getVertex($itemId);
43+
44+
$paths = [];
45+
46+
$mapSearch = new BreadthFirst($to);
47+
48+
$map = $mapSearch->getEdgesMap();
49+
50+
foreach ($map as $endVertexId => $path) {
51+
$endVertex = $graph->getVertex($endVertexId);
52+
$endVertexData = $endVertex->getAttribute('data');
53+
54+
if ($endVertexData['is_root']) {
55+
$paths[$endVertexId] = $path;
56+
}
57+
}
58+
59+
if ($to->getAttribute('data')['is_root']) {
60+
$edge = $to->createEdgeTo($to);
61+
$edge->setAttribute('name', '<self>');
62+
$paths[$to->getId()] = [$edge];
63+
}
64+
65+
return $paths;
66+
}
67+
68+
/**
69+
* Build the graph if necessary and return it.
70+
*
71+
* @param Graph
72+
*/
73+
protected function getGraph()
74+
{
75+
if (null === $this->graph) {
76+
$this->buildGraph();
77+
}
78+
79+
return $this->graph;
80+
}
81+
82+
/**
83+
* Build the graph from the items.
84+
*/
85+
protected function buildGraph()
86+
{
87+
$this->graph = new Graph();
88+
$this->createVertices();
89+
$this->createEdges();
90+
}
91+
92+
/**
93+
* Create vertices on the graph from items.
94+
*/
95+
protected function createVertices()
96+
{
97+
foreach ($this->items as $itemId => $itemData) {
98+
$vertex = $this->graph->createVertex($itemId);
99+
$vertex->setAttribute('data', $itemData);
100+
}
101+
}
102+
103+
/**
104+
* Create edges on the graph between vertices.
105+
*/
106+
protected function createEdges()
107+
{
108+
foreach ($this->items as $itemId => $itemData) {
109+
if (isset($itemData['children'])) {
110+
$children = $itemData['children'];
111+
$vertex = $this->graph->getVertex($itemId);
112+
113+
foreach ($children as $link => $child) {
114+
$childVertex = $this->graph->getVertex($child);
115+
$childVertex->createEdgeTo($vertex)->setAttribute('name', $link);
116+
}
117+
}
118+
}
119+
}
120+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace BitOne\PhpMemInfo\Analyzer;
4+
5+
/**
6+
* Analyzer to do simple query on the data.
7+
*
8+
* @author Benoit Jacquemont <[email protected]>
9+
* @copyright 2016 Benoit Jacquemont
10+
* @license http://opensource.org/licenses/MIT MIT
11+
*/
12+
class QueryExecutor
13+
{
14+
/** @var array */
15+
protected $items;
16+
17+
/**
18+
* @param array $items
19+
*/
20+
public function __construct(array $items)
21+
{
22+
$this->items = $items;
23+
24+
foreach ($this->items as $itemid => &$itemData) {
25+
$itemData['id'] = $itemid;
26+
}
27+
}
28+
29+
/**
30+
* Query the existing items.
31+
*
32+
* @param array $filters
33+
* @param int $limit
34+
*
35+
* @return array
36+
*/
37+
public function executeQuery(array $filters, $limit = 10)
38+
{
39+
$results = [];
40+
41+
foreach ($this->items as $itemId => $itemData) {
42+
if ($this->matches($itemData, $filters)) {
43+
$results[$itemId] = $itemData;
44+
}
45+
46+
if (count($results) >= $limit) {
47+
break;
48+
}
49+
}
50+
51+
return $results;
52+
}
53+
54+
/**
55+
* Apply filters on an item and return if it is a match or not.
56+
*
57+
* @param array $item
58+
* @param array $filters
59+
*
60+
* @return bool
61+
*/
62+
public function matches(array $item, array $filters)
63+
{
64+
$matches = true;
65+
66+
foreach ($filters as $attribute => $filter) {
67+
$operator = $filter['operator'];
68+
$value = $filter['value'];
69+
70+
if (!isset($item[$attribute])) {
71+
$matches = false;
72+
} else {
73+
switch ($operator) {
74+
case '=':
75+
$matches = ($item[$attribute] == $value);
76+
break;
77+
case '~':
78+
$pattern = sprintf('#%s#', $value);
79+
$regexpResult = preg_match($pattern, $item[$attribute]);
80+
if (false === $regexpResult) {
81+
throw new \InvalidArgumentException("Provided regexp is invalid: $value");
82+
}
83+
$matches = (1 === $regexpResult);
84+
break;
85+
}
86+
}
87+
88+
if (!$matches) {
89+
break;
90+
}
91+
}
92+
93+
return $matches;
94+
}
95+
}

0 commit comments

Comments
 (0)