mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-31 05:05:33 +08:00
- XhprofProfileParserService(Base/App):纯数组解析 jsonl 行, meta 字段以真实 fixture 为准;simple_url 缺失兜底与采集端回调同算法; watch 正则按 callee 物化(ct/wt_sum/wt_max),非法正则编译计 fail 跳过 - XhprofLogReaderService(Base/App):增量读取,半行缓冲超限整行丢弃 (不照抄 nginx 8MB 强制切行);EOF 无换行半行不 yield 不推进 offset; 进度不自动落盘,由导入侧在同一事务内显式 savePosition - XhprofLogImport 定时任务(Base/App):不受 XHPROF_ENABLE 门控, runs-*.jsonl 按文件名序增量导入;run 逐条 insert + detail chunk 20 insertAll + run_watch insertAll 与 position 同事务落盘;导至 EOF 后 只删已导完文件 - 修复 T1 缺陷:request_time decimal(12,3) 装不下 10 位时间戳, Scheme 与 migration 同步改 decimal(13,3) - 新增 parser 单元测试(真实 fixture 断言,9 用例 42 断言) 与 jsonl fixture;phpunit 注册 unit 测试套件
183 lines
7.0 KiB
PHP
183 lines
7.0 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace tests\Unit;
|
||
|
||
use app\common\service\XhprofProfileParserService;
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
/**
|
||
* XHProf jsonl 行解析器单元测试(纯数组处理,不触 DB).
|
||
*
|
||
* fixture:tests/fixtures/xhprof-sample.jsonl 为容器内真实采样首行
|
||
* (103441 字节,md5 与容器内原行一致),断言值来自该 fixture 实测。
|
||
*/
|
||
class XhprofProfileParserTest extends TestCase
|
||
{
|
||
private XhprofProfileParserService $parser;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
$this->parser = new XhprofProfileParserService();
|
||
}
|
||
|
||
private function loadFixtureLine(): string
|
||
{
|
||
$content = file_get_contents(__DIR__ . '/../fixtures/xhprof-sample.jsonl');
|
||
self::assertNotFalse($content, 'fixture 文件缺失');
|
||
|
||
return trim($content);
|
||
}
|
||
|
||
/**
|
||
* 真实 fixture 行:解析出的字段值与 fixture 内实际数值一致.
|
||
*/
|
||
public function test_parse_real_fixture_line(): void
|
||
{
|
||
$parsed = $this->parser->parse($this->loadFixtureLine());
|
||
self::assertNotNull($parsed);
|
||
|
||
// 断言值来自 analyze 实测(见 .omo/evidence/task-3-xhprof-log-backend.txt)
|
||
self::assertSame('/', $parsed['url']);
|
||
self::assertSame('', $parsed['simple_url']); // 根路径 simple_url 原生即为空串
|
||
self::assertSame('GET', $parsed['method']);
|
||
self::assertSame(8402968, $parsed['wall_time']);
|
||
self::assertSame(340812, $parsed['cpu_time']);
|
||
self::assertSame(1981664, $parsed['memory_peak']);
|
||
// request_ts_micro = {"sec":1786744108,"usec":846845}
|
||
self::assertEqualsWithDelta(1786744108.847, $parsed['request_time'], 0.0005);
|
||
self::assertFalse($parsed['degraded']);
|
||
self::assertCount(954, $parsed['profile']);
|
||
}
|
||
|
||
public function test_parse_rejects_bad_lines(): void
|
||
{
|
||
self::assertNull($this->parser->parse(''));
|
||
self::assertNull($this->parser->parse('not json at all'));
|
||
self::assertNull($this->parser->parse('{truncated'));
|
||
self::assertNull($this->parser->parse('{"meta":{"url":"/"}}')); // 缺 profile 键
|
||
self::assertNull($this->parser->parse('["array","not","object"]'));
|
||
}
|
||
|
||
/**
|
||
* simple_url 缺失时兜底:path 去 query、纯数字段替换 :id(与 bootstrap 回调算法一致).
|
||
*/
|
||
public function test_simple_url_fallback(): void
|
||
{
|
||
$line = json_encode([
|
||
'profile' => ['main()' => ['ct' => 1, 'wt' => 10, 'cpu' => 1, 'mu' => 1, 'pmu' => 2]],
|
||
'meta' => ['url' => '/api/goods/45/detail?foo=bar&baz=1'],
|
||
], JSON_UNESCAPED_SLASHES);
|
||
$parsed = $this->parser->parse($line);
|
||
self::assertNotNull($parsed);
|
||
self::assertSame('api/goods/:id/detail', $parsed['simple_url']);
|
||
self::assertSame(10, $parsed['wall_time']);
|
||
self::assertSame(2, $parsed['memory_peak']);
|
||
self::assertSame('', $parsed['method']);
|
||
self::assertSame(0.0, $parsed['request_time']);
|
||
}
|
||
|
||
/**
|
||
* url 超 500 截断.
|
||
*/
|
||
public function test_url_truncated_to_500(): void
|
||
{
|
||
$line = json_encode([
|
||
'profile' => ['main()' => ['ct' => 1, 'wt' => 1, 'cpu' => 0, 'mu' => 0, 'pmu' => 0]],
|
||
'meta' => ['url' => '/' . str_repeat('a', 600), 'simple_url' => 'x'],
|
||
]);
|
||
$parsed = $this->parser->parse($line);
|
||
self::assertNotNull($parsed);
|
||
self::assertSame(500, mb_strlen($parsed['url']));
|
||
}
|
||
|
||
/**
|
||
* main() 缺失:降级 0 且 degraded=true(行仍可入库,由导入侧计 fail).
|
||
*/
|
||
public function test_parse_missing_main_degraded(): void
|
||
{
|
||
$line = json_encode([
|
||
'profile' => ['foo==>bar' => ['ct' => 1, 'wt' => 5]],
|
||
'meta' => ['url' => '/x'],
|
||
]);
|
||
$parsed = $this->parser->parse($line);
|
||
self::assertNotNull($parsed);
|
||
self::assertTrue($parsed['degraded']);
|
||
self::assertSame(0, $parsed['wall_time']);
|
||
self::assertSame(0, $parsed['cpu_time']);
|
||
self::assertSame(0, $parsed['memory_peak']);
|
||
}
|
||
|
||
/**
|
||
* watch 物化:按 callee 匹配,caller 匹配不命中;ct 累加、wt_sum 累加、wt_max 取最大.
|
||
*/
|
||
public function test_materialize_watches_by_callee(): void
|
||
{
|
||
$profile = [
|
||
'main()' => ['ct' => 1, 'wt' => 1000, 'cpu' => 10, 'mu' => 0, 'pmu' => 0],
|
||
'main()==>think\\db\\PDOConnection::query' => ['ct' => 2, 'wt' => 500],
|
||
'foo==>think\\db\\PDOConnection::query' => ['ct' => 3, 'wt' => 700],
|
||
'think\\db\\PDOConnection::query==>bar' => ['ct' => 9, 'wt' => 9999], // callee=bar 不命中
|
||
'plain_function' => ['ct' => 4, 'wt' => 4], // 裸键不参与
|
||
];
|
||
$patterns = [7 => '~^think\\\\db\\\\PDOConnection::~'];
|
||
|
||
$result = $this->parser->materializeWatches($profile, $patterns);
|
||
|
||
self::assertArrayHasKey(7, $result);
|
||
self::assertSame(5, $result[7]['ct']);
|
||
self::assertSame(1200, $result[7]['wt_sum']);
|
||
self::assertSame(700, $result[7]['wt_max']);
|
||
}
|
||
|
||
/**
|
||
* 真实 fixture 的 watch 物化:PDOConnection 规则必然命中(fixture 实测 33 条边).
|
||
*/
|
||
public function test_materialize_watches_on_real_fixture(): void
|
||
{
|
||
$parsed = $this->parser->parse($this->loadFixtureLine());
|
||
self::assertNotNull($parsed);
|
||
|
||
$result = $this->parser->materializeWatches($parsed['profile'], [99 => '~PDOConnection::~']);
|
||
self::assertArrayHasKey(99, $result);
|
||
self::assertGreaterThan(0, $result[99]['ct']);
|
||
self::assertGreaterThan(0, $result[99]['wt_sum']);
|
||
}
|
||
|
||
/**
|
||
* 正则编译:非法正则跳过该条并计 fail,合法条目进 patterns.
|
||
*/
|
||
public function test_compile_watch_regexes(): void
|
||
{
|
||
$watches = [
|
||
['id' => 1, 'regex' => '~^think\\\\db\\\\PDOConnection::.*$~'],
|
||
['id' => 2, 'regex' => '/[unclosed'], // 非法正则
|
||
['id' => 3, 'regex' => ''], // 空正则
|
||
['id' => 0, 'regex' => '~ok~'], // 非法 id
|
||
];
|
||
$compiled = $this->parser->compileWatchRegexes($watches);
|
||
|
||
self::assertSame(3, $compiled['fails']);
|
||
self::assertSame([1 => '~^think\\\\db\\\\PDOConnection::.*$~'], $compiled['patterns']);
|
||
}
|
||
|
||
/**
|
||
* request_time 退化链:request_ts_micro 优先,缺失退化 REQUEST_TIME_FLOAT.
|
||
*/
|
||
public function test_request_time_fallback_chain(): void
|
||
{
|
||
$base = ['profile' => ['main()' => ['ct' => 1, 'wt' => 1]]];
|
||
|
||
$withTsMicro = $this->parser->parse(json_encode($base + [
|
||
'meta' => ['request_ts_micro' => ['sec' => 1000000000, 'usec' => 250000]],
|
||
]));
|
||
self::assertSame(1000000000.25, $withTsMicro['request_time']);
|
||
|
||
$withFloatOnly = $this->parser->parse(json_encode($base + [
|
||
'meta' => ['SERVER' => ['REQUEST_TIME_FLOAT' => 1234567890.123456]],
|
||
]));
|
||
self::assertSame(1234567890.123, $withFloatOnly['request_time']);
|
||
}
|
||
}
|