Created
April 16, 2021 15:36
-
-
Save ciencia/8f6c1837a80fbe5d74a13fb616dc0ded to your computer and use it in GitHub Desktop.
opcache status - Script to get a nice visualization of opcache status (memory available, consumed, dirty...) I don't remember from where I grabbed it, but I haven't coded it
This file contains 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 | |
define('THOUSAND_SEPARATOR',true); | |
if (!extension_loaded('Zend OPcache')) { | |
echo '<div style="background-color: #F2DEDE; color: #B94A48; padding: 1em;">You do not have the Zend OPcache extension loaded, sample data is being shown instead.</div>'; | |
require 'data-sample.php'; | |
} | |
class OpCacheDataModel | |
{ | |
private $_configuration; | |
private $_status; | |
private $_d3Scripts = array(); | |
public function __construct() | |
{ | |
$this->_configuration = opcache_get_configuration(); | |
$this->_status = opcache_get_status(); | |
} | |
public function getPageTitle() | |
{ | |
return 'PHP ' . phpversion() . " with OpCache {$this->_configuration['version']['version']}"; | |
} | |
public function getStatusDataRows() | |
{ | |
$rows = array(); | |
foreach ($this->_status as $key => $value) { | |
if ($key === 'scripts') { | |
continue; | |
} | |
if (is_array($value)) { | |
foreach ($value as $k => $v) { | |
if ($v === false) { | |
$value = 'false'; | |
} | |
if ($v === true) { | |
$value = 'true'; | |
} | |
if ($k === 'used_memory' || $k === 'free_memory' || $k === 'wasted_memory') { | |
$v = $this->_size_for_humans( | |
$v | |
); | |
} | |
if ($k === 'current_wasted_percentage' || $k === 'opcache_hit_rate') { | |
$v = number_format( | |
$v, | |
2 | |
) . '%'; | |
} | |
if ($k === 'blacklist_miss_ratio') { | |
$v = number_format($v, 2) . '%'; | |
} | |
if ($k === 'start_time' || $k === 'last_restart_time') { | |
$v = ($v ? date(DATE_RFC822, $v) : 'never'); | |
} | |
if (THOUSAND_SEPARATOR === true && is_int($v)) { | |
$v = number_format($v); | |
} | |
$rows[] = "<tr><th>$k</th><td>$v</td></tr>\n"; | |
} | |
continue; | |
} | |
if ($value === false) { | |
$value = 'false'; | |
} | |
if ($value === true) { | |
$value = 'true'; | |
} | |
$rows[] = "<tr><th>$key</th><td>$value</td></tr>\n"; | |
} | |
return implode("\n", $rows); | |
} | |
public function getConfigDataRows() | |
{ | |
$rows = array(); | |
foreach ($this->_configuration['directives'] as $key => $value) { | |
if ($value === false) { | |
$value = 'false'; | |
} | |
if ($value === true) { | |
$value = 'true'; | |
} | |
if ($key == 'opcache.memory_consumption') { | |
$value = $this->_size_for_humans($value); | |
} | |
$rows[] = "<tr><th>$key</th><td>$value</td></tr>\n"; | |
} | |
return implode("\n", $rows); | |
} | |
public function getScriptStatusRows() | |
{ | |
foreach ($this->_status['scripts'] as $key => $data) { | |
$dirs[dirname($key)][basename($key)] = $data; | |
$this->_arrayPset($this->_d3Scripts, $key, array( | |
'name' => basename($key), | |
'size' => $data['memory_consumption'], | |
)); | |
} | |
asort($dirs); | |
$basename = ''; | |
while (true) { | |
if (count($this->_d3Scripts) !=1) break; | |
$basename .= DIRECTORY_SEPARATOR . key($this->_d3Scripts); | |
$this->_d3Scripts = reset($this->_d3Scripts); | |
} | |
$this->_d3Scripts = $this->_processPartition($this->_d3Scripts, $basename); | |
$id = 1; | |
$rows = array(); | |
foreach ($dirs as $dir => $files) { | |
$count = count($files); | |
$file_plural = $count > 1 ? 's' : null; | |
$m = 0; | |
foreach ($files as $file => $data) { | |
$m += $data["memory_consumption"]; | |
} | |
$m = $this->_size_for_humans($m); | |
if ($count > 1) { | |
$rows[] = '<tr>'; | |
$rows[] = "<th class=\"clickable\" id=\"head-{$id}\" colspan=\"3\" onclick=\"toggleVisible('#head-{$id}', '#row-{$id}')\">{$dir} ({$count} file{$file_plural}, {$m})</th>"; | |
$rows[] = '</tr>'; | |
} | |
foreach ($files as $file => $data) { | |
$rows[] = "<tr id=\"row-{$id}\">"; | |
$rows[] = "<td>" . $this->_format_value($data["hits"]) . "</td>"; | |
$rows[] = "<td>" . $this->_size_for_humans($data["memory_consumption"]) . "</td>"; | |
$rows[] = $count > 1 ? "<td>{$file}</td>" : "<td>{$dir}/{$file}</td>"; | |
$rows[] = '</tr>'; | |
} | |
++$id; | |
} | |
return implode("\n", $rows); | |
} | |
public function getScriptStatusCount() | |
{ | |
return count($this->_status["scripts"]); | |
} | |
public function getGraphDataSetJson() | |
{ | |
$dataset = array(); | |
$dataset['memory'] = array( | |
$this->_status['memory_usage']['used_memory'], | |
$this->_status['memory_usage']['free_memory'], | |
$this->_status['memory_usage']['wasted_memory'], | |
); | |
$dataset['keys'] = array( | |
$this->_status['opcache_statistics']['num_cached_keys'], | |
$this->_status['opcache_statistics']['max_cached_keys'] - $this->_status['opcache_statistics']['num_cached_keys'], | |
0 | |
); | |
$dataset['hits'] = array( | |
$this->_status['opcache_statistics']['misses'], | |
$this->_status['opcache_statistics']['hits'], | |
0, | |
); | |
$dataset['restarts'] = array( | |
$this->_status['opcache_statistics']['oom_restarts'], | |
$this->_status['opcache_statistics']['manual_restarts'], | |
$this->_status['opcache_statistics']['hash_restarts'], | |
); | |
if (THOUSAND_SEPARATOR === true) { | |
$dataset['TSEP'] = 1; | |
} else { | |
$dataset['TSEP'] = 0; | |
} | |
return json_encode($dataset); | |
} | |
public function getHumanUsedMemory() | |
{ | |
return $this->_size_for_humans($this->getUsedMemory()); | |
} | |
public function getHumanFreeMemory() | |
{ | |
return $this->_size_for_humans($this->getFreeMemory()); | |
} | |
public function getHumanWastedMemory() | |
{ | |
return $this->_size_for_humans($this->getWastedMemory()); | |
} | |
public function getUsedMemory() | |
{ | |
return $this->_status['memory_usage']['used_memory']; | |
} | |
public function getFreeMemory() | |
{ | |
return $this->_status['memory_usage']['free_memory']; | |
} | |
public function getWastedMemory() | |
{ | |
return $this->_status['memory_usage']['wasted_memory']; | |
} | |
public function getWastedMemoryPercentage() | |
{ | |
return number_format($this->_status['memory_usage']['current_wasted_percentage'], 2); | |
} | |
public function getD3Scripts() | |
{ | |
return $this->_d3Scripts; | |
} | |
private function _processPartition($value, $name = null) | |
{ | |
if (array_key_exists('size', $value)) { | |
return $value; | |
} | |
$array = array('name' => $name,'children' => array()); | |
foreach ($value as $k => $v) { | |
$array['children'][] = $this->_processPartition($v, $k); | |
} | |
return $array; | |
} | |
private function _format_value($value) | |
{ | |
if (THOUSAND_SEPARATOR === true) { | |
return number_format($value); | |
} else { | |
return $value; | |
} | |
} | |
private function _size_for_humans($bytes) | |
{ | |
if ($bytes > 1048576) { | |
return sprintf('%.2f MB', $bytes / 1048576); | |
} else { | |
if ($bytes > 1024) { | |
return sprintf('%.2f kB', $bytes / 1024); | |
} else { | |
return sprintf('%d bytes', $bytes); | |
} | |
} | |
} | |
// Borrowed from Laravel | |
private function _arrayPset(&$array, $key, $value) | |
{ | |
if (is_null($key)) return $array = $value; | |
$keys = explode(DIRECTORY_SEPARATOR, ltrim($key, DIRECTORY_SEPARATOR)); | |
while (count($keys) > 1) { | |
$key = array_shift($keys); | |
if ( ! isset($array[$key]) || ! is_array($array[$key])) { | |
$array[$key] = array(); | |
} | |
$array =& $array[$key]; | |
} | |
$array[array_shift($keys)] = $value; | |
return $array; | |
} | |
} | |
$dataModel = new OpCacheDataModel(); | |
?> | |
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<html> | |
<head> | |
<style> | |
body { | |
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; | |
margin: 0; | |
padding: 0; | |
} | |
#container { | |
width: 1024px; | |
margin: auto; | |
position: relative; | |
} | |
h1 { | |
padding: 10px 0; | |
} | |
table { | |
border-collapse: collapse; | |
} | |
tbody tr:nth-child(even) { | |
background-color: #eee; | |
} | |
p.capitalize { | |
text-transform: capitalize; | |
} | |
.tabs { | |
position: relative; | |
float: left; | |
width: 60%; | |
} | |
.tab { | |
float: left; | |
} | |
.tab label { | |
background: #eee; | |
padding: 10px 12px; | |
border: 1px solid #ccc; | |
margin-left: -1px; | |
position: relative; | |
left: 1px; | |
} | |
.tab [type=radio] { | |
display: none; | |
} | |
.tab th, .tab td { | |
padding: 8px 12px; | |
} | |
.content { | |
position: absolute; | |
top: 28px; | |
left: 0; | |
background: white; | |
border: 1px solid #ccc; | |
height: 450px; | |
width: 100%; | |
overflow: auto; | |
} | |
.content table { | |
width: 100%; | |
} | |
.content th, .tab:nth-child(3) td { | |
text-align: left; | |
} | |
.content td { | |
text-align: right; | |
} | |
.clickable { | |
cursor: pointer; | |
} | |
[type=radio]:checked ~ label { | |
background: white; | |
border-bottom: 1px solid white; | |
z-index: 2; | |
} | |
[type=radio]:checked ~ label ~ .content { | |
z-index: 1; | |
} | |
#graph { | |
float: right; | |
width: 40%; | |
position: relative; | |
} | |
#graph > form { | |
position: absolute; | |
right: 60px; | |
top: -20px; | |
} | |
#graph > svg { | |
position: absolute; | |
top: 0; | |
right: 0; | |
} | |
#stats { | |
position: absolute; | |
right: 125px; | |
top: 145px; | |
} | |
#stats th, #stats td { | |
padding: 6px 10px; | |
font-size: 0.8em; | |
} | |
#partition { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
z-index: 10; | |
top: 0; | |
left: 0; | |
background: #ddd; | |
display: none; | |
} | |
#close-partition { | |
display: none; | |
position: absolute; | |
z-index: 20; | |
right: 15px; | |
top: 15px; | |
background: #f9373d; | |
color: #fff; | |
padding: 12px 15px; | |
} | |
#close-partition:hover { | |
background: #D32F33; | |
cursor: pointer; | |
} | |
#partition rect { | |
stroke: #fff; | |
fill: #aaa; | |
fill-opacity: 1; | |
} | |
#partition rect.parent { | |
cursor: pointer; | |
fill: steelblue; | |
} | |
#partition text { | |
pointer-events: none; | |
} | |
label { | |
cursor: pointer; | |
} | |
</style> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.0.1/d3.v3.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> | |
<script> | |
var hidden = {}; | |
function toggleVisible(head, row) { | |
if (!hidden[row]) { | |
d3.selectAll(row).transition().style('display', 'none'); | |
hidden[row] = true; | |
d3.select(head).transition().style('color', '#ccc'); | |
} else { | |
d3.selectAll(row).transition().style('display'); | |
hidden[row] = false; | |
d3.select(head).transition().style('color', '#000'); | |
} | |
} | |
</script> | |
<title><?php echo $dataModel->getPageTitle(); ?></title> | |
</head> | |
<body> | |
<div id="container"> | |
<h1><?php echo $dataModel->getPageTitle(); ?></h1> | |
<div class="tabs"> | |
<div class="tab"> | |
<input type="radio" id="tab-status" name="tab-group-1" checked> | |
<label for="tab-status">Status</label> | |
<div class="content"> | |
<table> | |
<?php echo $dataModel->getStatusDataRows(); ?> | |
</table> | |
</div> | |
</div> | |
<div class="tab"> | |
<input type="radio" id="tab-config" name="tab-group-1"> | |
<label for="tab-config">Configuration</label> | |
<div class="content"> | |
<table> | |
<?php echo $dataModel->getConfigDataRows(); ?> | |
</table> | |
</div> | |
</div> | |
<div class="tab"> | |
<input type="radio" id="tab-scripts" name="tab-group-1"> | |
<label for="tab-scripts">Scripts (<?php echo $dataModel->getScriptStatusCount(); ?>)</label> | |
<div class="content"> | |
<table style="font-size:0.8em;"> | |
<tr> | |
<th width="10%">Hits</th> | |
<th width="20%">Memory</th> | |
<th width="70%">Path</th> | |
</tr> | |
<?php echo $dataModel->getScriptStatusRows(); ?> | |
</table> | |
</div> | |
</div> | |
<div class="tab"> | |
<input type="radio" id="tab-visualise" name="tab-group-1"> | |
<label for="tab-visualise">Visualise Partition</label> | |
<div class="content"></div> | |
</div> | |
</div> | |
<div id="graph"> | |
<form> | |
<label><input type="radio" name="dataset" value="memory" checked> Memory</label> | |
<label><input type="radio" name="dataset" value="keys"> Keys</label> | |
<label><input type="radio" name="dataset" value="hits"> Hits</label> | |
<label><input type="radio" name="dataset" value="restarts"> Restarts</label> | |
</form> | |
<div id="stats"></div> | |
</div> | |
</div> | |
<div id="close-partition">✖ Close Visualisation</div> | |
<div id="partition"></div> | |
<script> | |
var dataset = <?php echo $dataModel->getGraphDataSetJson(); ?>; | |
var width = 400, | |
height = 400, | |
radius = Math.min(width, height) / 2, | |
colours = ['#B41F1F', '#1FB437', '#ff7f0e']; | |
d3.scale.customColours = function() { | |
return d3.scale.ordinal().range(colours); | |
}; | |
var colour = d3.scale.customColours(); | |
var pie = d3.layout.pie().sort(null); | |
var arc = d3.svg.arc().innerRadius(radius - 20).outerRadius(radius - 50); | |
var svg = d3.select("#graph").append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.append("g") | |
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); | |
var path = svg.selectAll("path") | |
.data(pie(dataset.memory)) | |
.enter().append("path") | |
.attr("fill", function(d, i) { return colour(i); }) | |
.attr("d", arc) | |
.each(function(d) { this._current = d; }); // store the initial values | |
d3.selectAll("input").on("change", change); | |
set_text("memory"); | |
function set_text(t) { | |
if (t === "memory") { | |
d3.select("#stats").html( | |
"<table><tr><th style='background:#B41F1F;'>Used</th><td><?php echo $dataModel->getHumanUsedMemory()?></td></tr>"+ | |
"<tr><th style='background:#1FB437;'>Free</th><td><?php echo $dataModel->getHumanFreeMemory()?></td></tr>"+ | |
"<tr><th style='background:#ff7f0e;' rowspan=\"2\">Wasted</th><td><?php echo $dataModel->getHumanWastedMemory()?></td></tr>"+ | |
"<tr><td><?php echo $dataModel->getWastedMemoryPercentage()?>%</td></tr></table>" | |
); | |
} else if (t === "keys") { | |
d3.select("#stats").html( | |
"<table><tr><th style='background:#B41F1F;'>Cached keys</th><td>"+format_value(dataset[t][0])+"</td></tr>"+ | |
"<tr><th style='background:#1FB437;'>Free Keys</th><td>"+format_value(dataset[t][1])+"</td></tr></table>" | |
); | |
} else if (t === "hits") { | |
d3.select("#stats").html( | |
"<table><tr><th style='background:#B41F1F;'>Misses</th><td>"+format_value(dataset[t][0])+"</td></tr>"+ | |
"<tr><th style='background:#1FB437;'>Cache Hits</th><td>"+format_value(dataset[t][1])+"</td></tr></table>" | |
); | |
} else if (t === "restarts") { | |
d3.select("#stats").html( | |
"<table><tr><th style='background:#B41F1F;'>Memory</th><td>"+dataset[t][0]+"</td></tr>"+ | |
"<tr><th style='background:#1FB437;'>Manual</th><td>"+dataset[t][1]+"</td></tr>"+ | |
"<tr><th style='background:#ff7f0e;'>Keys</th><td>"+dataset[t][2]+"</td></tr></table>" | |
); | |
} | |
} | |
function change() { | |
// Filter out any zero values to see if there is anything left | |
var remove_zero_values = dataset[this.value].filter(function(value) { | |
return value > 0; | |
}); | |
// Skip if the value is undefined for some reason | |
if (typeof dataset[this.value] !== 'undefined' && remove_zero_values.length > 0) { | |
$('#graph').find('> svg').show(); | |
path = path.data(pie(dataset[this.value])); // update the data | |
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs | |
// Hide the graph if we can't draw it correctly, not ideal but this works | |
} else { | |
$('#graph').find('> svg').hide(); | |
} | |
set_text(this.value); | |
} | |
function arcTween(a) { | |
var i = d3.interpolate(this._current, a); | |
this._current = i(0); | |
return function(t) { | |
return arc(i(t)); | |
}; | |
} | |
function size_for_humans(bytes) { | |
if (bytes > 1048576) { | |
return (bytes/1048576).toFixed(2) + ' MB'; | |
} else if (bytes > 1024) { | |
return (bytes/1024).toFixed(2) + ' KB'; | |
} else return bytes + ' bytes'; | |
} | |
function format_value(value) { | |
if (dataset["TSEP"] == 1) { | |
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |
} else { | |
return value; | |
} | |
} | |
var w = window.innerWidth, | |
h = window.innerHeight, | |
x = d3.scale.linear().range([0, w]), | |
y = d3.scale.linear().range([0, h]); | |
var vis = d3.select("#partition") | |
.style("width", w + "px") | |
.style("height", h + "px") | |
.append("svg:svg") | |
.attr("width", w) | |
.attr("height", h); | |
var partition = d3.layout.partition() | |
.value(function(d) { return d.size; }); | |
root = JSON.parse('<?php echo json_encode($dataModel->getD3Scripts()); ?>'); | |
var g = vis.selectAll("g") | |
.data(partition.nodes(root)) | |
.enter().append("svg:g") | |
.attr("transform", function(d) { return "translate(" + x(d.y) + "," + y(d.x) + ")"; }) | |
.on("click", click); | |
var kx = w / root.dx, | |
ky = h / 1; | |
g.append("svg:rect") | |
.attr("width", root.dy * kx) | |
.attr("height", function(d) { return d.dx * ky; }) | |
.attr("class", function(d) { return d.children ? "parent" : "child"; }); | |
g.append("svg:text") | |
.attr("transform", transform) | |
.attr("dy", ".35em") | |
.style("opacity", function(d) { return d.dx * ky > 12 ? 1 : 0; }) | |
.text(function(d) { return d.name; }) | |
d3.select(window) | |
.on("click", function() { click(root); }) | |
function click(d) { | |
if (!d.children) return; | |
kx = (d.y ? w - 40 : w) / (1 - d.y); | |
ky = h / d.dx; | |
x.domain([d.y, 1]).range([d.y ? 40 : 0, w]); | |
y.domain([d.x, d.x + d.dx]); | |
var t = g.transition() | |
.duration(d3.event.altKey ? 7500 : 750) | |
.attr("transform", function(d) { return "translate(" + x(d.y) + "," + y(d.x) + ")"; }); | |
t.select("rect") | |
.attr("width", d.dy * kx) | |
.attr("height", function(d) { return d.dx * ky; }); | |
t.select("text") | |
.attr("transform", transform) | |
.style("opacity", function(d) { return d.dx * ky > 12 ? 1 : 0; }); | |
d3.event.stopPropagation(); | |
} | |
function transform(d) { | |
return "translate(8," + d.dx * ky / 2 + ")"; | |
} | |
$(document).ready(function() { | |
function handleVisualisationToggle(close) { | |
$('#partition, #close-partition').fadeToggle(); | |
// Is the visualisation being closed? If so show the status tab again | |
if (close) { | |
$('#tab-visualise').removeAttr('checked'); | |
$('#tab-status').trigger('click'); | |
} | |
} | |
$('label[for="tab-visualise"], #close-partition').on('click', function() { | |
handleVisualisationToggle(($(this).attr('id') === 'close-partition')); | |
}); | |
$(document).keyup(function(e) { | |
if (e.keyCode == 27) handleVisualisationToggle(true); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment