PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / diagnostics / ShutdownCallbackTracer.php

ShutdownCallbackTracer.php in 404 Solution trunk, at includes/diagnostics/ShutdownCallbackTracer.php

174 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Attributes each foreign WordPress shutdown callback after handler work.
9 *
10 * Instrumentation is armed only when the AJAX response is ready, after normal
11 * plugins have registered their callbacks and before WordPress dispatches the
12 * shutdown action. Reference signatures retain the shared instrumenter's
13 * marker semantics; ordinary callbacks receive exact start/end boundaries.
14 */
15 final class ABJ_404_Solution_ShutdownCallbackTracer
16 implements ABJ_404_Solution_DiagnosticInternalHookObserver {
17
18 /** @var string */
19 private $requestId;
20
21 /** @var ABJ_404_Solution_HookInstrumentationLifecycleTracer */
22 private $lifecycle;
23
24 /** @var ABJ_404_Solution_HookCallbackInstrumenter<array<string, mixed>> */
25 private $instrumenter;
26
27 /** @var int */
28 private $sequence = 0;
29
30 /** @var bool */
31 private $armed = false;
32
33 /** @var bool */
34 private $completed = false;
35
36 public function __construct(string $requestId, string $directory) {
37 $this->requestId = $requestId;
38 $this->lifecycle = new ABJ_404_Solution_HookInstrumentationLifecycleTracer(
39 $requestId,
40 'shutdown_callback_tracer',
41 $directory
42 );
43 $this->instrumenter = new ABJ_404_Solution_HookCallbackInstrumenter(
44 array($this, 'beginCallback'),
45 array($this, 'completeCallback'),
46 $this->lifecycle
47 );
48 }
49
50 /** Register the coarse request sentinels through the same traced registry boundary. */
51 public function registerSentinels(callable $early, callable $late): void {
52 $this->lifecycle->registerAction('shutdown', $early, PHP_INT_MIN);
53 $this->lifecycle->registerAction('shutdown', $late, PHP_INT_MAX);
54 }
55
56 /** Install callback attribution and a last-priority restoration sentinel. */
57 public function arm(): void {
58 if ($this->armed || $this->requestId === '') {
59 return;
60 }
61 $this->armed = true;
62 try {
63 $counts = $this->instrumenter->instrument('shutdown');
64 ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent(
65 $this->requestId,
66 'shutdown_callback_instrumentation',
67 $counts
68 );
69 $this->lifecycle->registerAction(
70 'shutdown',
71 array($this, 'completeScope'),
72 PHP_INT_MAX
73 );
74 } catch (Throwable $e) {
75 $this->reportFailure('instrumentation failed: ' . $e->getMessage());
76 $this->restoreAfterArmFailure();
77 }
78 }
79
80 /**
81 * @param array{callback: string, source: string, has_reference: bool} $identity
82 * @return array<string, mixed>
83 */
84 public function beginCallback(
85 string $registeredHook,
86 string $actualHook,
87 int $priority,
88 array $identity,
89 int $ordinal
90 ): array {
91 $this->sequence++;
92 $fields = array(
93 'operation_id' => substr(hash(
94 'sha256',
95 $this->requestId . '|shutdown|' . $priority . '|' . $ordinal . '|'
96 . $identity['callback'] . '|' . $this->sequence
97 ), 0, 12),
98 'hook' => ABJ_404_Solution_HookCallbackIdentity::hookName($actualHook),
99 'callback' => $identity['callback'],
100 'source' => $identity['source'],
101 'priority' => $priority,
102 'callback_ordinal' => max(0, $ordinal),
103 'has_reference' => $identity['has_reference'],
104 'started_at' => $this->nowFloat(),
105 );
106 ABJ_404_Solution_AjaxCheckpointLogger::recordActiveOperation(
107 $this->requestId,
108 'shutdown_callback',
109 'active',
110 $fields
111 );
112 ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent(
113 $this->requestId,
114 'shutdown_callback_start',
115 $fields
116 );
117 return $fields;
118 }
119
120 /** @param array<string, mixed> $token */
121 public function completeCallback(array $token): void {
122 $startedAt = is_float($token['started_at'] ?? null) ? $token['started_at'] : null;
123 unset($token['started_at']);
124 $token['elapsed_ms'] = $this->elapsedMs($startedAt);
125 ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent(
126 $this->requestId,
127 'shutdown_callback_end',
128 $token
129 );
130 ABJ_404_Solution_AjaxCheckpointLogger::recordActiveOperation(
131 $this->requestId,
132 'shutdown_callback',
133 'complete',
134 $token
135 );
136 }
137
138 /** Final WordPress shutdown callback: close marker tokens and restore the registry. */
139 public function completeScope(): void {
140 if ($this->completed) {
141 return;
142 }
143 $this->completed = true;
144 try {
145 $this->instrumenter->restore(true);
146 } catch (Throwable $e) {
147 $this->reportFailure('restoration failed: ' . $e->getMessage());
148 }
149 }
150
151 private function restoreAfterArmFailure(): void {
152 try {
153 $this->instrumenter->restore(false);
154 } catch (Throwable $restoreError) {
155 $this->reportFailure('arm-failure restoration failed: ' . $restoreError->getMessage());
156 }
157 }
158
159 private function nowFloat(): ?float {
160 return function_exists('abj_clock') ? abj_clock()->nowFloat() : null;
161 }
162
163 private function elapsedMs(?float $startedAt): ?int {
164 $endedAt = $this->nowFloat();
165 return $startedAt === null || $endedAt === null
166 ? null
167 : max(0, (int)round(($endedAt - $startedAt) * 1000));
168 }
169
170 private function reportFailure(string $message): void {
171 abj404_logPhpFallback('shutdown-callback-tracer', $message);
172 }
173 }
174