diff --git a/README.md b/README.md index 7ea1291..08bd744 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,10 @@ All with no changes to your application and minimal overhead. (default: 0; 0=unlimited) -n, --max-depth= Set max stack trace depth (default: -1; -1=unlimited) + -N, --max-depth-outer= 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= Set request info parts to capture (q=query c=cookie u=uri p=path capital=negation) diff --git a/phpspy.c b/phpspy.c index 4722db1..6f51ffc 100644 --- a/phpspy.c +++ b/phpspy.c @@ -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; @@ -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= Set max stack trace depth\n"); fprintf(fp, " (default: %d; -1=unlimited)\n", opt_max_stack_depth); + fprintf(fp, " -N, --max-depth-outer= 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= Set request info parts to capture\n"); fprintf(fp, " (q=query c=cookie u=uri p=path\n"); fprintf(fp, " capital=negation)\n"); @@ -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' }, @@ -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; @@ -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]) { diff --git a/phpspy.h b/phpspy.h index 5951003..1a97255 100644 --- a/phpspy.h +++ b/phpspy.h @@ -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 diff --git a/phpspy_trace.c b/phpspy_trace.c index 3d8aad0..1d4ea4d 100644 --- a/phpspy_trace.c +++ b/phpspy_trace.c @@ -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), "", num_elided); + frame->loc.class[0] = '\0'; + frame->loc.class_len = 0; + frame->loc.file_len = snprintf(frame->loc.file, sizeof(frame->loc.file), ""); + 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)); diff --git a/tests/test_max_depth_outer.sh b/tests/test_max_depth_outer.sh new file mode 100755 index 0000000..b9ac0e2 --- /dev/null +++ b/tests/test_max_depth_outer.sh @@ -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 :-1$' +not_expected[no_marker ]='^\d+ $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