Skip to content

Label <main> frames by file in stackcollapse-phpspy.pl - #161

Open
dasl- wants to merge 1 commit into
adsr:masterfrom
dasl-:stackcollapse-main-file
Open

dasl- wants to merge 1 commit into
adsr:masterfrom
dasl-:stackcollapse-main-file

Conversation

@dasl-

@dasl- dasl- commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

stackcollapse-phpspy.pl drops the file:line field of every frame. PHP names the top-level scope of every file <main>, so every entry point and every included file collapses to the same label and merges into one tower at the base of the flamegraph.

Repro

printf '<?php\nrequire __DIR__ . "/level1.php";\n' > /tmp/nest.php
printf '<?php\nrequire __DIR__ . "/level2.php";\n' > /tmp/level1.php
printf '<?php\nsleep(3);\n'                        > /tmp/level2.php
phpspy -H 199 -b 65536 -o /tmp/nest.out -- php /tmp/nest.php

Three distinct files in the trace:

0 sleep <internal>:-1
1 <main> /tmp/level2.php:1
2 <main> /tmp/level1.php:1
3 <main> /tmp/nest.php:1

One label after collapsing:

<main>;<main>;<main>;sleep 525

Fix

<main> frames get their file appended, so the repro becomes:

<main>:nest.php;<main>:level1.php;<main>:level2.php;sleep 525

The label is the shortest trailing part of the path that is unique among the <main> files in the trace: usually just the basename, or e.g. admin/index.php and public/index.php when two files share one. Paths are collected while reading and shortened at output time. One consequence, noted in the header comment: the same file can be labelled differently in two captures if a same-named file appears in only one of them.

Notes:

  • A file included from inside a method inherits that class scope and phpspy reports it as Class::<main>. The class prefix is dropped so every file-scope frame reads <main>:file; the frame beneath it already names the method.
  • Code with no file (php -r, eval) is reported with <internal> as its file and stays a plain <main>.
  • ; and whitespace in a path become _, since both are delimiters in the folded format.
  • The path is appended before the existing decode / control-character scrub, so it gets the same XML sanitising as the function name.
  • Non-<main> frames are left alone — adding file:line there would split a function into a separate frame per callsite.

New test tests/test_stackcollapse_main_file.sh has a canned-input block that pins the labelling rules, and a live phpspy run with nested includes, a method-scope include, two files sharing a basename, and a directory name containing a space. Also updates the header comment's Example Output, which was already missing the sleep frame on two of three lines.

before and after comparisons

ex 1: nested includes

before

Screenshot 2026-09-13 at 7 23 56 PM

after

Screenshot 2026-09-13 at 7 26 07 PM

ex 2: including/requiring a file from a method

before

Screenshot 2026-09-13 at 7 34 49 PM

after

Screenshot 2026-09-13 at 7 35 04 PM

ex 3: a fake app w/ multiple entry points

before

Screenshot 2026-09-13 at 7 30 27 PM

after

Screenshot 2026-09-13 at 7 30 43 PM

🤖 Generated with Claude Code

@adsr

adsr commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Good find. I view this more as a bugfix. Can't think of a reason why we'd want to collapse different main frames into a single node. So IMO it doesn't need an opt out flag.

@dasl-
dasl- force-pushed the stackcollapse-main-file branch 5 times, most recently from ec91994 to e4a6589 Compare September 13, 2026 22:10
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dasl-
dasl- force-pushed the stackcollapse-main-file branch from e4a6589 to bd82eb4 Compare September 13, 2026 22:52
@dasl-
dasl- marked this pull request as ready for review September 13, 2026 23:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants