buildSimpleUrlFallback($url); } return [ 'url' => mb_substr($url, 0, self::URL_MAX_LENGTH), 'simple_url' => mb_substr($simpleUrl, 0, self::SIMPLE_URL_MAX_LENGTH), 'method' => mb_substr((string) ($meta['SERVER']['REQUEST_METHOD'] ?? ''), 0, self::METHOD_MAX_LENGTH), 'wall_time' => $wallTime, 'cpu_time' => $cpuTime, 'memory_peak' => $memoryPeak, 'request_time' => $this->parseRequestTime($meta), 'profile' => $data['profile'], 'degraded' => $degraded, ]; } /** * simple_url 兜底计算. * * 与采集端 bootstrap 的 profiler.simple_url 回调算法完全一致(唯一实现点在采集端, * 这里仅做缺失兜底):path 去 query、纯数字段替换 ':id'、'/' 连接(无前导斜杠)。 */ public function buildSimpleUrlFallback(string $url): string { $path = parse_url($url, PHP_URL_PATH); if (!is_string($path) || $path === '') { return ''; } $parts = explode('/', trim($path, '/')); foreach ($parts as &$part) { if ($part !== '' && ctype_digit($part)) { $part = ':id'; } } unset($part); return implode('/', $parts); } /** * 编译 watch 正则(导入批次调用一次,结果供 materializeWatches 复用,避免逐行重复编译). * * @param array $watches [['id' => int, 'regex' => string], ...] * * @return array{patterns: array, fails: int} * patterns 以 watch_id 为键;id 非法/正则为空/@preg_match 编译失败 * 的条目跳过并计入 fails(防非法正则打爆导入任务) */ public function compileWatchRegexes(array $watches): array { $patterns = []; $fails = 0; foreach ($watches as $watch) { $id = (int) ($watch['id'] ?? 0); $regex = (string) ($watch['regex'] ?? ''); if ($id <= 0 || $regex === '') { $fails++; continue; } if (@preg_match($regex, '') === false) { $fails++; continue; } $patterns[$id] = $regex; } return ['patterns' => $patterns, 'fails' => $fails]; } /** * watch 物化:拆 "caller==>callee" 边,按 callee(被调用函数名)匹配正则. * * 命中边 ct 累加、wt 累加 wt_sum、wt_max 取最大。 * 裸键(如 main())无 ==> 分隔符,不参与物化。 * * @param array $profile 解码后的 profile 边表 * @param array $patterns compileWatchRegexes 产出的 [watch_id => regex] * * @return array 以 watch_id 为键的聚合行 */ public function materializeWatches(array $profile, array $patterns): array { if (empty($patterns)) { return []; } $result = []; foreach ($profile as $key => $metrics) { $sep = strpos($key, '==>'); if ($sep === false) { continue; } $callee = substr($key, $sep + 3); $ct = (int) ($metrics['ct'] ?? 0); $wt = (int) ($metrics['wt'] ?? 0); foreach ($patterns as $watchId => $regex) { if (@preg_match($regex, $callee) !== 1) { continue; } if (!isset($result[$watchId])) { $result[$watchId] = ['ct' => 0, 'wt_sum' => 0, 'wt_max' => 0]; } $result[$watchId]['ct'] += $ct; $result[$watchId]['wt_sum'] += $wt; if ($wt > $result[$watchId]['wt_max']) { $result[$watchId]['wt_max'] = $wt; } } } return $result; } /** * request_time:优先 meta.request_ts_micro(sec+usec),缺失退化 meta.SERVER.REQUEST_TIME_FLOAT. * * @return float 浮点秒(round 3 位,与 decimal(12,3) 列精度对齐) */ protected function parseRequestTime(array $meta): float { $ts = $meta['request_ts_micro'] ?? null; if (is_array($ts) && isset($ts['sec'])) { $sec = (int) $ts['sec']; $usec = (int) ($ts['usec'] ?? 0); return round($sec + $usec / 1000000, 3); } $float = $meta['SERVER']['REQUEST_TIME_FLOAT'] ?? null; if (is_numeric($float)) { return round((float) $float, 3); } return 0.0; } }