Last active
November 29, 2024 11:42
-
-
Save shadyvb/7c867452d3d131afcf539235d838dcba to your computer and use it in GitHub Desktop.
Snippet to browse through traces and view their Flamegraphs.
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 | |
/** | |
* Validates XDebug configuration settings | |
* | |
* Checks if XDebug is loaded and properly configured for flame graph generation | |
* | |
* @return array List of configuration issues found | |
*/ | |
function validateXdebugConfig(): array | |
{ | |
$issues = []; | |
if (!extension_loaded('xdebug')) { | |
$issues[] = "XDebug extension is not loaded"; | |
return $issues; | |
} | |
$trace_format = ini_get('xdebug.trace_format'); | |
if ($trace_format != "3") { | |
$issues[] = "xdebug.trace_format should be 3 (computerized format), current value: " . ($trace_format ?: "not set"); | |
} | |
return $issues; | |
} | |
/** | |
* Gets available directory sources for trace files | |
* | |
* Returns an array of available directories where trace files can be found, | |
* including current directory, XDebug trace directory (if configured), and tmp directory | |
* | |
* @return array Array of directory sources with their labels and paths | |
*/ | |
function getDirectorySources(): array | |
{ | |
$current_dir = dirname(__FILE__); | |
$xdebug_dir = ini_get('xdebug.trace_output_dir'); | |
$tmp_dir = '/tmp'; | |
$sources = []; | |
// Only add xdebug option if the directory is configured | |
if ($xdebug_dir && is_dir($xdebug_dir)) { | |
$sources['xdebug'] = [ | |
'label' => 'XDebug Trace Directory', | |
'path' => $xdebug_dir | |
]; | |
} | |
$sources['tmp'] = [ | |
'label' => 'Temporary Directory', | |
'path' => $tmp_dir | |
]; | |
$sources['current'] = [ | |
'label' => 'Current Directory', | |
'path' => $current_dir | |
]; | |
return $sources; | |
} | |
/** | |
* Gets the selected directory path from available sources | |
* | |
* @param array $sources Available directory sources | |
* @param string $selected_source Selected source key | |
* | |
* @return string Path of the selected directory | |
*/ | |
function getSelectedDirectory(array $sources, string $selected_source): string | |
{ | |
// If the selected source doesn't exist, default to 'current' | |
if (!isset($sources[$selected_source])) { | |
$selected_source = 'current'; | |
} | |
return $sources[$selected_source]['path']; | |
} | |
/** | |
* Gets trace files from a specified directory | |
* | |
* @param string $directory Directory to search for trace files | |
* | |
* @return array Array of trace files with their paths, names, and sizes | |
*/ | |
function getTraceFiles(string $directory): array | |
{ | |
$files = []; | |
if (!$directory) { | |
return $files; | |
} | |
$glob_pattern = "$directory/*.xt"; | |
$found_files = glob($glob_pattern); | |
foreach ($found_files as $file) { | |
$files[] = [ | |
'path' => $file, | |
'name' => basename($file), | |
'size' => filesize($file) | |
]; | |
} | |
return $files; | |
} | |
/** | |
* Generates a flame graph from a trace file | |
* | |
* @param string $file Path to the trace file | |
* | |
* @return array Array containing either SVG content or error message | |
*/ | |
function generateFlameGraph(string $file): array | |
{ | |
if (!file_exists($file)) { | |
return ["error" => "Input file does not exist"]; | |
} | |
if (!is_readable($file)) { | |
return ["error" => "Cannot read input file"]; | |
} | |
ob_start(); | |
passthru(__DIR__.'/FlameGraph/flamegraph.pl ' . $file); | |
$output = ob_get_clean(); | |
return ["svg" => $output]; | |
} | |
// Initialize variables | |
$config_issues = validateXdebugConfig(); | |
$sources = getDirectorySources(); | |
$selected_source = $_REQUEST['directory_source'] ?? 'tmp'; | |
$current_dir = getSelectedDirectory($sources, $selected_source); | |
$trace_files = getTraceFiles($current_dir); | |
// Generate flame graph if file is selected | |
$flame_graph = null; | |
if (!empty($_REQUEST['file'])) { | |
$flame_graph = generateFlameGraph($_REQUEST['file']); | |
} | |
?> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>XDebug Flame Graph</title> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2" /> | |
<style> | |
:root { | |
--primary-color: #2563eb; | |
--background-color: #f8fafc; | |
--text-color: #1e293b; | |
--border-color: #e2e8f0; | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
line-height: 1.6; | |
color: var(--text-color); | |
background: var(--background-color); | |
padding: 2rem; | |
} | |
h1 { | |
font-size: 2rem; | |
font-weight: 600; | |
margin-bottom: 1rem; | |
color: var(--primary-color); | |
} | |
.container { | |
max-width: 1200px; | |
margin: 0 auto; | |
} | |
.header { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 2rem; | |
} | |
.controls-toggle { | |
background: var(--primary-color); | |
color: white; | |
border: none; | |
padding: 0.75rem 1.5rem; | |
border-radius: 6px; | |
font-size: 1rem; | |
cursor: pointer; | |
transition: background-color 0.2s; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
} | |
.controls-toggle:hover { | |
background: #1d4ed8; | |
} | |
.controls-toggle svg { | |
width: 20px; | |
height: 20px; | |
transition: transform 0.2s; | |
} | |
.controls-toggle.active svg { | |
transform: rotate(180deg); | |
} | |
.load { | |
background: white; | |
padding: 2rem; | |
border-radius: 8px; | |
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
margin-bottom: 2rem; | |
display: none; | |
} | |
.load.active { | |
display: block; | |
} | |
.form-group { | |
margin-bottom: 1.5rem; | |
} | |
label { | |
cursor: pointer; | |
display: block; | |
margin-bottom: 0.5rem; | |
font-weight: 500; | |
} | |
select { | |
width: 100%; | |
padding: 0.75rem; | |
border: 1px solid var(--border-color); | |
border-radius: 6px; | |
margin-bottom: 1rem; | |
font-size: 1rem; | |
background: white; | |
} | |
button { | |
background: var(--primary-color); | |
color: white; | |
border: none; | |
padding: 0.75rem 1.5rem; | |
border-radius: 6px; | |
font-size: 1rem; | |
cursor: pointer; | |
transition: background-color 0.2s; | |
} | |
button:hover { | |
background: #1d4ed8; | |
} | |
code { | |
background: #e2e8f0; | |
padding: 0.2rem 0.4rem; | |
border-radius: 4px; | |
font-size: 0.875rem; | |
} | |
p { | |
color: #64748b; | |
font-size: 0.875rem; | |
margin-top: 1rem; | |
} | |
svg { | |
width: 100%; | |
height: auto; | |
border-radius: 8px; | |
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
} | |
.info { | |
background: #f1f5f9; | |
padding: 1rem; | |
border-radius: 6px; | |
margin-top: 1rem; | |
} | |
.flamegraph { | |
margin-top: 2rem; | |
} | |
.warning { | |
background: #fef3c7; | |
border: 1px solid #f59e0b; | |
color: #92400e; | |
padding: 1rem; | |
border-radius: 6px; | |
margin-bottom: 1.5rem; | |
} | |
.warning h2 { | |
font-size: 1.1rem; | |
margin-bottom: 0.5rem; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
} | |
.warning svg { | |
width: 24px; | |
height: 24px; | |
} | |
.warning ul { | |
margin: 0.5rem 0 0 1.5rem; | |
} | |
.warning code { | |
background: rgba(245, 158, 11, 0.1); | |
} | |
.credits { | |
margin-top: 3rem; | |
padding-top: 1rem; | |
border-top: 1px solid var(--border-color); | |
color: #64748b; | |
font-size: 0.875rem; | |
text-align: center; | |
} | |
.credits a { | |
color: var(--primary-color); | |
text-decoration: none; | |
} | |
.credits a:hover { | |
text-decoration: underline; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="header"> | |
<h1>XDebug Flame Graph</h1> | |
<button type="button" class="controls-toggle" onclick="toggleControls()"> | |
<span>Controls</span> | |
</button> | |
</div> | |
<?php if (!empty($config_issues)): ?> | |
<div class="warning"> | |
<h2> | |
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> | |
</svg> | |
XDebug Configuration Issues | |
</h2> | |
<p>Please fix the following configuration issues in your php.ini:</p> | |
<ul> | |
<?php foreach ($config_issues as $issue): ?> | |
<li><?php echo htmlspecialchars($issue); ?></li> | |
<?php endforeach; ?> | |
</ul> | |
<p>Example php.ini configuration:</p> | |
<pre><code>xdebug.trace_format=3</code></pre> | |
</div> | |
<?php endif; ?> | |
<form method="POST" class="load<?php echo !empty($_REQUEST['file']) ? '' : ' active' ?>"> | |
<div class="form-group"> | |
<label for="directory_source">Directory Source</label> | |
<select name="directory_source" id="directory_source"> | |
<?php foreach ($sources as $key => $source): ?> | |
<?php $selected = ($key === $selected_source) ? 'selected="selected"' : ''; ?> | |
<option value="<?php echo htmlspecialchars($key); ?>" <?php echo $selected; ?>> | |
<?php echo htmlspecialchars($source['label']); ?> (<?php echo htmlspecialchars($source['path']); ?>) | |
</option> | |
<?php endforeach; ?> | |
</select> | |
</div> | |
<div class="form-group"> | |
<label for="file">Select Trace File</label> | |
<select name="file" id="file"> | |
<?php if (empty($trace_files)): ?> | |
<option value="">No trace files found</option> | |
<?php else: ?> | |
<?php foreach ($trace_files as $file): ?> | |
<?php $selected = ($file['path'] == $_REQUEST['file']) ? 'selected="selected"' : ''; ?> | |
<option value="<?php echo htmlspecialchars($file['path']); ?>" <?php echo $selected; ?>> | |
<?php echo htmlspecialchars($file['name']); ?> (<?php echo number_format($file['size']); ?> bytes) | |
</option> | |
<?php endforeach; ?> | |
<?php endif; ?> | |
</select> | |
</div> | |
<button type="submit">Generate Flame Graph</button> | |
</form> | |
<div class="flamegraph"> | |
<?php if ($flame_graph): ?> | |
<?php if (isset($flame_graph['error'])): ?> | |
<p class="info">Error: <?php echo htmlspecialchars($flame_graph['error']); ?></p> | |
<?php else: ?> | |
<?php echo $flame_graph['svg']; ?> | |
<?php endif; ?> | |
<?php endif; ?> | |
</div> | |
</div> | |
<div class="credits"> | |
<p> | |
Created by <a href="https://sharaf.me" target="_blank" rel="noopener">Shadi Sharaf</a> | |
• Inspired by <a href="https://daniellockyer.com/php-flame-graphs/" target="_blank" rel="noopener">Daniel Lockyer</a> | |
• Using <a href="https://github.com/brendangregg/FlameGraph" target="_blank" rel="noopener">Brendan Gregg's FlameGraph</a> | |
</p> | |
</div> | |
<script> | |
function toggleControls() { | |
const form = document.querySelector('.load'); | |
const toggle = document.querySelector('.controls-toggle'); | |
form.classList.toggle('active'); | |
toggle.classList.toggle('active'); | |
} | |
// Auto-hide controls if flamegraph is present | |
document.addEventListener('DOMContentLoaded', function() { | |
const flamegraph = document.querySelector('.flamegraph svg'); | |
if (flamegraph) { | |
document.querySelector('.load').classList.remove('active'); | |
document.querySelector('.controls-toggle').classList.remove('active'); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment