Skip to content

Instantly share code, notes, and snippets.

@shadyvb
Last active November 29, 2024 11:42
Show Gist options
  • Save shadyvb/7c867452d3d131afcf539235d838dcba to your computer and use it in GitHub Desktop.
Save shadyvb/7c867452d3d131afcf539235d838dcba to your computer and use it in GitHub Desktop.
Snippet to browse through traces and view their Flamegraphs.
<?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