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 / BootWaypointRecorder.php

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

126 lines 6.0 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 * 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