Skip to content

Commit

Permalink
Initial import of MemLog code.
Browse files Browse the repository at this point in the history
  • Loading branch information
Bob Somers committed Jan 27, 2012
0 parents commit ae1c7d5
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 0 deletions.
124 changes: 124 additions & 0 deletions MemLog.php
@@ -0,0 +1,124 @@
<?php

/**
* This class can be used to log memory usage of your PHP code over time,
* with the intention of helping you locate where you're running into memory
* usage problems. It is not a full-featured profiling tool like Xdebug or
* Webgrind.
*
* MemLog writes out data to a log file, which can be analyzed with another
* tool (or by hand if you wish, it's just pseudo-JSON). Each line is a JSON
* array with the following fields:
*
* [time since logging began, in seconds (float),
* current PHP memory usage, in bytes (integer),
* file name of the currently executing code, if any (string),
* class name of the currently executing code, if any (string),
* function name of the currently executing code, if any (string)]
*
* Basic usage is as follows:
*
* $memlog = new MemLog();
* $memlog->start('My Terrible Code');
*
* ...your bad code eats gobs of memory...
*
* $memlog->stop();
*
* Log files are saved with a base path of MemLog::$basePath, Unless you have
* a compelling reason, /tmp is a good choice for $basePath.
*
* @author Bob Somers
*/
class MemLog {
public static $basePath = '/tmp';
private static $maxBufferSize = 100;

private $isLogging = false;
private $fp = null;
private $buffer = array();
private $startTime = 0.0;

public function __construct() {
// Tweak this if necessary.
declare(ticks=1);
}

public function __destruct() {
if ($this->isLogging) {
$this->stop();
}
}

public function start($name = 'MemLog') {
if ($this->isLogging) return;

$name .= ' - ' . date('r');

list($usec, $time) = explode(' ', microtime());
list($junk, $usec) = explode('.', $usec);
$fileName = 'php_mem_usage.' . date('Y-m-d-H-i-s', $time) . "-$usec.log";

$this->fp = fopen(self::$basePath . '/' . $fileName, 'w');
fwrite($this->fp, $name);
$this->buffer = array();

$this->isLogging = true;

$this->startTime = microtime(true);
register_tick_function(array(&$this, 'tick'));
}

public function stop() {
if (!$this->isLogging) return;

unregister_tick_function(array(&$this, 'tick'));

$this->flush();
fclose($this->fp);

$this->isLogging = false;
}

public function tick() {
$timestamp = microtime(true) - $this->startTime;
$memory = memory_get_usage(true);

$file = '';
$class = '';
$function = '';
$backtrace = debug_backtrace();
if (isset($backtrace[1])) {
if (isset($backtrace[1]['file'])) {
$file = basename($backtrace[1]['file']);
}
if (isset($backtrace[1]['class'])) {
$class = $backtrace[1]['class'];
}
if (isset($backtrace[1]['function'])) {
$function = $backtrace[1]['function'];
}
}

$this->buffer[] = array(
$timestamp,
$memory,
$file,
$class,
$function
);

if (count($this->buffer) > self::$maxBufferSize) {
$this->flush();
}
}

private function flush() {
for ($i = 0; $i < count($this->buffer); $i++) {
fwrite($this->fp, "\n" . json_encode($this->buffer[$i]));
}
$this->buffer = array();
}
}

?>
1 change: 1 addition & 0 deletions viewer/dygraph-combined.js

Large diffs are not rendered by default.

137 changes: 137 additions & 0 deletions viewer/index.php
@@ -0,0 +1,137 @@
<?php

/*
* DO NOT USE THIS ON A PRODUCTION MACHINE.
*
* There is an easy way to get this script to basically dump out any file on
* the machine that the web server user has read access to, which is absolutely
* atrociously bad for security.
*
* This is a developer tool. Use it on a machine that only your developers have
* access to. Failure to heed this advice means you are willing to accept
* whatever punishment the intertubes inflict upon you.
*
* YOU HAVE BEEN WARNED.
*
* @author Bob Somers
*/

if (isset($_POST['logfile'])) {
$logfile = base64_decode($_POST['logfile']);
} else if (isset($_GET['logfile'])) {
header('Content-type: application/json');

// HERE BE DRAGONS. This is insanely bad. Never ever do this.
$lines = file(base64_decode($_GET['logfile']), FILE_IGNORE_NEW_LINES);

$name = array_shift($lines);

echo "{\n";
echo "\t\"name\": " . json_encode($name) . ",\n";
echo "\t\"data\": [\n";
$first = true;
foreach ($lines as $line) {
if (!$first) {
echo ",\n";
} else {
$first = false;
}
echo "\t\t" . $line;
}
echo "\n\t]\n";
echo "}\n";

exit();
}

?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MemLog Viewer</title>

<link href="http://fonts.googleapis.com/css?family=Ubuntu:700"
rel="stylesheet" type="text/css">
<link href="http://fonts.googleapis.com/css?family=Anonymous+Pro:400,700"
rel="stylesheet" type="text/css">

<script src="https://ajax.googleapis.com/ajax/libs/mootools/1.4.1/mootools-yui-compressed.js"
type="text/javascript"></script>
<script src="dygraph-combined.js"
type="text/javascript"></script>
<script src="memlog.js"
type="text/javascript"></script>

<?php if (isset($logfile)): ?>
<script type="text/javascript">
window.addEvent('domready', function() {
new Request.JSON({
url: '?logfile=<?php echo base64_encode($logfile); ?>',
onSuccess: function(resp) {
$('name').set('text', resp.name);
memlog(resp.data);
}
}).get();
});
</script>
<?php endif; ?>
<style type="text/css">
#codeloc {
font: 24px 'Anonymous Pro';
width 960px;
text-align: right;
line-height: 30px;
width: 960px;
height: 30px;
}

#choose {
position: absolute;
top: 28px;
left: 480px;
}

h1 {
font: bold 48px 'Ubuntu';
margin: 0;
padding-left: 20px;
height: 64px;
line-height: 64px;
border-bottom: 1px solid #aaa;
}

h2 {
font: bold 24px 'Ubuntu';
margin: 0;
padding: 10px 0 0 10px;
}
</style>
</head>
<body>
<h1>MemLog Viewer</h1>
<form id="choose" method="post">
<select name="logfile">
<?php
$files = glob('/tmp/php_mem_usage*.log');
rsort($files);
foreach ($files as $file) {
echo '<option value="' . base64_encode($file) . '"';
if ($logfile == $file) {
echo ' selected';
}
echo ">$file</option>\n";
}
?>
</select>
<input type="submit" value="View">
</form>
<?php if (isset($logfile)): ?>

<h2 id="name">Loading...</h2>
<div id="codeloc"></div>
<div id="graph"></div>

<?php endif; ?>
</body>
</html>
74 changes: 74 additions & 0 deletions viewer/memlog.js
@@ -0,0 +1,74 @@
function memlog(data) {
var series = [];
var details = {};
for (var i = 0; i < data.length; i++) {
var sample = data[i];

var timestamp = sample[0] * 1000; // convert to ms
var memory = sample[1];
var file = sample[2];
var klass = sample[3];
var func = sample[4];

series.push([timestamp, memory]);
details[String(timestamp)] = {
file: file,
klass: klass,
func: func
};
}

// Helper functions for formatting. Thanks to:
// http://dygraphs.com/tests/labelsKMB.html
function round(num, places) {
var shift = Math.pow(10, places);
return Math.round(num * shift)/shift;
}

function formatBytes(v) {
var suffixes = ['', 'k', 'M', 'G', 'T'];
if (v < 1000) return v;

var magnitude = Math.floor(String(Math.floor(v)).length / 3);
if (magnitude > suffixes.length - 1) {
magnitude = suffixes.length - 1;
}

return String(round(v / Math.pow(10, magnitude * 3), 2)) + suffixes[magnitude];
}

function formatMs(v) {
return String(round(v, 3) + " ms");
}

var codeloc = $('codeloc');
function showDetails(e, x) {
var info = details[String(x)];
var where = '';
if (info.file) {
where += '<strong>' + info.file + '</strong>:&nbsp;';
}
if (info.klass) {
where += info.klass + '::';
}
if (info.func) {
where += info.func + '()';
}
codeloc.set('html', where);
}

var graph = new Dygraph(document.getElementById('graph'), series, {
width: 960,
height: 300,
fillGraph: true,
stepPlot: true,
labels: ['Time', 'Mem'],
xlabel: 'Time (ms)',
ylabel: 'Memory Usage',
labelsKMG2: true,
xValueFormatter: formatMs,
yValueFormatter: formatBytes,
highlightCallback: showDetails,
colors: ["#336DA8"]
});
}

0 comments on commit ae1c7d5

Please sign in to comment.