Files
2026-08-15 07:20:29 +08:00

239 lines
8.8 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\admin\controller\xhprof;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnotation;
use think\App;
/**
* XHProf 性能采样记录(只读列表)
* 数据由定时任务从 jsonl 导入,后台仅查看与函数级分析,不支持手动增删改。
*
* @ControllerAnnotation(title="XHProf 性能采样记录")
*/
class Run extends AdminController
{
use \app\admin\traits\Curd;
protected $sort = [
'request_time' => '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);
}
}