Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ All with no changes to your application and minimal overhead.
(default: 0; 0=unlimited)
-n, --max-depth=<max> Set max stack trace depth
(default: -1; -1=unlimited)
-N, --max-depth-outer=<max> Also keep the outermost `max` frames,
eliding the middle of deeper stacks.
Used alone, only the outermost are
kept. (default: -1; -1=off)
-r, --request-info=<opts> Set request info parts to capture
(q=query c=cookie u=uri p=path
capital=negation)
Expand Down
9 changes: 8 additions & 1 deletion phpspy.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ int opt_capture_req_uri = 0;
int opt_capture_req_path = 0;
int opt_capture_mem = 0;
int opt_max_stack_depth = -1;
int opt_max_stack_depth_outer = -1;
char opt_frame_delim = '\n';
char opt_trace_delim = '\n';
uint64_t opt_trace_limit = 0;
Expand Down Expand Up @@ -135,6 +136,10 @@ void usage(FILE *fp, int exit_code) {
fprintf(fp, " (default: %lu; 0=unlimited)\n", opt_time_limit_ms);
fprintf(fp, " -n, --max-depth=<max> Set max stack trace depth\n");
fprintf(fp, " (default: %d; -1=unlimited)\n", opt_max_stack_depth);
fprintf(fp, " -N, --max-depth-outer=<max> Also keep the outermost `max` frames,\n");
fprintf(fp, " eliding the middle of deeper stacks.\n");
fprintf(fp, " Used alone, only the outermost are\n");
fprintf(fp, " kept. (default: %d; -1=off)\n", opt_max_stack_depth_outer);
fprintf(fp, " -r, --request-info=<opts> Set request info parts to capture\n");
fprintf(fp, " (q=query c=cookie u=uri p=path\n");
fprintf(fp, " capital=negation)\n");
Expand Down Expand Up @@ -244,6 +249,7 @@ static void parse_opts(int argc, char **argv) {
{ "limit", required_argument, NULL, 'l' },
{ "time-limit-ms", required_argument, NULL, 'i' },
{ "max-depth", required_argument, NULL, 'n' },
{ "max-depth-outer", required_argument, NULL, 'N' },
{ "request-info", required_argument, NULL, 'r' },
{ "memory-usage", no_argument, NULL, 'm' },
{ "output", required_argument, NULL, 'o' },
Expand Down Expand Up @@ -280,7 +286,7 @@ static void parse_opts(int argc, char **argv) {
while (
optind < argc
&& argv[optind][0] == '-'
&& (c = getopt_long(argc, argv, "hp:P:T:te:s:H:V:l:i:n:r:mo:O:E:x:a:1b:f:F:d:cqj:J:#:@vSe:g:tw:", long_opts, NULL)) != -1
&& (c = getopt_long(argc, argv, "hp:P:T:te:s:H:V:l:i:n:N:r:mo:O:E:x:a:1b:f:F:d:cqj:J:#:@vSe:g:tw:", long_opts, NULL)) != -1
) {
switch (c) {
case 'h': usage(stdout, 0); break;
Expand All @@ -293,6 +299,7 @@ static void parse_opts(int argc, char **argv) {
case 'l': opt_trace_limit = strtoull(optarg, NULL, 10); break;
case 'i': opt_time_limit_ms = strtol_with_min_or_exit("-i", optarg, 0); break;
case 'n': opt_max_stack_depth = atoi_with_min_or_exit("-n", optarg, -1); break;
case 'N': opt_max_stack_depth_outer = atoi_with_min_or_exit("-N", optarg, 0); break;
case 'r':
for (i = 0; i < strlen(optarg); i++) {
switch (optarg[i]) {
Expand Down
4 changes: 4 additions & 0 deletions phpspy.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
#define PHPSPY_MAX_ARRAY_BUCKETS 128
#define PHPSPY_MAX_ARRAY_TABLE_SIZE 512

/* Ceiling on how far a stack walk will chase prev_execute_data. Sampling a
running process can read a torn pointer, which would otherwise loop. */
#define PHPSPY_MAX_WALK 100000

#define PHPSPY_OK 0
#define PHPSPY_ERR 1
#define PHPSPY_ERR_PID_DEAD 2
Expand Down
69 changes: 68 additions & 1 deletion phpspy_trace.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,79 @@ static int trace_stack(trace_context_t *context, zend_execute_data *remote_execu
zend_op zop;
trace_target_t *target;
trace_frame_t *frame;
zend_execute_data *walk, *remote_prev;
int total_depth, keep_inner, keep_outer_from, num_elided, outer_emitted;

target = &context->target;
frame = &context->event.frame;
*depth = 0;

while (remote_execute_data && *depth != opt_max_stack_depth) { /* TODO make options struct */
keep_inner = 0;
keep_outer_from = -1;
num_elided = 0;
outer_emitted = 0;

if (opt_max_stack_depth_outer >= 0) {
/* Keeping the outermost frames means knowing how deep the stack is
before emitting anything, so count it first. This pass copies only
the prev_execute_data pointer, not the whole frame. */
total_depth = 0;
walk = remote_execute_data;
while (walk && total_depth < PHPSPY_MAX_WALK) {
try_copy_proc_mem(
"prev_execute_data",
((char*)walk) + offsetof(zend_execute_data, prev_execute_data),
&remote_prev,
sizeof(remote_prev)
);
walk = remote_prev;
total_depth += 1;
}

keep_inner = opt_max_stack_depth >= 0 ? opt_max_stack_depth : 0;
if (keep_inner + opt_max_stack_depth_outer < total_depth) {
keep_outer_from = total_depth - opt_max_stack_depth_outer;
num_elided = keep_outer_from - keep_inner;
}
}

while (remote_execute_data && *depth < PHPSPY_MAX_WALK) {
/* plain `-n` with no `-N`: stop once we have the innermost frames */
if (keep_outer_from < 0 && *depth == opt_max_stack_depth) break;

/* The stack can grow between the counting pass and this one, so cap
the outer frames by count rather than trusting the boundary; the
whole point of `-N` is a bounded number of them. */
if (keep_outer_from >= 0 && *depth >= keep_outer_from) {
if (outer_emitted >= opt_max_stack_depth_outer) break;
outer_emitted += 1;
}

if (keep_outer_from >= 0 && *depth >= keep_inner && *depth < keep_outer_from) {
/* Inside the elided middle. Stand one marker in for the run of
skipped frames, at the depth where they began -- consumers such
as stackcollapse-phpspy.pl treat depth 0 as the start of a
trace, so something must always be emitted there. */
if (*depth == keep_inner) {
frame->loc.func_len = snprintf(frame->loc.func, sizeof(frame->loc.func), "<elided:%d>", num_elided);
frame->loc.class[0] = '\0';
frame->loc.class_len = 0;
frame->loc.file_len = snprintf(frame->loc.file, sizeof(frame->loc.file), "<elided>");
frame->loc.lineno = -1;
frame->depth = *depth;
try(rv, context->event_handler(context, PHPSPY_TRACE_EVENT_FRAME));
}
try_copy_proc_mem(
"prev_execute_data",
((char*)remote_execute_data) + offsetof(zend_execute_data, prev_execute_data),
&remote_prev,
sizeof(remote_prev)
);
remote_execute_data = remote_prev;
*depth += 1;
continue;
}

memset(&execute_data, 0, sizeof(execute_data));
memset(&zfunc, 0, sizeof(zfunc));
memset(&zstring, 0, sizeof(zstring));
Expand Down
59 changes: 59 additions & 0 deletions tests/test_max_depth_outer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/bin/bash

php_src='function f($n){ if($n) f($n-1); else usleep(2000000); } f(20);'

# -n alone keeps the innermost frames, as before, with no elision marker.
phpspy_opts=(--max-depth=2 -- $PHP -r "$php_src")
declare -A expected
declare -A not_expected
expected[frame_0 ]='^0 usleep <internal>:-1$'
not_expected[no_marker ]='^\d+ <elided'
not_expected[no_outer ]='^22 <main> <internal>:-1$'
source $TEST_SH

# -N keeps the outermost frames. The marker must take depth 0, because
# stackcollapse-phpspy.pl treats depth 0 as the start of a trace.
phpspy_opts=(--max-depth-outer=2 -- $PHP -r "$php_src")
declare -A expected
declare -A not_expected
expected[marker_at_0 ]='^0 <elided:\d+> <elided>:-1$'
expected[outermost ]='^22 <main> <internal>:-1$'
not_expected[no_innermost]='^0 usleep <internal>:-1$'
source $TEST_SH

# Both ends, middle elided.
phpspy_opts=(--max-depth=2 --max-depth-outer=2 -- $PHP -r "$php_src")
declare -A expected
expected[frame_0 ]='^0 usleep <internal>:-1$'
expected[frame_1 ]='^1 f Command line code:1$'
expected[marker ]='^2 <elided:\d+> <elided>:-1$'
expected[outermost ]='^22 <main> <internal>:-1$'
source $TEST_SH

# A stack shallower than the cap must not be elided at all.
phpspy_opts=(--max-depth-outer=10 -- $PHP -r 'usleep(2000000);')
declare -A expected
declare -A not_expected
expected[frame_0 ]='^0 usleep <internal>:-1$'
expected[frame_1 ]='^1 <main> <internal>:-1$'
not_expected[no_marker ]='<elided'
source $TEST_SH

# Regression guard for the format trap: without a depth-0 line every trace in
# the file would collapse into a single stack. Assert each sample is counted.
collapse_out=$(mktemp)
on_exit() { rm -f $collapse_out; }
trap on_exit EXIT

REPO=$(dirname $(dirname $TEST_SH))
$PHPSPY --limit=0 --time-limit-ms=1500 --max-depth-outer=3 --warn-no-traces-s=0 \
-O/dev/null -E/dev/null -- $PHP -r "$php_src" >$collapse_out 2>/dev/null

n_traces=$(grep -c '^0 ' $collapse_out)
n_collapsed=$($REPO/stackcollapse-phpspy.pl <$collapse_out | awk '{s+=$NF} END{print s+0}')
if [ "$n_traces" -gt 0 -a "$n_traces" -eq "$n_collapsed" ]; then
echo -e " \x1b[32mOK \x1b[0m stackcollapse ($n_traces traces, $n_collapsed samples)"
else
echo -e " \x1b[31mERR \x1b[0m stackcollapse\nexpected=$n_traces samples\n\nactual=$n_collapsed"
exit 1
fi