| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* One boot lifecycle waypoint, stamped with the build generation that is |
| 9 |
* actually executing (Bruno timeout cause matrix, gaps G3 and GF). |
| 10 |
* |
| 11 |
* Split out of ABJ_404_Solution_AjaxCheckpointLogger, whose subject is a |
| 12 |
* different one: how a single checkpoint reaches disk. This class answers |
| 13 |
* "how far into boot are we, and whose opcodes are running" -- it composes |
| 14 |
* ABJ_404_Solution_OpcacheGenerationProbe, both compiled build markers and |
| 15 |
* ABJ_404_Solution_RequestEnvironmentFingerprint into a build-generation |
| 16 |
* verdict, and only then uses the journal as a sink. Nothing a reader of the |
| 17 |
* journal writer needs to understand lives here, and nothing here needs the |
| 18 |
* writer's internals. |
| 19 |
* |
| 20 |
* The dependency runs one way only (recorder -> journal writer); the writer |
| 21 |
* has no knowledge of boot waypoints. |
| 22 |
* |
| 23 |
* Called by the plugin bootstrap (404-solution.php: the file's own first |
| 24 |
* executable line, then `plugins_loaded`, `init` and `admin_init` at |
| 25 |
* PHP_INT_MIN) and by ABJ_404_Solution_AjaxAdminEndpointRegistrar on |
| 26 |
* admin-ajax dispatch. |
| 27 |
*/ |
| 28 |
final class ABJ_404_Solution_BootWaypointRecorder { |
| 29 |
|
| 30 |
/** |
| 31 |
* The boundary a caller that names none is measured at: the checkpoint |
| 32 |
* logger module itself, because its compiled marker is what every other |
| 33 |
* build id on the record is compared against. |
| 34 |
* |
| 35 |
* Stated as an explicit name/path pair rather than taken from __FILE__, |
| 36 |
* so moving this recorder between files can never silently re-point what |
| 37 |
* "the default boundary" means. |
| 38 |
*/ |
| 39 |
const DEFAULT_MODULE = 'AjaxCheckpointLogger'; |
| 40 |
|
| 41 |
/** |
| 42 |
* Record one boot lifecycle waypoint (Bruno timeout cause matrix, gap |
| 43 |
* G3): our own plugin file's first executable line, `plugins_loaded`, |
| 44 |
* `init`, `admin_init`, or admin-ajax action dispatch. Every record |
| 45 |
* carries the delta from REQUEST_TIME_FLOAT (via |
| 46 |
* ABJ_404_Solution_RequestEnvironmentFingerprint::bootDelta(), the same |
| 47 |
* formula request_start uses), so the gaps between consecutive |
| 48 |
* waypoints localize a slow boot to a phase instead of a single total, |
| 49 |
* and a request that dies before trace construction still has its boot |
| 50 |
* cost attributable from whichever waypoints it reached. |
| 51 |
* |
| 52 |
* Scope is gated by ABJ_404_Solution_AjaxDiagnosticRequestPolicy::bootWaypointRequestId() |
| 53 |
* to our own table-AJAX and canary-ladder requests: never for ordinary |
| 54 |
* front-end page views, where the write cost would land on the hot 404 |
| 55 |
* path. Never throws. |
| 56 |
* |
| 57 |
* @param array{module?: string, path?: string, build_id?: string} $boundary |
| 58 |
*/ |
| 59 |
public static function record(string $event, array $boundary = array()): void { |
| 60 |
try { |
| 61 |
$requestId = ABJ_404_Solution_AjaxDiagnosticRequestPolicy::bootWaypointRequestId(); |
| 62 |
if ($requestId === '') { |
| 63 |
return; |
| 64 |
} |
| 65 |
$loggerBuildId = ABJ_404_Solution_AjaxCheckpointLogger::DIAGNOSTIC_BUILD_ID; |
| 66 |
$now = self::nowFloat(); |
| 67 |
$module = is_string($boundary['module'] ?? null) && $boundary['module'] !== '' |
| 68 |
? $boundary['module'] : self::DEFAULT_MODULE; |
| 69 |
$modulePath = is_string($boundary['path'] ?? null) && $boundary['path'] !== '' |
| 70 |
? $boundary['path'] : __DIR__ . '/AjaxCheckpointLogger.php'; |
| 71 |
$boundaryBuildId = $boundary['build_id'] ?? null; |
| 72 |
$moduleBuildId = self::validBuildId($boundaryBuildId) |
| 73 |
? (string)$boundaryBuildId : $loggerBuildId; |
| 74 |
$rootBuildId = defined('ABJ404_DIAGNOSTIC_BUILD_ID') |
| 75 |
&& self::validBuildId(ABJ404_DIAGNOSTIC_BUILD_ID) |
| 76 |
? (string)ABJ404_DIAGNOSTIC_BUILD_ID : null; |
| 77 |
$boundaryOpcache = ABJ_404_Solution_OpcacheGenerationProbe::boundarySnapshot($modulePath); |
| 78 |
$boundaryOpcache['module'] = $module; |
| 79 |
$boundaryOpcache['compiled_build_id'] = $moduleBuildId; |
| 80 |
$boundaryOpcache['matches_checkpoint_logger'] = hash_equals( |
| 81 |
$loggerBuildId, |
| 82 |
$moduleBuildId |
| 83 |
); |
| 84 |
// A waypoint with no clock is still worth recording: WHICH boot |
| 85 |
// phase was reached is the measurement, and the delta is the |
| 86 |
// refinement. Sending a stand-in number into bootDelta() would |
| 87 |
// publish a boot duration that no clock produced. |
| 88 |
$fields = $now === null |
| 89 |
? array('request_time_float' => null, 'boot_delta_ms' => null) |
| 90 |
: ABJ_404_Solution_RequestEnvironmentFingerprint::bootDelta($now); |
| 91 |
$fields['diagnostic_build_id'] = $moduleBuildId; |
| 92 |
$fields['checkpoint_logger_build_id'] = $loggerBuildId; |
| 93 |
$fields['root_boot_build_id'] = $rootBuildId; |
| 94 |
$fields['build_generation_consistent'] = |
| 95 |
$boundaryOpcache['matches_checkpoint_logger'] |
| 96 |
&& ($rootBuildId === null || hash_equals($loggerBuildId, $rootBuildId)); |
| 97 |
$fields['boundary_opcache'] = $boundaryOpcache; |
| 98 |
ABJ_404_Solution_AjaxCheckpointLogger::record($requestId, $event, $fields); |
| 99 |
} catch (Throwable $e) { |
| 100 |
// Same inert sink the journal writer reports through: the plugin's |
| 101 |
// own logger reads settings to find its log file, which is exactly |
| 102 |
// the recursion a boot-time diagnostic must not start. |
| 103 |
abj404_logPhpFallback( |
| 104 |
'ajax-checkpoint', |
| 105 |
'AJAX boot waypoint record failed (' . $event . '): ' . $e->getMessage() |
| 106 |
); |
| 107 |
} |
| 108 |
} |
| 109 |
|
| 110 |
/** @param mixed $value */ |
| 111 |
private static function validBuildId($value): bool { |
| 112 |
return is_string($value) && preg_match('/^[0-9a-f]{40}$/', $value) === 1; |
| 113 |
} |
| 114 |
|
| 115 |
private static function nowFloat(): ?float { |
| 116 |
if (function_exists('abj_clock')) { |
| 117 |
return abj_clock()->nowFloat(); |
| 118 |
} |
| 119 |
if (class_exists('ABJ_404_Solution_SystemClock')) { |
| 120 |
return (new ABJ_404_Solution_SystemClock())->nowFloat(); |
| 121 |
} |
| 122 |
return null; |
| 123 |
} |
| 124 |
|
| 125 |
} |
| 126 |
|