Skip to content

Commit c1bc8ee

Browse files
committed
fix: guard engine hook span allocation
1 parent 0d5400d commit c1bc8ee

3 files changed

Lines changed: 151 additions & 78 deletions

File tree

CODE_REVIEW.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ Validation performed on macOS/PHP 8.5.6:
5252
Result: **7 passed, 0 failed**.
5353
- `TEST_PHP_EXECUTABLE=$(which php) make test NO_INTERACTION=1 TESTS='tests/221-twig-long-block-name-clamps-buffer.phpt tests/015-long-function-names.phpt tests/214-twig-template-name.phpt tests/215-twig-wrapper-template-name.phpt tests/216-symfony-event-dispatch-name.phpt tests/201-io-spans-with-trace-functions-off.phpt tests/205-many-io-spans.phpt'`
5454
Result: **7 passed, 0 failed**.
55+
- `TEST_PHP_EXECUTABLE=$(which php) make test NO_INTERACTION=1 TESTS='tests/222-engine-hooks-capacity-smoke.phpt tests/084-otlp-runtime-env.phpt tests/080-otlp-valid-json.phpt tests/102-span-capacity-grows.phpt tests/217-exception-status-first-span.phpt tests/218-log-exception-status-first-span.phpt'`
56+
Result: **6 passed, 0 failed**.
5557

5658
### Critical / High Follow-up Status
5759

@@ -74,7 +76,7 @@ Validation performed on macOS/PHP 8.5.6:
7476
|---------|--------|-------|
7577
| Manual spans are never finished | **Fixed** | Manual spans are marked and finalized during request shutdown finalization before UDP export. `createSpan()` parent lookup now uses the validated current-span helper that skips sentinels. Covered by `tests/219-manual-span-parent-skips-sentinel.phpt` and `tests/220-manual-span-exported-on-disable.phpt`. |
7678
| cURL hook uses private ext/curl layout and leaks `curl_slist` | **Open / partial** | Header restore support exists, but the hook still relies on private ext/curl layout and restored `curl_slist`s are not retained/freed after `curl_easy_setopt()`. `curl_setopt_array()` is still not handled. |
77-
| Engine hooks ignore capacity failures | **Open** | Compile/GC hooks still call `ensure_span_capacity(state)` and increment `span_count` even if capacity fails. |
79+
| Engine hooks ignore capacity failures | **Fixed** | Compile/GC hooks now return early when `ensure_span_capacity(state)` fails, so `span_count` is only incremented after capacity is guaranteed. Engine db attrs also grow on demand. Covered by `tests/222-engine-hooks-capacity-smoke.phpt` and span capacity regression coverage. |
7880
| Global request state is not ZTS-safe | **Open** | `g_state` is still process-global. README currently claims full ZTS support, which does not match the implementation. |
7981

8082
### Tests, CI, and Packaging Status
@@ -86,15 +88,14 @@ Validation performed on macOS/PHP 8.5.6:
8688
| Optional integrations mostly skipped in default CI | **Open** | Redis, MySQLi, and Memcached tests still depend on local optional services/extensions and skipped locally. |
8789
| macOS/BSD portability is not covered by CI | **Open** | `.github/workflows/tests.yml` still only runs Ubuntu. |
8890
| Build config has implicit libcurl dependency | **Open** | `config.m4` still does not check curl headers/libraries despite `hook_curl.c` including `<curl/curl.h>`. |
89-
| README/demo drift | **Open** | README still documents `akari.mode=auto`, lists 74 PHPT tests while 89 exist, and demo README uses port 8081 while `docker-compose.yml` maps the PHP demo to 8083. |
91+
| README/demo drift | **Open** | README still documents `akari.mode=auto`, lists 74 PHPT tests while 95 exist, and demo README uses port 8081 while `docker-compose.yml` maps the PHP demo to 8083. |
9092

9193
### Remaining Remediation Order
9294

93-
1. Fix engine hook capacity handling before `span_count++`.
94-
2. Replace network-backed curl PHPTs with local fixtures or real `--SKIPIF--` checks.
95-
3. Add CI jobs for `go test ./...`, macOS, and optional Redis/MySQL/Memcached integration tests.
96-
4. Add explicit libcurl checks to `config.m4`.
97-
5. Update README/demo claims to match current behavior.
95+
1. Replace network-backed curl PHPTs with local fixtures or real `--SKIPIF--` checks.
96+
2. Add CI jobs for `go test ./...`, macOS, and optional Redis/MySQL/Memcached integration tests.
97+
3. Add explicit libcurl checks to `config.m4`.
98+
4. Update README/demo claims to match current behavior.
9899

99100
## Executive Summary
100101

src/hook_engine.c

Lines changed: 99 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,46 @@
1717
static zend_op_array *(*original_compile_file)(zend_file_handle *file_handle, int type) = NULL;
1818
static int (*original_gc_collect_cycles)(void) = NULL;
1919

20+
/* ── Engine attr helpers ── */
21+
22+
static profiler_db_attr_t *engine_db_attr_alloc(profiler_state_t *state,
23+
uint32_t span_index,
24+
const char *system)
25+
{
26+
if (state->db_attr_count >= state->db_attr_capacity) {
27+
size_t new_cap = state->db_attr_capacity
28+
? state->db_attr_capacity * 2
29+
: PROFILER_INITIAL_DB_ATTRS;
30+
profiler_db_attr_t *new_attrs = realloc(state->db_attrs,
31+
new_cap * sizeof(profiler_db_attr_t));
32+
if (!new_attrs) return NULL;
33+
state->db_attrs = new_attrs;
34+
state->db_attr_capacity = new_cap;
35+
}
36+
37+
profiler_db_attr_t *attr = &state->db_attrs[state->db_attr_count++];
38+
memset(attr, 0, sizeof(profiler_db_attr_t));
39+
attr->span_index = span_index;
40+
snprintf(attr->db_system, sizeof(attr->db_system), "%s", system);
41+
return attr;
42+
}
43+
44+
static void engine_db_attr_set_statement(profiler_db_attr_t *attr,
45+
const char *statement,
46+
size_t statement_len)
47+
{
48+
if (!attr || !statement || statement_len == 0) return;
49+
50+
size_t copy_len = statement_len;
51+
if (copy_len >= sizeof(attr->db_statement)) {
52+
copy_len = sizeof(attr->db_statement) - 1;
53+
}
54+
55+
memcpy(attr->db_statement, statement, copy_len);
56+
attr->db_statement[copy_len] = '\0';
57+
attr->db_statement_len = copy_len;
58+
}
59+
2060
/* ── Compile file hook ──
2161
*
2262
* Wraps zend_compile_file to measure compilation time per file.
@@ -37,48 +77,38 @@ static zend_op_array *profiler_compile_file(zend_file_handle *file_handle, int t
3777

3878
/* Only record if compilation took >1ms */
3979
if (elapsed >= 1000000) {
40-
ensure_span_capacity(state);
80+
if (!ensure_span_capacity(state)) {
81+
return result;
82+
}
4183

4284
size_t span_idx = state->span_count++;
43-
if (span_idx < state->span_capacity) {
44-
profiler_span_t *span = &state->spans[span_idx];
45-
memset(span, 0, sizeof(profiler_span_t));
46-
47-
memcpy(span->trace_id, state->trace_id, 32);
48-
profiler_generate_hex_id(state, span->span_id, 16);
49-
span->start_time_ns = t_start;
50-
span->end_time_ns = t_end;
51-
span->depth = state->stack_depth;
52-
span->kind = SPAN_KIND_INTERNAL;
53-
span->status_code = SPAN_STATUS_UNSET;
54-
55-
/* Parent = current root if active */
56-
if (state->root.active) {
57-
memcpy(span->parent_span_id, state->root.span_id, 16);
58-
span->has_parent = 1;
59-
}
60-
61-
/* Record filename as attribute */
62-
if (file_handle && file_handle->filename) {
63-
const char *fname = ZSTR_VAL(file_handle->filename);
64-
size_t fname_len = ZSTR_LEN(file_handle->filename);
65-
66-
if (state->db_attr_count < state->db_attr_capacity) {
67-
profiler_db_attr_t *attr = &state->db_attrs[state->db_attr_count];
68-
memset(attr, 0, sizeof(profiler_db_attr_t));
69-
attr->span_index = (uint32_t)span_idx;
70-
snprintf(attr->db_system, DB_SYSTEM_MAX, "compile");
71-
size_t copy_len = fname_len;
72-
if (copy_len >= DB_STATEMENT_MAX) copy_len = DB_STATEMENT_MAX - 1;
73-
memcpy(attr->db_statement, fname, copy_len);
74-
attr->db_statement[copy_len] = '\0';
75-
attr->db_statement_len = copy_len;
76-
state->db_attr_count++;
77-
}
78-
}
79-
80-
maybe_flush(state);
85+
profiler_span_t *span = &state->spans[span_idx];
86+
memset(span, 0, sizeof(profiler_span_t));
87+
88+
memcpy(span->trace_id, state->trace_id, 32);
89+
profiler_generate_hex_id(state, span->span_id, 16);
90+
span->start_time_ns = t_start;
91+
span->end_time_ns = t_end;
92+
span->depth = state->stack_depth;
93+
span->kind = SPAN_KIND_INTERNAL;
94+
span->status_code = SPAN_STATUS_UNSET;
95+
96+
/* Parent = current root if active */
97+
if (state->root.active) {
98+
memcpy(span->parent_span_id, state->root.span_id, 16);
99+
span->has_parent = 1;
100+
}
101+
102+
/* Record filename as attribute */
103+
if (file_handle && file_handle->filename) {
104+
profiler_db_attr_t *attr = engine_db_attr_alloc(state,
105+
(uint32_t)span_idx, "compile");
106+
engine_db_attr_set_statement(attr,
107+
ZSTR_VAL(file_handle->filename),
108+
ZSTR_LEN(file_handle->filename));
81109
}
110+
111+
maybe_flush(state);
82112
}
83113
}
84114

@@ -105,41 +135,39 @@ static int profiler_gc_collect_cycles(void)
105135

106136
/* Only record if GC took >5ms (GC is typically fast) */
107137
if (elapsed >= 5000000) {
108-
ensure_span_capacity(state);
138+
if (!ensure_span_capacity(state)) {
139+
return collected;
140+
}
109141

110142
size_t span_idx = state->span_count++;
111-
if (span_idx < state->span_capacity) {
112-
profiler_span_t *span = &state->spans[span_idx];
113-
memset(span, 0, sizeof(profiler_span_t));
114-
115-
memcpy(span->trace_id, state->trace_id, 32);
116-
profiler_generate_hex_id(state, span->span_id, 16);
117-
span->start_time_ns = t_start;
118-
span->end_time_ns = t_end;
119-
span->depth = state->stack_depth;
120-
span->kind = SPAN_KIND_INTERNAL;
121-
span->status_code = SPAN_STATUS_UNSET;
122-
123-
if (state->root.active) {
124-
memcpy(span->parent_span_id, state->root.span_id, 16);
125-
span->has_parent = 1;
126-
}
127-
128-
/* Record GC stats as attribute */
129-
if (state->db_attr_count < state->db_attr_capacity) {
130-
profiler_db_attr_t *attr = &state->db_attrs[state->db_attr_count];
131-
memset(attr, 0, sizeof(profiler_db_attr_t));
132-
attr->span_index = (uint32_t)span_idx;
133-
snprintf(attr->db_system, DB_SYSTEM_MAX, "gc");
134-
snprintf(attr->db_statement, DB_STATEMENT_MAX,
135-
"collected %d roots in %.2fms",
136-
collected, elapsed / 1000000.0);
137-
attr->db_statement_len = strlen(attr->db_statement);
138-
state->db_attr_count++;
139-
}
140-
141-
maybe_flush(state);
143+
profiler_span_t *span = &state->spans[span_idx];
144+
memset(span, 0, sizeof(profiler_span_t));
145+
146+
memcpy(span->trace_id, state->trace_id, 32);
147+
profiler_generate_hex_id(state, span->span_id, 16);
148+
span->start_time_ns = t_start;
149+
span->end_time_ns = t_end;
150+
span->depth = state->stack_depth;
151+
span->kind = SPAN_KIND_INTERNAL;
152+
span->status_code = SPAN_STATUS_UNSET;
153+
154+
if (state->root.active) {
155+
memcpy(span->parent_span_id, state->root.span_id, 16);
156+
span->has_parent = 1;
142157
}
158+
159+
/* Record GC stats as attribute */
160+
profiler_db_attr_t *attr = engine_db_attr_alloc(state,
161+
(uint32_t)span_idx, "gc");
162+
if (attr) {
163+
int n = snprintf(attr->db_statement, sizeof(attr->db_statement),
164+
"collected %d roots in %.2fms",
165+
collected, elapsed / 1000000.0);
166+
attr->db_statement_len = profiler_clamp_snprintf_len(n,
167+
sizeof(attr->db_statement));
168+
}
169+
170+
maybe_flush(state);
143171
}
144172
}
145173

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
Engine hooks: compile and GC profiling preserve span state
3+
--SKIPIF--
4+
<?php include __DIR__ . '/_skipif.inc'; ?>
5+
--INI--
6+
akari.enable=1
7+
akari.trace_functions=0
8+
akari.trace_internal=1
9+
akari.trace_compile=1
10+
akari.trace_gc=1
11+
--FILE--
12+
<?php
13+
$path = tempnam(sys_get_temp_dir(), 'akari_engine_hook_');
14+
$prefix = 'akari_engine_hook_' . getmypid() . '_';
15+
$code = "<?php\n";
16+
for ($i = 0; $i < 800; $i++) {
17+
$code .= "function {$prefix}{$i}() { return {$i}; }\n";
18+
}
19+
file_put_contents($path, $code);
20+
include $path;
21+
unlink($path);
22+
23+
gc_enable();
24+
$cycles = [];
25+
for ($i = 0; $i < 1000; $i++) {
26+
$node = [];
27+
$node['self'] = &$node;
28+
$cycles[] = $node;
29+
}
30+
unset($cycles, $node);
31+
gc_collect_cycles();
32+
33+
usleep(100);
34+
35+
$json = Akari\getSpansJson();
36+
$data = json_decode($json, true);
37+
38+
echo "valid json: " . (is_array($data) ? 'yes' : 'no') . "\n";
39+
$spans = $data['resourceSpans'][0]['scopeSpans'][0]['spans'] ?? [];
40+
echo "has spans: " . (count($spans) > 0 ? 'yes' : 'no') . "\n";
41+
?>
42+
--EXPECT--
43+
valid json: yes
44+
has spans: yes

0 commit comments

Comments
 (0)