'desc', ]; public function __construct(App $app) { parent::__construct($app); $this->model = new \app\admin\model\XhprofRun(); } /** * @NodeAnotation(title="列表") */ public function index() { if ($this->request->isAjax()) { if (input('selectFields')) { return $this->selectList(); } // wall_time 搜索下限:前端输入毫秒,换算为微秒后再构建查询条件 // 注意:Request::withGet 是整体替换,必须先取全量 GET 再合并写回 $filter = (string) $this->request->get('filter', ''); if ($filter !== '') { $filterArr = json_decode($filter, true) ?: []; if (isset($filterArr['wall_time']) && $filterArr['wall_time'] !== '') { $filterArr['wall_time'] = (string) ((float) $filterArr['wall_time'] * 1000); $this->request->withGet(array_merge($this->request->get(), [ 'filter' => json_encode($filterArr, JSON_UNESCAPED_UNICODE), ])); } } list($page, $limit, $where, $excludes, $request_options, $group) = $this->buildTableParames(); $count = $this->model ->where($where) ->group($group) ->count(); $list = $this->model ->where($where) ->page($page, $limit) ->order($this->sort) ->group($group) ->select(); $data = [ 'code' => 0, 'msg' => '', 'count' => $count, 'data' => $list, ]; return json($data); } // 节点下拉选项(多节点环境区分数据来源) $nodeIds = $this->model->distinct(true)->column('node_id'); $nodeSelect = array_combine($nodeIds, $nodeIds) ?: []; $this->assign('select_list_node_id', $nodeSelect, true); return $this->fetch(); } /** * 采样详情:页面/接口同体 * * 页面模式(浏览器/弹层 iframe,无 Accept json):渲染详情页壳,顶部摘要在服务端渲染, * 函数表与 SQL 摘要由 detail.js 以接口模式取同一 URL 补齐。 * 接口模式(Accept: application/json,框架 RequestBase::isAjax 判别): * 返回函数级聚合分析 JSON(XHProf 边模型,XHGui 同款算法)。 * * @NodeAnotation(title="详情") */ public function detail() { $id = (int) input('id', 0); if ($this->request->isAjax()) { return $this->detailAnalysis($id); } // 页面模式:渲染页面壳(get_page_data=1 时 fetch 自动转 assign 数据 JSON) $run = $this->model->find($id); if (!$run) { $this->error('采样记录不存在或已被清理'); } $runArr = $run->toArray(); $summary = [ 'wall_time_ms' => round((int) $runArr['wall_time'] / 1000, 1), 'cpu_time_ms' => round((int) $runArr['cpu_time'] / 1000, 1), 'memory_peak_mb' => round((int) $runArr['memory_peak'] / 1048576, 2), 'request_time_text' => date('Y-m-d H:i:s', (int) $runArr['request_time']), 'url_display' => htmlspecialchars($runArr['url'] ?: '/', ENT_QUOTES, 'UTF-8'), 'node_display' => htmlspecialchars((string) $runArr['node_id'], ENT_QUOTES, 'UTF-8'), ]; $this->assign('run', $runArr); $this->assign('summary', $summary); return $this->fetch(); } /** * 函数级聚合分析(接口模式). * * XHProf 边模型(XHGui 同款): * 1. inclusive(func) = Σ 所有 `X==>func` 边的 wt/ct/mu(callee 侧累计); * main() 无入边,其 inclusive 由裸键 profile['main()'] 播种; * 2. exclusive(func) = inclusive(func) - Σ 所有 `func==>callee` 边的 wt; * 递归边(foo==>foo)一次计入 inclusive、一次扣减,对 exclusive 净影响为零; * 3. 按 exclusive wt 降序取 top 100。 * * 两遍 O(n) 遍历,不对 LONGTEXT 做 SQL 匹配,SQL 摘要为纯 PHP 过滤聚合结果。 * * @param int $id 采样记录 id */ protected function detailAnalysis(int $id) { $run = $this->model->find($id); if (!$run) { $this->error('采样记录不存在或已被清理'); } $detail = \app\admin\model\XhprofRunDetail::find($id); if (!$detail) { $this->error('采样明细数据缺失'); } $raw = json_decode((string) $detail->profile_data, true); $profile = (is_array($raw) && isset($raw['profile']) && is_array($raw['profile'])) ? $raw['profile'] : []; // 第一遍:裸键播种(main())+ 边累计 callee 侧 inclusive $inclWt = []; $inclCt = []; $inclMu = []; foreach ($profile as $key => $stat) { $wt = (int) ($stat['wt'] ?? 0); $ct = (int) ($stat['ct'] ?? 0); $mu = (int) ($stat['mu'] ?? 0); if (strpos($key, '==>') === false) { $fn = $key; $inclWt[$fn] = ($inclWt[$fn] ?? 0) + $wt; $inclCt[$fn] = ($inclCt[$fn] ?? 0) + $ct; $inclMu[$fn] = ($inclMu[$fn] ?? 0) + $mu; continue; } [$caller, $callee] = explode('==>', $key, 2); $inclWt[$callee] = ($inclWt[$callee] ?? 0) + $wt; $inclCt[$callee] = ($inclCt[$callee] ?? 0) + $ct; $inclMu[$callee] = ($inclMu[$callee] ?? 0) + $mu; } // 第二遍:exclusive = inclusive - Σ 出边 wt(caller 只出现无入边记录时兜底 0) $exclWt = $inclWt; foreach ($profile as $key => $stat) { if (strpos($key, '==>') === false) { continue; } [$caller, $callee] = explode('==>', $key, 2); if (!isset($exclWt[$caller])) { $exclWt[$caller] = 0; $inclCt[$caller] = $inclCt[$caller] ?? 0; $inclMu[$caller] = $inclMu[$caller] ?? 0; } $exclWt[$caller] -= (int) ($stat['wt'] ?? 0); } // 汇总函数行 + SQL 摘要(think\db 前缀,大小写不敏感;独占耗时合计避免重复计数) $rows = []; $sqlCt = 0; $sqlWt = 0; $sqlFnCount = 0; foreach ($inclWt as $fn => $wt) { $excl = max(0, $exclWt[$fn] ?? 0); $rows[] = [ 'fn' => $fn, 'ct' => $inclCt[$fn] ?? 0, 'excl_wt' => $excl, 'incl_wt' => $wt, 'mu' => $inclMu[$fn] ?? 0, ]; if (stripos($fn, 'think\\db') === 0) { $sqlCt += $inclCt[$fn] ?? 0; $sqlWt += $excl; $sqlFnCount++; } } // caller-only 出现在 exclWt 但不在 inclWt 的函数(独占已兜底 0),不进入函数表 usort($rows, function ($a, $b) { return $b['excl_wt'] <=> $a['excl_wt']; }); $functions = array_slice($rows, 0, 100); $wallTime = (int) $run->wall_time ?: ($inclWt['main()'] ?? 0); $sqlPct = $wallTime > 0 ? round($sqlWt * 100 / $wallTime, 2) : 0.0; $summary = [ 'id' => (int) $run->id, 'method' => $run->method, 'url' => $run->url, 'simple_url' => $run->simple_url, 'node_id' => $run->node_id, 'wall_time' => $wallTime, 'cpu_time' => (int) $run->cpu_time, 'memory_peak' => (int) $run->memory_peak, 'request_time' => (float) $run->request_time, 'request_time_text' => date('Y-m-d H:i:s', (int) $run->request_time), 'function_total' => count($rows), ]; $payload = [ 'summary' => $summary, 'sql_summary' => [ 'fn_count' => $sqlFnCount, 'ct' => $sqlCt, 'wt' => $sqlWt, 'wt_pct' => $sqlPct, ], 'functions' => $functions, ]; $this->success('获取成功', $payload); } }