<?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>