Last active
February 24, 2025 20:52
-
-
Save SerafimArts/dcf64d3211831d1c02a403c104d97f19 to your computer and use it in GitHub Desktop.
PHP Memory Map Visualization
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* ----------------------------------------------------------------------------- | |
* zend_long.h | |
* ----------------------------------------------------------------------------- | |
*/ | |
#ifdef ZEND_ENABLE_ZVAL_LONG64 | |
typedef int64_t zend_long; | |
typedef uint64_t zend_ulong; | |
typedef int64_t zend_off_t; | |
#define ZEND_LONG_MAX INT64_MAX | |
#define ZEND_LONG_MIN INT64_MIN | |
#define ZEND_ULONG_MAX UINT64_MAX | |
#define Z_L(i) INT64_C(i) | |
#define Z_UL(i) UINT64_C(i) | |
#define SIZEOF_ZEND_LONG 8 | |
#else | |
typedef int32_t zend_long; | |
typedef uint32_t zend_ulong; | |
typedef int32_t zend_off_t; | |
#define ZEND_LONG_MAX INT32_MAX | |
#define ZEND_LONG_MIN INT32_MIN | |
#define ZEND_ULONG_MAX UINT32_MAX | |
#define Z_L(i) INT32_C(i) | |
#define Z_UL(i) UINT32_C(i) | |
#define SIZEOF_ZEND_LONG 4 | |
#endif | |
/** | |
* ----------------------------------------------------------------------------- | |
* zend_alloc_sizes.h | |
* ----------------------------------------------------------------------------- | |
*/ | |
// 22: | |
#define ZEND_MM_CHUNK_SIZE ((size_t) (2 * 1024 * 1024)) /* 2 MB */ | |
#define ZEND_MM_PAGE_SIZE (4 * 1024) /* 4 KB */ | |
#define ZEND_MM_PAGES (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) /* 512 */ | |
#define ZEND_MM_FIRST_PAGE (1) | |
#define ZEND_MM_MIN_SMALL_SIZE 8 | |
#define ZEND_MM_MAX_SMALL_SIZE 3072 | |
#define ZEND_MM_MAX_LARGE_SIZE (ZEND_MM_CHUNK_SIZE - (ZEND_MM_PAGE_SIZE * ZEND_MM_FIRST_PAGE)) | |
/** | |
* ----------------------------------------------------------------------------- | |
* zend_alloc.h | |
* ----------------------------------------------------------------------------- | |
*/ | |
// 243: | |
typedef struct _zend_mm_heap zend_mm_heap; | |
// 266: | |
ZEND_API zend_mm_heap *zend_mm_get_heap(void); | |
/** | |
* ----------------------------------------------------------------------------- | |
* zend_alloc.c | |
* ----------------------------------------------------------------------------- | |
*/ | |
// 150: | |
typedef uint32_t zend_mm_page_info; /* 4-byte integer */ | |
typedef zend_ulong zend_mm_bitset; /* 4-byte or 8-byte integer */ | |
// 160: | |
#define ZEND_MM_BITSET_LEN (sizeof(zend_mm_bitset) * 8) /* 32 or 64 */ | |
#define ZEND_MM_PAGE_MAP_LEN (ZEND_MM_PAGES / ZEND_MM_BITSET_LEN) /* 16 or 8 */ | |
typedef zend_mm_bitset zend_mm_page_map[ZEND_MM_PAGE_MAP_LEN]; /* 64B */ | |
// 192: | |
#define ZEND_MM_BINS 30 | |
// 197: | |
typedef struct _zend_mm_chunk zend_mm_chunk; | |
// 234: | |
struct _zend_mm_heap { | |
#if ZEND_MM_CUSTOM | |
int use_custom_heap; | |
#endif | |
#if ZEND_MM_STORAGE | |
zend_mm_storage *storage; | |
#endif | |
#if ZEND_MM_STAT | |
size_t size; /* current memory usage */ | |
size_t peak; /* peak memory usage */ | |
#endif | |
zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */ | |
#if ZEND_MM_STAT || ZEND_MM_LIMIT | |
size_t real_size; /* current size of allocated pages */ | |
#endif | |
#if ZEND_MM_STAT | |
size_t real_peak; /* peak size of allocated pages */ | |
#endif | |
#if ZEND_MM_LIMIT | |
size_t limit; /* memory limit */ | |
int overflow; /* memory overflow flag */ | |
#endif | |
zend_mm_huge_list *huge_list; /* list of huge allocated blocks */ | |
zend_mm_chunk *main_chunk; | |
zend_mm_chunk *cached_chunks; /* list of unused chunks */ | |
int chunks_count; /* number of allocated chunks */ | |
int peak_chunks_count; /* peak number of allocated chunks for current request */ | |
int cached_chunks_count; /* number of cached chunks */ | |
double avg_chunks_count; /* average number of chunks allocated per request */ | |
int last_chunks_delete_boundary; /* number of chunks after last deletion */ | |
int last_chunks_delete_count; /* number of deletion over the last boundary */ | |
#if ZEND_MM_CUSTOM | |
union { | |
struct { | |
// void *(*_malloc)(size_t); | |
// void (*_free)(void*); | |
// void *(*_realloc)(void*, size_t); | |
void* _malloc; | |
void *_free; | |
void *_realloc; | |
} std; | |
struct { | |
// void *(*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC); | |
// void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC); | |
// void *(*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC); | |
void* _malloc; | |
void *_free; | |
void *_realloc; | |
} debug; | |
} custom_heap; | |
// HashTable *tracked_allocs; | |
void *tracked_allocs; | |
#endif | |
}; | |
// 284: | |
struct _zend_mm_chunk { | |
zend_mm_heap *heap; | |
zend_mm_chunk *next; | |
zend_mm_chunk *prev; | |
uint32_t free_pages; /* number of free pages */ | |
uint32_t free_tail; /* number of free pages at the end of chunk */ | |
uint32_t num; | |
char reserve[64 - (sizeof(void*) * 3 + sizeof(uint32_t) * 3)]; | |
zend_mm_heap heap_slot; /* used only in main chunk */ | |
zend_mm_page_map free_map; /* 512 bits or 64 bytes */ | |
zend_mm_page_info map[ZEND_MM_PAGES]; /* 2 KB = 512 * 4 */ | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"require": { | |
"php": "^8.2", | |
"ext-ffi": "*", | |
"ffi/proxy": "^1.0", | |
"ffi/preprocessor": "^0.2" | |
}, | |
"autoload": { | |
"psr-4": { | |
"Serafim\\MemProf\\": "src" | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Serafim\MemProf\Zend; | |
use FFI\Contracts\Preprocessor\Exception\DirectiveDefinitionExceptionInterface; | |
use FFI\Contracts\Preprocessor\Exception\PreprocessorExceptionInterface; | |
use FFI\Contracts\Preprocessor\PreprocessorInterface; | |
use FFI\Preprocessor\Preprocessor; | |
final readonly class Headers implements \Stringable | |
{ | |
/** | |
* @var non-empty-string | |
*/ | |
private const HEADERS_PATHNAME = __DIR__ . '/../../resources/alloc.h'; | |
/** | |
* @var non-empty-string | |
*/ | |
private string $headers; | |
/** | |
* @throws PreprocessorExceptionInterface | |
*/ | |
public function __construct(PreprocessorInterface $pre = null) | |
{ | |
$pre = $this->prepare($pre ?? new Preprocessor()); | |
$this->headers = (string)$pre->process( | |
new \SplFileInfo(self::HEADERS_PATHNAME), | |
); | |
} | |
/** | |
* @param PreprocessorInterface $pre | |
* | |
* @return PreprocessorInterface | |
* @throws DirectiveDefinitionExceptionInterface | |
*/ | |
private function prepare(PreprocessorInterface $pre): PreprocessorInterface | |
{ | |
$pre = clone $pre; | |
if (\PHP_INT_SIZE === 8) { | |
$pre->define('ZEND_ENABLE_ZVAL_LONG64', '1'); | |
} | |
$pre->define('ZEND_MM_STAT', '1'); | |
$pre->define('ZEND_MM_STORAGE', '1'); | |
$pre->define('ZEND_MM_CUSTOM', '1'); | |
$pre->define('ZEND_MM_LIMIT', '1'); | |
if (\PHP_OS_FAMILY === 'Windows') { | |
$pre->define('ZEND_API', '__declspec(dllimport)'); | |
$pre->define('ZEND_FASTCALL', '__vectorcall'); | |
} else { | |
$pre->define('ZEND_API', ''); | |
} | |
$pre->define('zend_mm_storage', 'void'); | |
$pre->define('zend_mm_free_slot', 'void'); | |
$pre->define('zend_mm_huge_list', 'void'); | |
return $pre; | |
} | |
public function __toString(): string | |
{ | |
return $this->headers; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Serafim\MemProf\Zend; | |
use FFI\CData; | |
use FFI\Proxy\Proxy; | |
/** | |
* @method CData|ZendMMHeap zend_mm_get_heap() | |
*/ | |
final class Library extends Proxy | |
{ | |
public function __construct( | |
string|\Stringable $headers = new Headers(), | |
string $library = null, | |
) { | |
$ffi = \FFI::cdef((string)$headers, $library ?? $this->getLibrary()); | |
parent::__construct($ffi); | |
} | |
/** | |
* @return non-empty-string | |
*/ | |
private function getLibrary(): string | |
{ | |
if (\PHP_OS_FAMILY === 'Windows') { | |
return 'php' . \PHP_MAJOR_VERSION . '.dll'; | |
} | |
return ''; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Serafim\MemProf; | |
use Serafim\MemProf\Page\Type; | |
use Serafim\MemProf\Zend\Library; | |
/** | |
* @template-implements \IteratorAggregate<int, Page> | |
*/ | |
final class Map implements \IteratorAggregate | |
{ | |
// zend_alloc.c:165 | |
private const ZEND_MM_IS_FRUN = 0x00000000; | |
private const ZEND_MM_IS_LRUN = 0x40000000; | |
private const ZEND_MM_IS_SRUN = 0x80000000; | |
// zend_alloc.c:169 | |
private const ZEND_MM_LRUN_PAGES_MASK = 0x000003ff; | |
private const ZEND_MM_LRUN_PAGES_OFFSET = 0; | |
// zend_alloc.c:172 | |
private const ZEND_MM_SRUN_BIN_NUM_MASK = 0x0000001f; | |
private const ZEND_MM_SRUN_BIN_NUM_OFFSET = 0; | |
// zend_alloc.c:175 | |
private const ZEND_MM_SRUN_FREE_COUNTER_MASK = 0x01ff0000; | |
private const ZEND_MM_SRUN_FREE_COUNTER_OFFSET = 16; | |
// zend_alloc.c:178 | |
private const ZEND_MM_NRUN_OFFSET_MASK = 0x01ff0000; | |
private const ZEND_MM_NRUN_OFFSET_OFFSET = 16; | |
// zend_alloc_sizes.h:32 | |
private const ZEND_MM_BINS_INFO_COUNT = [ | |
1, 1, 1, 1, 1, 1, | |
1, 1, 1, 1, 1, 1, | |
1, 1, 1, 1, 5, 3, | |
1, 1, 5, 3, 2, 2, | |
5, 3, 7, 4, 5, 3, | |
]; | |
/** | |
* @var array<int<0, max>, Page> | |
*/ | |
private array $map; | |
/** | |
* @var object|\ArrayAccess<int<0, max>, int> | |
*/ | |
private object $ref; | |
public function __construct( | |
private Library $zend, | |
) { | |
$heap = $this->zend->zend_mm_get_heap(); | |
$this->ref = $heap->main_chunk->map; | |
for ($i = 0, $size = \count($this->ref); $i < $size; ++$i) { | |
$this->map[] = new Page($i === 0 ? Type::INIT : Type::FREE); | |
} | |
$this->update(); | |
} | |
public function draw(): string | |
{ | |
$result = ''; | |
for ($i = 0; $i < 8; ++$i) { | |
for ($j = 0; $j < 64; ++$j) { | |
$page = $this->map[$i * 64 + $j]; | |
if ($page->highlight) { | |
$result .= $page->type === Type::FREE ? "\033[33m" : "\033[31m"; | |
} | |
$result .= match ($page->type) { | |
Type::FREE => '□', | |
Type::INIT => '◙', | |
Type::SMALL => '▣', | |
Type::LARGE => '▩', | |
}; | |
if ($page->highlight) { | |
$result .= "\033[0m"; | |
} | |
} | |
$result .= "\n"; | |
} | |
return $result; | |
} | |
public function update(): void | |
{ | |
$index = 0; | |
$length = \count($this->map); | |
while ($index < $length) { | |
$page = $this->ref[$index]; | |
$count = 1; | |
if ($page === self::ZEND_MM_IS_FRUN) { | |
$this->updatePage($index, Type::FREE, $count); | |
$index += $count; | |
continue; | |
} | |
$isSmall = ($page & self::ZEND_MM_IS_SRUN) === self::ZEND_MM_IS_SRUN; | |
$isLarge = ($page & self::ZEND_MM_IS_LRUN) === self::ZEND_MM_IS_LRUN; | |
if ($isSmall && $isLarge) { | |
$count = self::ZEND_MM_BINS_INFO_COUNT[$page & self::ZEND_MM_SRUN_BIN_NUM_MASK]; | |
$this->updatePage($index, Type::SMALL, $count); | |
} elseif ($isSmall) { | |
$this->updatePage($index, Type::SMALL, $count); | |
} elseif ($isLarge) { | |
$count = $page & self::ZEND_MM_SRUN_BIN_NUM_MASK; | |
$this->updatePage($index, Type::LARGE, $count); | |
} | |
$index += ($count ?: 1); | |
} | |
} | |
private function updatePage(int $id, Type $type, int $count): void | |
{ | |
for ($length = $id + $count; $id < $length; ++$id) { | |
$this->map[$id]->highlight = $this->map[$id]->type !== $type; | |
$this->map[$id]->type = $type; | |
} | |
} | |
public function getIterator(): \Traversable | |
{ | |
return new \ArrayIterator($this->map); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Serafim\MemProf; | |
use Serafim\MemProf\Page\Type; | |
final class Page | |
{ | |
public function __construct( | |
public Type $type, | |
public bool $highlight = false, | |
) { | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Serafim\MemProf\Page; | |
enum Type | |
{ | |
case FREE; | |
case INIT; | |
case SMALL; | |
case LARGE; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Result: