Files
ulthon_admin/app/admin/controller/xhprof/Run.php
2026-08-15 07:20:29 +08:00

239 lines
8.8 KiB
PHP
Raw 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 判别):
* 返回函数级聚合分析 JSONXHProf 边模型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/mucallee 侧累计);
* 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 - Σ 出边 wtcaller 只出现无入边记录时兜底 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);
}
}