@uwa4d/openapi-mcp 0.2.0-beta.5 → 0.2.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composite/name-pattern.d.ts +8 -0
- package/dist/composite/name-pattern.js +25 -0
- package/dist/composite/report-diagnosis.js +2 -1
- package/dist/composite/stack-agg.d.ts +15 -3
- package/dist/composite/stack-agg.js +154 -2
- package/dist/composite/stack-test-mode.d.ts +33 -0
- package/dist/composite/stack-test-mode.js +60 -0
- package/dist/composite/top-functions.d.ts +7 -2
- package/dist/composite/top-functions.js +74 -29
- package/dist/indicator-dashboard-keys.d.ts +6 -0
- package/dist/indicator-dashboard-keys.js +30 -0
- package/dist/indicator-dashboard-keys.json +338 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +57 -2
- package/dist/version-guide.js +34 -0
- package/package.json +2 -2
- package/spec/uwa-openapi.json +489 -105
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 按函数名过滤(子串或 /regex/flags),供 top_functions 本地过滤 idmap 结果。 */
|
|
2
|
+
export declare function matchesNamePattern(name: string, pattern?: unknown): boolean;
|
|
3
|
+
export declare function filterRowsByNamePattern<T extends {
|
|
4
|
+
name: string;
|
|
5
|
+
}>(rows: T[], pattern: unknown): {
|
|
6
|
+
rows: T[];
|
|
7
|
+
matchedTotal: number;
|
|
8
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** 按函数名过滤(子串或 /regex/flags),供 top_functions 本地过滤 idmap 结果。 */
|
|
2
|
+
export function matchesNamePattern(name, pattern) {
|
|
3
|
+
if (pattern == null || (typeof pattern === 'string' && !pattern.trim()))
|
|
4
|
+
return true;
|
|
5
|
+
const p = String(pattern).trim();
|
|
6
|
+
if (p.length >= 2 && p.startsWith('/') && p.lastIndexOf('/') > 0) {
|
|
7
|
+
const last = p.lastIndexOf('/');
|
|
8
|
+
const body = p.slice(1, last);
|
|
9
|
+
const flags = p.slice(last + 1);
|
|
10
|
+
try {
|
|
11
|
+
return new RegExp(body, flags).test(name);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return name.toLowerCase().includes(p.toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return name.toLowerCase().includes(p.toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
export function filterRowsByNamePattern(rows, pattern) {
|
|
20
|
+
if (pattern == null || (typeof pattern === 'string' && !pattern.trim())) {
|
|
21
|
+
return { rows, matchedTotal: rows.length };
|
|
22
|
+
}
|
|
23
|
+
const matched = rows.filter((r) => matchesNamePattern(r.name, pattern));
|
|
24
|
+
return { rows: matched, matchedTotal: matched.length };
|
|
25
|
+
}
|
|
@@ -66,7 +66,8 @@ export const reportDiagnosisTool = {
|
|
|
66
66
|
'对单份 GOT Online 报告做一次结构化体检:自动选对 Overview 统计 v1/v2,并串联 Top 资源、Top 函数与场景摘要。',
|
|
67
67
|
'适用引擎:Unity / UE|复合工具',
|
|
68
68
|
'调用方不必自己判断 2026-07-09 / 2026-03-25 分界或 SDK 版本——内部按 get_report_detail 路由。',
|
|
69
|
-
'Top
|
|
69
|
+
'Top 函数:Overview 走 method/idmap/statistic v1.0.2 + stackTestMode;Mono 走堆栈树;逐帧曲线用 method/curve/presign v1.0.2。',
|
|
70
|
+
'查面板统计/曲线前先调 gotonline_overview_indicator_dashboard_keys 核对 indicatorDashboards(勿把 fps_mean 等统计字段当面板名)。',
|
|
70
71
|
'返回摘要而非原始大包;需要明细时再按 _apisUsed 中的原子工具深挖。',
|
|
71
72
|
].join('\n'),
|
|
72
73
|
inputSchema: {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { UwaClient } from '../client.js';
|
|
2
2
|
import type { ReportIdentity } from './route-overview.js';
|
|
3
|
+
import { type OverviewStackTestMode } from './stack-test-mode.js';
|
|
4
|
+
export type { OverviewStackTestMode, OverviewStackTestModeProfile } from './stack-test-mode.js';
|
|
3
5
|
export type StackMetric = 'selfTime' | 'totalTime' | 'callCount' | 'selfMemory';
|
|
4
6
|
export interface StackAggRow {
|
|
5
7
|
methodId: string;
|
|
@@ -18,15 +20,25 @@ export interface StackAggRow {
|
|
|
18
20
|
}
|
|
19
21
|
export interface StackAggResult {
|
|
20
22
|
testMode: string | null;
|
|
23
|
+
/** 仅 Overview:堆栈采集模式 CPU_ONLY / LUA_MEM_ONLY / CPU_AND_LUA_MEM */
|
|
24
|
+
stackTestMode?: OverviewStackTestMode | null;
|
|
25
|
+
availableMetrics?: StackMetric[];
|
|
26
|
+
availableFields?: string[];
|
|
27
|
+
metricGuide?: string;
|
|
21
28
|
metric: StackMetric;
|
|
22
29
|
unit: string;
|
|
23
30
|
rows: Array<{
|
|
24
31
|
name: string;
|
|
25
32
|
methodId: string;
|
|
33
|
+
stackMethodId?: string;
|
|
26
34
|
value: number;
|
|
27
35
|
unit: string;
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
selfTimeMean?: number | null;
|
|
37
|
+
totalTimeMean?: number | null;
|
|
38
|
+
callCountTotal?: number | null;
|
|
39
|
+
selfMemoryMean?: number | null;
|
|
40
|
+
selfMemoryTotal?: number | null;
|
|
41
|
+
selfMemoryUnit?: string | null;
|
|
30
42
|
selfMemoryKb?: number;
|
|
31
43
|
callCount: number;
|
|
32
44
|
callFrameCount: number;
|
|
@@ -48,6 +60,7 @@ interface TreeNode {
|
|
|
48
60
|
totalMemoryKb: number;
|
|
49
61
|
selfMemoryKb: number;
|
|
50
62
|
}
|
|
63
|
+
export declare function aggregateOverviewStackFromStatistic(client: UwaClient, identity: ReportIdentity, topN: number, metricInput?: StackMetric, threadName?: string, namePattern?: string): Promise<StackAggResult>;
|
|
51
64
|
/**
|
|
52
65
|
* Overview:线程堆栈树(主线程)+ 函数 idmap → 按函数名合并节点。
|
|
53
66
|
* 同名(同一 methodId)节点的 selfTime / callCount 相加,再按指标排序取 Top N。
|
|
@@ -62,4 +75,3 @@ export declare function aggregateMonoStack(client: UwaClient, identity: ReportId
|
|
|
62
75
|
* 解析无表头 CSV。列数随 testMode 变化;UE 固定 7 列(与 CPU_ONLY 同形)。
|
|
63
76
|
*/
|
|
64
77
|
export declare function parseStackCsv(csvText: string, testMode: string | null): TreeNode[];
|
|
65
|
-
export {};
|
|
@@ -1,4 +1,155 @@
|
|
|
1
1
|
import { isObj } from './helpers.js';
|
|
2
|
+
import { assertOverviewMetricSupported, inferOverviewStackTestMode, normalizeOverviewStackTestMode, OVERVIEW_STACK_TEST_MODE_PROFILES, resolveOverviewDefaultMetric, } from './stack-test-mode.js';
|
|
3
|
+
import { matchesNamePattern } from './name-pattern.js';
|
|
4
|
+
/**
|
|
5
|
+
* Overview:函数 idmap 统计(v1.0.2)+ Overview stackTestMode → Top N。
|
|
6
|
+
* stackTestMode 仅 Overview 有效;决定哪些统计字段/metric 可用。
|
|
7
|
+
*/
|
|
8
|
+
function parseStatNumber(obj) {
|
|
9
|
+
if (!isObj(obj) || obj['value'] == null)
|
|
10
|
+
return null;
|
|
11
|
+
const n = Number(obj['value']);
|
|
12
|
+
return Number.isFinite(n) ? n : null;
|
|
13
|
+
}
|
|
14
|
+
function parseStatUnit(obj) {
|
|
15
|
+
if (!isObj(obj))
|
|
16
|
+
return null;
|
|
17
|
+
const unit = obj['unit'];
|
|
18
|
+
if (!isObj(unit) || unit['value'] == null)
|
|
19
|
+
return null;
|
|
20
|
+
return String(unit['value']);
|
|
21
|
+
}
|
|
22
|
+
async function fetchOverviewStackTestMode(client, identity) {
|
|
23
|
+
if (identity.engine === 'unreal') {
|
|
24
|
+
return { stackTestMode: 'CPU_ONLY', apisUsed: [] };
|
|
25
|
+
}
|
|
26
|
+
const keyQuery = identity.dataKey
|
|
27
|
+
? { dataKey: identity.dataKey }
|
|
28
|
+
: { recordId: identity.recordId };
|
|
29
|
+
try {
|
|
30
|
+
const meta = (await client.call('GET', '/openapi/v1/data/gotonline/overview/stack/overall/tree/presign', 'v1.0.1', keyQuery));
|
|
31
|
+
return {
|
|
32
|
+
stackTestMode: normalizeOverviewStackTestMode(meta['testMode']),
|
|
33
|
+
apisUsed: ['gotonline_overview_stack_overall_tree_presign'],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { stackTestMode: null, apisUsed: [] };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function aggregateOverviewStackFromStatistic(client, identity, topN, metricInput, threadName, namePattern) {
|
|
41
|
+
const keyQuery = identity.dataKey
|
|
42
|
+
? { dataKey: identity.dataKey }
|
|
43
|
+
: { recordId: identity.recordId };
|
|
44
|
+
if (identity.engine === 'unreal') {
|
|
45
|
+
keyQuery['threadName'] = threadName?.trim() || 'GameThread';
|
|
46
|
+
}
|
|
47
|
+
const apisUsed = [];
|
|
48
|
+
const [{ stackTestMode: presignMode, apisUsed: presignApis }, raw] = await Promise.all([
|
|
49
|
+
fetchOverviewStackTestMode(client, identity),
|
|
50
|
+
client.call('GET', '/openapi/v1/data/gotonline/overview/method/idmap/statistic', 'v1.0.2', keyQuery),
|
|
51
|
+
]);
|
|
52
|
+
apisUsed.push(...presignApis, 'gotonline_overview_method_idmap_statistic');
|
|
53
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
54
|
+
if (!list.length) {
|
|
55
|
+
throw new Error('函数 idmap 统计返回空列表(报告可能未开堆栈或 SDK < 2.5.1)');
|
|
56
|
+
}
|
|
57
|
+
const rows = [];
|
|
58
|
+
for (const item of list) {
|
|
59
|
+
if (!isObj(item))
|
|
60
|
+
continue;
|
|
61
|
+
const methodId = item['stackMethodId'] != null ? String(item['stackMethodId']) : '';
|
|
62
|
+
const name = item['stackMethodName'] != null ? String(item['stackMethodName']) : methodId;
|
|
63
|
+
if (!methodId)
|
|
64
|
+
continue;
|
|
65
|
+
const memMeanObj = item['selfMemoryMean'];
|
|
66
|
+
const memTotalObj = item['selfMemoryTotal'];
|
|
67
|
+
const selfTimeMean = parseStatNumber(item['selfTimeMean']);
|
|
68
|
+
const totalTimeMean = parseStatNumber(item['totalTimeMean']);
|
|
69
|
+
const callCountTotal = parseStatNumber(item['callCountTotal']);
|
|
70
|
+
const selfMemoryMean = parseStatNumber(memMeanObj);
|
|
71
|
+
const selfMemoryTotal = parseStatNumber(memTotalObj);
|
|
72
|
+
const selfMemoryUnit = parseStatUnit(memMeanObj) ?? parseStatUnit(memTotalObj);
|
|
73
|
+
if (selfTimeMean == null &&
|
|
74
|
+
totalTimeMean == null &&
|
|
75
|
+
callCountTotal == null &&
|
|
76
|
+
selfMemoryMean == null &&
|
|
77
|
+
selfMemoryTotal == null) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
rows.push({
|
|
81
|
+
stackMethodId: methodId,
|
|
82
|
+
name,
|
|
83
|
+
selfTimeMean,
|
|
84
|
+
totalTimeMean,
|
|
85
|
+
callCountTotal,
|
|
86
|
+
selfMemoryMean,
|
|
87
|
+
selfMemoryTotal,
|
|
88
|
+
selfMemoryUnit,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (!rows.length) {
|
|
92
|
+
throw new Error('函数 idmap 统计无有效数据');
|
|
93
|
+
}
|
|
94
|
+
const hasTime = rows.some((r) => r.selfTimeMean != null || r.totalTimeMean != null);
|
|
95
|
+
const hasMem = rows.some((r) => r.selfMemoryMean != null || r.selfMemoryTotal != null);
|
|
96
|
+
const stackTestMode = presignMode ?? inferOverviewStackTestMode({ hasTime, hasMem });
|
|
97
|
+
if (!stackTestMode) {
|
|
98
|
+
throw new Error('无法判断 Overview stackTestMode');
|
|
99
|
+
}
|
|
100
|
+
const profile = OVERVIEW_STACK_TEST_MODE_PROFILES[stackTestMode];
|
|
101
|
+
const metric = metricInput ?? resolveOverviewDefaultMetric(stackTestMode);
|
|
102
|
+
assertOverviewMetricSupported(metric, profile);
|
|
103
|
+
const metricValue = (r) => {
|
|
104
|
+
if (metric === 'totalTime')
|
|
105
|
+
return r.totalTimeMean != null ? r.totalTimeMean : -1;
|
|
106
|
+
if (metric === 'callCount')
|
|
107
|
+
return r.callCountTotal != null ? r.callCountTotal : -1;
|
|
108
|
+
if (metric === 'selfMemory') {
|
|
109
|
+
if (r.selfMemoryMean != null)
|
|
110
|
+
return r.selfMemoryMean;
|
|
111
|
+
if (r.selfMemoryTotal != null)
|
|
112
|
+
return r.selfMemoryTotal;
|
|
113
|
+
return -1;
|
|
114
|
+
}
|
|
115
|
+
return r.selfTimeMean != null ? r.selfTimeMean : -1;
|
|
116
|
+
};
|
|
117
|
+
const sorted = rows.sort((a, b) => metricValue(b) - metricValue(a));
|
|
118
|
+
const pattern = namePattern?.trim();
|
|
119
|
+
const matched = pattern ? sorted.filter((r) => matchesNamePattern(r.name, pattern)) : sorted;
|
|
120
|
+
const unit = metric === 'callCount' ? '次' : metric === 'selfMemory' ? rows.find((r) => r.selfMemoryUnit)?.selfMemoryUnit ?? 'KB' : 'ms';
|
|
121
|
+
return {
|
|
122
|
+
testMode: stackTestMode,
|
|
123
|
+
stackTestMode,
|
|
124
|
+
availableMetrics: [...profile.availableMetrics],
|
|
125
|
+
availableFields: [...profile.availableFields],
|
|
126
|
+
metricGuide: profile.description,
|
|
127
|
+
metric,
|
|
128
|
+
unit,
|
|
129
|
+
rows: matched.slice(0, topN).map((r) => ({
|
|
130
|
+
name: r.name,
|
|
131
|
+
methodId: r.stackMethodId,
|
|
132
|
+
stackMethodId: r.stackMethodId,
|
|
133
|
+
selfTimeMean: r.selfTimeMean != null ? round2(r.selfTimeMean) : null,
|
|
134
|
+
totalTimeMean: r.totalTimeMean != null ? round2(r.totalTimeMean) : null,
|
|
135
|
+
callCountTotal: r.callCountTotal != null ? Math.round(r.callCountTotal) : null,
|
|
136
|
+
selfMemoryMean: r.selfMemoryMean != null ? round2(r.selfMemoryMean) : null,
|
|
137
|
+
selfMemoryTotal: r.selfMemoryTotal != null ? round2(r.selfMemoryTotal) : null,
|
|
138
|
+
selfMemoryUnit: r.selfMemoryUnit,
|
|
139
|
+
value: metric === 'callCount'
|
|
140
|
+
? Math.round(metricValue(r) >= 0 ? metricValue(r) : 0)
|
|
141
|
+
: round2(metricValue(r) >= 0 ? metricValue(r) : 0),
|
|
142
|
+
unit,
|
|
143
|
+
callCount: r.callCountTotal != null ? Math.round(r.callCountTotal) : 0,
|
|
144
|
+
callFrameCount: 0,
|
|
145
|
+
nodeCount: 1,
|
|
146
|
+
source: 'overview.method.idmap.statistic',
|
|
147
|
+
})),
|
|
148
|
+
totalNodes: rows.length,
|
|
149
|
+
uniqueFunctions: rows.length,
|
|
150
|
+
apisUsed,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
2
153
|
/**
|
|
3
154
|
* Overview:线程堆栈树(主线程)+ 函数 idmap → 按函数名合并节点。
|
|
4
155
|
* 同名(同一 methodId)节点的 selfTime / callCount 相加,再按指标排序取 Top N。
|
|
@@ -231,10 +382,11 @@ function finalizeAgg(nodes, idMap, topN, metric, testMode, apisUsed, source) {
|
|
|
231
382
|
return {
|
|
232
383
|
name: r.name,
|
|
233
384
|
methodId: r.methodId,
|
|
385
|
+
stackMethodId: r.methodId,
|
|
386
|
+
selfTimeMean: round2(r.selfTimeUs / 1000),
|
|
387
|
+
totalTimeMean: round2(r.totalTimeUs / 1000),
|
|
234
388
|
value,
|
|
235
389
|
unit,
|
|
236
|
-
selfTimeMs: round2(r.selfTimeUs / 1000),
|
|
237
|
-
totalTimeMs: round2(r.totalTimeUs / 1000),
|
|
238
390
|
selfMemoryKb: round2(r.selfMemoryKb),
|
|
239
391
|
callCount: r.callCount,
|
|
240
392
|
callFrameCount: r.callFrameCount,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StackMetric } from './stack-agg.js';
|
|
2
|
+
/**
|
|
3
|
+
* Overview 堆栈采集模式(来自 overview/stack/overall/tree/presign 的 testMode)。
|
|
4
|
+
* 仅适用于 Overview 报告;Mono 报告走独立堆栈,不使用本枚举。
|
|
5
|
+
*/
|
|
6
|
+
export type OverviewStackTestMode = 'CPU_ONLY' | 'LUA_MEM_ONLY' | 'CPU_AND_LUA_MEM';
|
|
7
|
+
/** @deprecated 使用 OverviewStackTestMode */
|
|
8
|
+
export type StackTestMode = OverviewStackTestMode;
|
|
9
|
+
export interface OverviewStackTestModeProfile {
|
|
10
|
+
stackTestMode: OverviewStackTestMode;
|
|
11
|
+
availableMetrics: StackMetric[];
|
|
12
|
+
availableFields: readonly string[];
|
|
13
|
+
description: string;
|
|
14
|
+
}
|
|
15
|
+
/** @deprecated 使用 OverviewStackTestModeProfile */
|
|
16
|
+
export type StackTestModeProfile = OverviewStackTestModeProfile;
|
|
17
|
+
export declare const OVERVIEW_STACK_TEST_MODE_PROFILES: Record<OverviewStackTestMode, OverviewStackTestModeProfile>;
|
|
18
|
+
export declare const STACK_TEST_MODE_PROFILES: Record<OverviewStackTestMode, OverviewStackTestModeProfile>;
|
|
19
|
+
export declare function normalizeOverviewStackTestMode(raw: unknown): OverviewStackTestMode | null;
|
|
20
|
+
/** @deprecated 使用 normalizeOverviewStackTestMode */
|
|
21
|
+
export declare const normalizeStackTestMode: typeof normalizeOverviewStackTestMode;
|
|
22
|
+
export declare function inferOverviewStackTestMode(flags: {
|
|
23
|
+
hasTime: boolean;
|
|
24
|
+
hasMem: boolean;
|
|
25
|
+
}): OverviewStackTestMode | null;
|
|
26
|
+
/** @deprecated 使用 inferOverviewStackTestMode */
|
|
27
|
+
export declare const inferStackTestMode: typeof inferOverviewStackTestMode;
|
|
28
|
+
export declare function resolveOverviewDefaultMetric(stackTestMode: OverviewStackTestMode): StackMetric;
|
|
29
|
+
/** @deprecated 使用 resolveOverviewDefaultMetric */
|
|
30
|
+
export declare const resolveDefaultMetric: typeof resolveOverviewDefaultMetric;
|
|
31
|
+
export declare function assertOverviewMetricSupported(metric: StackMetric, profile: OverviewStackTestModeProfile): void;
|
|
32
|
+
/** @deprecated 使用 assertOverviewMetricSupported */
|
|
33
|
+
export declare const assertMetricSupported: typeof assertOverviewMetricSupported;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const OVERVIEW_STACK_TEST_MODE_PROFILES = {
|
|
2
|
+
CPU_ONLY: {
|
|
3
|
+
stackTestMode: 'CPU_ONLY',
|
|
4
|
+
availableMetrics: ['selfTime', 'totalTime', 'callCount'],
|
|
5
|
+
availableFields: ['selfTimeMean', 'totalTimeMean', 'callCountTotal'],
|
|
6
|
+
description: 'CPU_ONLY:仅有耗时(selfTimeMean/totalTimeMean,ms)与调用次数(callCountTotal);内存字段恒为 null。',
|
|
7
|
+
},
|
|
8
|
+
LUA_MEM_ONLY: {
|
|
9
|
+
stackTestMode: 'LUA_MEM_ONLY',
|
|
10
|
+
availableMetrics: ['callCount', 'selfMemory'],
|
|
11
|
+
availableFields: ['callCountTotal', 'selfMemoryMean', 'selfMemoryTotal'],
|
|
12
|
+
description: 'LUA_MEM_ONLY:仅有调用次数(callCountTotal)与内存(selfMemoryMean/selfMemoryTotal);耗时字段恒为 null。',
|
|
13
|
+
},
|
|
14
|
+
CPU_AND_LUA_MEM: {
|
|
15
|
+
stackTestMode: 'CPU_AND_LUA_MEM',
|
|
16
|
+
availableMetrics: ['selfTime', 'totalTime', 'callCount', 'selfMemory'],
|
|
17
|
+
availableFields: [
|
|
18
|
+
'selfTimeMean',
|
|
19
|
+
'totalTimeMean',
|
|
20
|
+
'callCountTotal',
|
|
21
|
+
'selfMemoryMean',
|
|
22
|
+
'selfMemoryTotal',
|
|
23
|
+
],
|
|
24
|
+
description: 'CPU_AND_LUA_MEM:耗时、调用次数、内存均可用。',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
export const STACK_TEST_MODE_PROFILES = OVERVIEW_STACK_TEST_MODE_PROFILES;
|
|
28
|
+
export function normalizeOverviewStackTestMode(raw) {
|
|
29
|
+
const tm = String(raw ?? '').toUpperCase();
|
|
30
|
+
if (tm === 'CPU_ONLY' || tm === 'LUA_MEM_ONLY' || tm === 'CPU_AND_LUA_MEM') {
|
|
31
|
+
return tm;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** @deprecated 使用 normalizeOverviewStackTestMode */
|
|
36
|
+
export const normalizeStackTestMode = normalizeOverviewStackTestMode;
|
|
37
|
+
export function inferOverviewStackTestMode(flags) {
|
|
38
|
+
if (flags.hasTime && flags.hasMem)
|
|
39
|
+
return 'CPU_AND_LUA_MEM';
|
|
40
|
+
if (flags.hasMem && !flags.hasTime)
|
|
41
|
+
return 'LUA_MEM_ONLY';
|
|
42
|
+
if (flags.hasTime)
|
|
43
|
+
return 'CPU_ONLY';
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** @deprecated 使用 inferOverviewStackTestMode */
|
|
47
|
+
export const inferStackTestMode = inferOverviewStackTestMode;
|
|
48
|
+
export function resolveOverviewDefaultMetric(stackTestMode) {
|
|
49
|
+
return stackTestMode === 'LUA_MEM_ONLY' ? 'selfMemory' : 'selfTime';
|
|
50
|
+
}
|
|
51
|
+
/** @deprecated 使用 resolveOverviewDefaultMetric */
|
|
52
|
+
export const resolveDefaultMetric = resolveOverviewDefaultMetric;
|
|
53
|
+
export function assertOverviewMetricSupported(metric, profile) {
|
|
54
|
+
if (!profile.availableMetrics.includes(metric)) {
|
|
55
|
+
throw new Error(`当前 Overview stackTestMode=${profile.stackTestMode} 不支持 metric=${metric}。` +
|
|
56
|
+
`可用:${profile.availableMetrics.join('、')}。${profile.description}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** @deprecated 使用 assertOverviewMetricSupported */
|
|
60
|
+
export const assertMetricSupported = assertOverviewMetricSupported;
|
|
@@ -7,8 +7,13 @@ export interface FunctionRow {
|
|
|
7
7
|
source: string;
|
|
8
8
|
label?: string;
|
|
9
9
|
methodId?: string;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
stackMethodId?: string;
|
|
11
|
+
selfTimeMean?: number | null;
|
|
12
|
+
totalTimeMean?: number | null;
|
|
13
|
+
callCountTotal?: number | null;
|
|
14
|
+
selfMemoryMean?: number | null;
|
|
15
|
+
selfMemoryTotal?: number | null;
|
|
16
|
+
selfMemoryUnit?: string | null;
|
|
12
17
|
callCount?: number;
|
|
13
18
|
nodeCount?: number;
|
|
14
19
|
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { z } from './types.js';
|
|
2
2
|
import { errorResult, jsonToolResult, requireReportKey, topNOf } from './helpers.js';
|
|
3
3
|
import { fetchReportIdentity } from './route-overview.js';
|
|
4
|
-
import { aggregateMonoStack,
|
|
5
|
-
|
|
4
|
+
import { aggregateMonoStack, aggregateOverviewStackFromStatistic } from './stack-agg.js';
|
|
5
|
+
import { filterRowsByNamePattern } from './name-pattern.js';
|
|
6
|
+
function resolveOverviewMetricInput(raw) {
|
|
6
7
|
if (raw === 'totalTime' || raw === 'callCount' || raw === 'selfMemory' || raw === 'selfTime') {
|
|
7
8
|
return raw;
|
|
8
9
|
}
|
|
9
|
-
return
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
function resolveMonoMetric(raw) {
|
|
13
|
+
if (raw === 'totalTime' || raw === 'callCount' || raw === 'selfMemory' || raw === 'selfTime') {
|
|
14
|
+
return raw;
|
|
15
|
+
}
|
|
16
|
+
return 'selfMemory';
|
|
10
17
|
}
|
|
11
18
|
export async function runTopFunctions(client, args) {
|
|
12
19
|
const key = requireReportKey(args);
|
|
@@ -17,33 +24,42 @@ export async function runTopFunctions(client, args) {
|
|
|
17
24
|
const identity = await fetchReportIdentity(client, key);
|
|
18
25
|
const apisUsed = ['get_report_detail'];
|
|
19
26
|
const mode = subtype ?? identity.serviceSubtype ?? 'overview';
|
|
20
|
-
const metric = resolveMetric(args['metric'], mode);
|
|
21
27
|
if (mode === 'mono') {
|
|
28
|
+
const metric = resolveMonoMetric(args['metric']);
|
|
22
29
|
if (identity.engine !== 'unity') {
|
|
23
30
|
return {
|
|
24
31
|
engine: identity.engine,
|
|
25
32
|
dataKey: identity.dataKey,
|
|
33
|
+
serviceSubtype: 'mono',
|
|
26
34
|
items: [],
|
|
27
35
|
_apisUsed: apisUsed,
|
|
28
|
-
_note: 'Mono 模式仅支持 Unity
|
|
36
|
+
_note: 'Mono 模式仅支持 Unity 报告(与 Overview stackTestMode 无关)。',
|
|
29
37
|
_limitations: ['engine!=unity'],
|
|
30
38
|
};
|
|
31
39
|
}
|
|
32
40
|
try {
|
|
33
|
-
const
|
|
41
|
+
const namePattern = typeof args['namePattern'] === 'string' && args['namePattern'].trim()
|
|
42
|
+
? String(args['namePattern']).trim()
|
|
43
|
+
: undefined;
|
|
44
|
+
const fetchN = namePattern ? Math.max(topN, 200) : topN;
|
|
45
|
+
const agg = await aggregateMonoStack(client, identity, fetchN, metric);
|
|
46
|
+
const filtered = filterRowsByNamePattern(agg.rows, namePattern);
|
|
47
|
+
const items = filtered.rows.slice(0, topN);
|
|
34
48
|
return {
|
|
35
49
|
engine: identity.engine,
|
|
36
50
|
dataKey: identity.dataKey,
|
|
37
51
|
serviceSubtype: 'mono',
|
|
38
52
|
topN,
|
|
39
53
|
metric: agg.metric,
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
namePattern: namePattern ?? null,
|
|
55
|
+
matchedFunctions: namePattern ? filtered.matchedTotal : agg.uniqueFunctions,
|
|
56
|
+
items,
|
|
42
57
|
totalNodes: agg.totalNodes,
|
|
43
58
|
uniqueFunctions: agg.uniqueFunctions,
|
|
44
59
|
_apisUsed: [...apisUsed, ...agg.apisUsed],
|
|
45
|
-
_note:
|
|
46
|
-
|
|
60
|
+
_note: `Mono 模式:已下载正向堆栈树合并取 Top ${items.length}(共 ${agg.totalNodes} 节点)。` +
|
|
61
|
+
`Mono 不使用 Overview stackTestMode;默认按 selfMemory 排序。` +
|
|
62
|
+
(namePattern ? ` namePattern「${String(namePattern)}」命中 ${filtered.matchedTotal} 个。` : ''),
|
|
47
63
|
};
|
|
48
64
|
}
|
|
49
65
|
catch (e) {
|
|
@@ -58,25 +74,44 @@ export async function runTopFunctions(client, args) {
|
|
|
58
74
|
};
|
|
59
75
|
}
|
|
60
76
|
}
|
|
61
|
-
|
|
77
|
+
const threadName = typeof args['threadName'] === 'string' && args['threadName'].trim()
|
|
78
|
+
? String(args['threadName']).trim()
|
|
79
|
+
: undefined;
|
|
80
|
+
const metricInput = resolveOverviewMetricInput(args['metric']);
|
|
81
|
+
const namePattern = typeof args['namePattern'] === 'string' && args['namePattern'].trim()
|
|
82
|
+
? String(args['namePattern']).trim()
|
|
83
|
+
: undefined;
|
|
62
84
|
try {
|
|
63
|
-
const agg = await
|
|
85
|
+
const agg = await aggregateOverviewStackFromStatistic(client, identity, topN, metricInput, threadName, namePattern);
|
|
86
|
+
const nameFilterNote = namePattern
|
|
87
|
+
? agg.rows.length === 0
|
|
88
|
+
? ` namePattern「${namePattern}」无匹配函数。`
|
|
89
|
+
: ` namePattern「${namePattern}」已过滤后取 Top ${agg.rows.length}。`
|
|
90
|
+
: '';
|
|
64
91
|
return {
|
|
65
92
|
engine: identity.engine,
|
|
66
93
|
dataKey: identity.dataKey,
|
|
67
94
|
createDate: identity.createDate,
|
|
68
95
|
sdkVersion: identity.sdkVersion,
|
|
69
|
-
serviceSubtype:
|
|
96
|
+
serviceSubtype: 'overview',
|
|
97
|
+
stackTestMode: agg.stackTestMode,
|
|
98
|
+
availableMetrics: agg.availableMetrics,
|
|
99
|
+
availableFields: agg.availableFields,
|
|
70
100
|
topN,
|
|
71
101
|
metric: agg.metric,
|
|
72
|
-
|
|
102
|
+
namePattern: namePattern ?? null,
|
|
73
103
|
items: agg.rows,
|
|
74
104
|
totalNodes: agg.totalNodes,
|
|
75
105
|
uniqueFunctions: agg.uniqueFunctions,
|
|
76
106
|
_apisUsed: [...apisUsed, ...agg.apisUsed],
|
|
77
|
-
_note:
|
|
78
|
-
|
|
79
|
-
|
|
107
|
+
_note: `Overview 模式 stackTestMode=${agg.stackTestMode}。${agg.metricGuide ?? ''} ` +
|
|
108
|
+
`本次按 ${agg.metric} 排序,Top ${agg.rows.length} / 共 ${agg.totalNodes} 函数。` +
|
|
109
|
+
`数据来自 method/idmap/statistic v1.0.2(非堆栈树解析)。` +
|
|
110
|
+
`逐帧曲线:method/curve/presign v1.0.2 + stackMethodId。` +
|
|
111
|
+
(identity.engine === 'unreal'
|
|
112
|
+
? ` UE 默认 threadName=GameThread${threadName ? `(本次 ${threadName})` : ''};UE 固定 CPU_ONLY。`
|
|
113
|
+
: '') +
|
|
114
|
+
nameFilterNote,
|
|
80
115
|
};
|
|
81
116
|
}
|
|
82
117
|
catch (e) {
|
|
@@ -85,12 +120,12 @@ export async function runTopFunctions(client, args) {
|
|
|
85
120
|
dataKey: identity.dataKey,
|
|
86
121
|
createDate: identity.createDate,
|
|
87
122
|
sdkVersion: identity.sdkVersion,
|
|
88
|
-
serviceSubtype:
|
|
123
|
+
serviceSubtype: 'overview',
|
|
89
124
|
items: [],
|
|
90
125
|
_apisUsed: apisUsed,
|
|
91
|
-
_note:
|
|
92
|
-
|
|
93
|
-
_limitations: ['
|
|
126
|
+
_note: `Overview 函数 Top 失败:${e instanceof Error ? e.message : String(e)}。` +
|
|
127
|
+
`通常需 SDK ≥ 2.5.1 且报告开启堆栈。`,
|
|
128
|
+
_limitations: ['overview_idmap_statistic_unavailable'],
|
|
94
129
|
};
|
|
95
130
|
}
|
|
96
131
|
}
|
|
@@ -100,26 +135,36 @@ export const topFunctionsTool = {
|
|
|
100
135
|
engines: ['unity', 'unreal'],
|
|
101
136
|
filterKeys: ['top_functions', 'composite', 'preset.default', 'overview', 'mono'],
|
|
102
137
|
description: [
|
|
103
|
-
'
|
|
138
|
+
'从函数统计聚合指定报告的函数 Top N。',
|
|
104
139
|
'适用引擎:Unity / UE|复合工具',
|
|
105
|
-
'Overview
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'
|
|
140
|
+
'Overview:method/idmap/statistic v1.0.2;stackTestMode 决定可用字段(仅 Overview):',
|
|
141
|
+
' CPU_ONLY → selfTimeMean/totalTimeMean/callCountTotal;',
|
|
142
|
+
' LUA_MEM_ONLY → callCountTotal/selfMemoryMean/selfMemoryTotal;',
|
|
143
|
+
' CPU_AND_LUA_MEM → 全部;UE 固定 CPU_ONLY。',
|
|
144
|
+
'Mono(仅 Unity):独立堆栈树,不使用 stackTestMode,默认 selfMemory。',
|
|
145
|
+
'逐帧曲线:method/curve/presign v1.0.2 + stackMethodId。',
|
|
146
|
+
'namePattern:可选,按函数名子串或 /正则/ 过滤(Overview 在 idmap 结果上过滤;Mono 在合并后的函数列表上过滤)。',
|
|
110
147
|
].join('\n'),
|
|
111
148
|
inputSchema: {
|
|
112
149
|
dataKey: z.string().optional().describe('报告 dataKey,与 recordId 二选一'),
|
|
113
150
|
recordId: z.union([z.string(), z.number()]).optional().describe('报告 recordId,与 dataKey 二选一'),
|
|
114
151
|
topN: z.number().optional().describe('返回条数,默认 20,最大 200'),
|
|
152
|
+
namePattern: z
|
|
153
|
+
.string()
|
|
154
|
+
.optional()
|
|
155
|
+
.describe('函数名过滤:默认子串匹配(忽略大小写);正则写 /pattern/i,如 /Canvas\\./ 或 Update'),
|
|
115
156
|
metric: z
|
|
116
157
|
.enum(['selfTime', 'totalTime', 'callCount', 'selfMemory'])
|
|
117
158
|
.optional()
|
|
118
|
-
.describe('
|
|
159
|
+
.describe('排序指标。Overview 须与 stackTestMode 匹配(CPU_ONLY 无 selfMemory;LUA_MEM_ONLY 无 selfTime/totalTime);未传时 Overview 按模式默认 selfTime 或 selfMemory'),
|
|
160
|
+
threadName: z
|
|
161
|
+
.string()
|
|
162
|
+
.optional()
|
|
163
|
+
.describe('UE Overview 线程名,默认 GameThread;Unity 忽略'),
|
|
119
164
|
serviceSubtype: z
|
|
120
165
|
.enum(['overview', 'mono'])
|
|
121
166
|
.optional()
|
|
122
|
-
.describe('
|
|
167
|
+
.describe('overview(默认)或 mono;stackTestMode 仅 overview 有效'),
|
|
123
168
|
},
|
|
124
169
|
async handler(args, ctx) {
|
|
125
170
|
try {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** indicatorDashboards 合法面板传参名称(与 got-query OverviewIndicatorDashboardCatalogService 同源)。 */
|
|
2
|
+
export declare const INDICATOR_DASHBOARD_KEYS: readonly string[];
|
|
3
|
+
/** 常见误传:统计维度后缀 / 子指标名,不是面板标识符。 */
|
|
4
|
+
export declare const INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES: readonly ["fps_mean", "fps_maximum", "frametime_min", "frametime_gt_40_pct", "temperature_mean", "drawcall_maximum"];
|
|
5
|
+
export declare function splitIndicatorDashboards(raw: unknown): string[];
|
|
6
|
+
export declare function findUnknownIndicatorDashboards(raw: unknown): string[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const keysJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'indicator-dashboard-keys.json'), 'utf8'));
|
|
5
|
+
/** indicatorDashboards 合法面板传参名称(与 got-query OverviewIndicatorDashboardCatalogService 同源)。 */
|
|
6
|
+
export const INDICATOR_DASHBOARD_KEYS = keysJson.keys;
|
|
7
|
+
/** 常见误传:统计维度后缀 / 子指标名,不是面板标识符。 */
|
|
8
|
+
export const INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES = [
|
|
9
|
+
'fps_mean',
|
|
10
|
+
'fps_maximum',
|
|
11
|
+
'frametime_min',
|
|
12
|
+
'frametime_gt_40_pct',
|
|
13
|
+
'temperature_mean',
|
|
14
|
+
'drawcall_maximum',
|
|
15
|
+
];
|
|
16
|
+
export function splitIndicatorDashboards(raw) {
|
|
17
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
18
|
+
return [];
|
|
19
|
+
return raw
|
|
20
|
+
.split(',')
|
|
21
|
+
.map((s) => s.trim())
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
export function findUnknownIndicatorDashboards(raw) {
|
|
25
|
+
const parts = splitIndicatorDashboards(raw);
|
|
26
|
+
if (!parts.length)
|
|
27
|
+
return [];
|
|
28
|
+
const valid = new Set(INDICATOR_DASHBOARD_KEYS);
|
|
29
|
+
return parts.filter((k) => !valid.has(k));
|
|
30
|
+
}
|