Skip to content

Instantly share code, notes, and snippets.

@bskiefer
Created May 3, 2024 20:03
Show Gist options
  • Save bskiefer/beefcb3aaddcc6c1d65472adea73c3d8 to your computer and use it in GitHub Desktop.
Save bskiefer/beefcb3aaddcc6c1d65472adea73c3d8 to your computer and use it in GitHub Desktop.
Parse Xdebug traces and produce unique listing of usages. Includes require/include files, functions/methods, used files, and a csv containing every entry.

Running

  1. Enable xdebug tracing (see xdebug.ini)
  2. Build the image docker build . -t php/trace
  3. Run docker run -v {XDEBUG_OUTPUT_DIR}:/home/dev/traces -t php/trace
  4. View output at {XDEBUG_OUTPUT_DIR}/out

More Info

After traces are processed, they are removed. All unique values will remain in the generated files:

  • files.txt // source file
  • functions.txt // called functions/methods
  • includes.txt // files referenced by require/include
  • output.csv // function, includes, source file, line number
FROM php:8.2-cli
RUN groupadd -r dev -g 433 && \
useradd -u 431 -r -g dev -s /sbin/nologin -c "Docker image user" dev
WORKDIR /home/dev
COPY trace.php /home/dev/trace.php
COPY entry.sh /home/dev/entry.sh
RUN chmod +x /home/dev/entry.sh
USER dev
ENTRYPOINT ["/bin/bash", "/home/dev/entry.sh"]
#!/bin/bash
php trace.php "$@"
<?php
if (!file_exists(__DIR__ . '/traces/out/')) {
mkdir(__DIR__ . '/traces/out/');
}
$dir = __DIR__ . '/' . ($argv[1] ?? './traces');
if (is_dir($dir)) {
$files = scandir($dir);
foreach ($files as $file) {
if (str_contains($file, ".xt")) {
scan($dir . '/' . $file);
}
}
}
function scan(string $filename)
{
$loader = load($filename);
if (count($loader) === 0) {
return;
}
if (!file_exists(__DIR__ . '/traces/out/files.txt')) {
touch(__DIR__ . '/traces/out/files.txt');
}
if (!file_exists(__DIR__ . '/traces/out/includes.txt')) {
touch(__DIR__ . '/traces/out/includes.txt');
}
if (!file_exists(__DIR__ . '/traces/out/functions.txt')) {
touch(__DIR__ . '/traces/out/functions.txt');
}
$files_raw = explode("\n", file_get_contents(__DIR__ . '/traces/out/files.txt'));
$files = array_column($loader, 'file');
$files = array_unique($files + $files_raw);
$includes_raw = explode("\n", file_get_contents(__DIR__ . '/traces/out/includes.txt'));
$includes = array_filter(array_column($loader, 'include_file'));
$includes = array_unique($includes + $includes_raw);
$functions_raw = explode("\n", file_get_contents(__DIR__ . '/traces/out/functions.txt'));
$functions = array_filter(array_column($loader, 'fn'));
$functions = array_unique($functions + $functions_raw);
file_put_contents(__DIR__ . '/traces/out/files.txt', implode("\n", $files));
file_put_contents(__DIR__ . '/traces/out/includes.txt', implode("\n", $includes));
file_put_contents(__DIR__ . '/traces/out/functions.txt', implode("\n", $functions));
$origCsv = csv_to_array(__DIR__ . '/traces/out/output.csv');
$newCsvData = (is_array($origCsv) ? $origCsv : []) + (array) $loader;
$newCsvData = array_unique(array_values($newCsvData), SORT_REGULAR);
$fp = fopen(__DIR__ . '/traces/out/output.csv', 'w', FILE_APPEND);
fputcsv($fp, array_keys($loader[0]));
foreach ($newCsvData as $fields) {
fputcsv($fp, $fields);
}
fclose($fp);
unlink($filename);
}
function csv_to_array($filename = '', $delimiter = ',')
{
if (!file_exists($filename) || !is_readable($filename))
return false;
$header = null;
$data = array();
if (($handle = fopen($filename, 'r')) !== false) {
while (($row = fgetcsv($handle, 1000, $delimiter)) !== false) {
if (!$header)
$header = $row;
else
$data[] = array_combine($header, $row);
}
fclose($handle);
}
return $data;
}
function load(string $filename)
{
$file = new \SplFileObject($filename);
$frames = [];
while (!$file->eof()) {
$line = $file->fgetcsv("\t");
if (!$line) {
break;
}
if (count($line) >= 11) {
if ($line[5]) {
$b = get_defined_functions();
if (in_array($line[5], $b['internal'])) {
continue;
}
}
$frames[] = [
"fn" => $line[5],
"include_file" => $line[7],
"file" => $line[8],
"line" => $line[9],
];
}
}
return $frames;
}
; See https://xdebug.org/docs/trace
xdebug.mode=debug,trace
; For all requests
xdebug.auto_trace = 1
; Only when trigger is present
xdebug.trace_enable_trigger = 1
xdebug.default_enable=0
xdebug.remote_enable=0
; Used in the docker commands
xdebug.output_dir = {XDEBUG_OUTPUT_DIR}
xdebug.profiler_enable = 0
xdebug.use_compression = false
xdebug.trace_output_name = %R.%s.%c.%U
xdebug.trace_format = 1
xdebug.collect_assignments = false
xdebug.collect_params = true
xdebug.collect_return = false
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment