@uwa4d/openapi-mcp 0.2.0-beta.1 → 0.2.0-beta.3
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/README.md +27 -2
- package/dist/cli.js +18 -6
- package/dist/composite/helpers.d.ts +19 -0
- package/dist/composite/helpers.js +90 -0
- package/dist/composite/index.d.ts +25 -0
- package/dist/composite/index.js +46 -0
- package/dist/composite/overview-view.d.ts +33 -0
- package/dist/composite/overview-view.js +373 -0
- package/dist/composite/report-diagnosis.d.ts +2 -0
- package/dist/composite/report-diagnosis.js +174 -0
- package/dist/composite/route-overview.d.ts +72 -0
- package/dist/composite/route-overview.js +233 -0
- package/dist/composite/stack-agg.d.ts +65 -0
- package/dist/composite/stack-agg.js +257 -0
- package/dist/composite/top-functions.d.ts +16 -0
- package/dist/composite/top-functions.js +133 -0
- package/dist/composite/top-resources.d.ts +12 -0
- package/dist/composite/top-resources.js +263 -0
- package/dist/composite/types.d.ts +25 -0
- package/dist/composite/types.js +2 -0
- package/dist/presets.js +4 -0
- package/dist/server.d.ts +6 -1
- package/dist/server.js +29 -4
- package/dist/tools.js +5 -0
- package/dist/version-guide.d.ts +9 -0
- package/dist/version-guide.js +90 -0
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { isObj } from './helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Overview:线程堆栈树(主线程)+ 函数 idmap → 按函数名合并节点。
|
|
4
|
+
* 同名(同一 methodId)节点的 selfTime / callCount 相加,再按指标排序取 Top N。
|
|
5
|
+
*/
|
|
6
|
+
export async function aggregateOverviewStack(client, identity, topN, metric = 'selfTime') {
|
|
7
|
+
const keyQuery = identity.dataKey
|
|
8
|
+
? { dataKey: identity.dataKey }
|
|
9
|
+
: { recordId: identity.recordId };
|
|
10
|
+
const apisUsed = [];
|
|
11
|
+
const [treeMeta, idMap] = await Promise.all([
|
|
12
|
+
client.call('GET', '/openapi/v1/data/gotonline/overview/stack/overall/tree/presign', 'v1.0.1', keyQuery),
|
|
13
|
+
fetchFunctionIdMap(client, keyQuery),
|
|
14
|
+
]);
|
|
15
|
+
apisUsed.push('gotonline_overview_stack_overall_tree_presign', 'gotonline_overview_stack_id_map');
|
|
16
|
+
const url = typeof treeMeta['dataPresignUrl'] === 'string' ? treeMeta['dataPresignUrl'] : null;
|
|
17
|
+
if (!url)
|
|
18
|
+
throw new Error('线程堆栈树未返回 dataPresignUrl(报告可能未开 CPU 堆栈或 SDK < 2.5.1)');
|
|
19
|
+
const testMode = treeMeta['testMode'] != null ? String(treeMeta['testMode']) : null;
|
|
20
|
+
const payload = await client.downloadPresign(url);
|
|
21
|
+
const csvText = payload.kind === 'text' ? (payload.text ?? '') : JSON.stringify(payload.json ?? '');
|
|
22
|
+
const nodes = parseStackCsv(csvText, testMode);
|
|
23
|
+
return finalizeAgg(nodes, idMap, topN, metric, testMode, apisUsed, 'overview.stack.overall');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Mono:正向堆栈树 + Mono 函数 idmap。
|
|
27
|
+
* 文本格式与 Overview CSV 不同时走宽松解析;失败则抛错由上层处理。
|
|
28
|
+
*/
|
|
29
|
+
export async function aggregateMonoStack(client, identity, topN, metric = 'selfMemory') {
|
|
30
|
+
const keyQuery = identity.dataKey
|
|
31
|
+
? { dataKey: identity.dataKey }
|
|
32
|
+
: { recordId: identity.recordId };
|
|
33
|
+
const apisUsed = [];
|
|
34
|
+
const [treeMeta, idMapRaw] = await Promise.all([
|
|
35
|
+
client.call('GET', '/openapi/v1/data/gotonline/mono/stack/tree/overall/presign', 'v1.0.1', keyQuery),
|
|
36
|
+
client.call('GET', '/openapi/v1/data/gotonline/mono/stack/method/idmap/presign', 'v1.0.1', keyQuery),
|
|
37
|
+
]);
|
|
38
|
+
apisUsed.push('gotonline_mono_stack_tree_overall_presign', 'gotonline_mono_stack_method_idmap_presign');
|
|
39
|
+
const treeUrl = typeof treeMeta['dataPresignUrl'] === 'string' ? treeMeta['dataPresignUrl'] : null;
|
|
40
|
+
const mapUrl = typeof idMapRaw['dataPresignUrl'] === 'string' ? idMapRaw['dataPresignUrl'] : null;
|
|
41
|
+
if (!treeUrl)
|
|
42
|
+
throw new Error('Mono 堆栈树未返回 dataPresignUrl');
|
|
43
|
+
if (!mapUrl)
|
|
44
|
+
throw new Error('Mono 函数 idmap 未返回 dataPresignUrl');
|
|
45
|
+
const [treePayload, mapPayload] = await Promise.all([
|
|
46
|
+
client.downloadPresign(treeUrl),
|
|
47
|
+
client.downloadPresign(mapUrl),
|
|
48
|
+
]);
|
|
49
|
+
const idMap = parseIdMapPayload(mapPayload.json ?? mapPayload.text);
|
|
50
|
+
const csvText = treePayload.kind === 'text' ? (treePayload.text ?? '') : JSON.stringify(treePayload.json ?? '');
|
|
51
|
+
// Mono 正向树多为文本/CSV;按 CPU_ONLY/内存列尝试解析
|
|
52
|
+
const nodes = parseStackCsv(csvText, 'LUA_MEM_ONLY');
|
|
53
|
+
const useMetric = metric === 'selfTime' || metric === 'totalTime' ? 'selfMemory' : metric;
|
|
54
|
+
return finalizeAgg(nodes, idMap, topN, useMetric, 'LUA_MEM_ONLY', apisUsed, 'mono.stack.overall');
|
|
55
|
+
}
|
|
56
|
+
async function fetchFunctionIdMap(client, keyQuery) {
|
|
57
|
+
const raw = await client.call('GET', '/openapi/v1/data/gotonline/overview/stack/id/map', 'v1.0.1', keyQuery);
|
|
58
|
+
return parseIdMapPayload(raw);
|
|
59
|
+
}
|
|
60
|
+
function parseIdMapPayload(raw) {
|
|
61
|
+
const map = new Map();
|
|
62
|
+
let list = null;
|
|
63
|
+
if (isObj(raw)) {
|
|
64
|
+
list = raw['id_map'] ?? raw['stats_id_map'] ?? raw['idMap'];
|
|
65
|
+
}
|
|
66
|
+
if (!list && typeof raw === 'string') {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (isObj(parsed))
|
|
70
|
+
list = parsed['id_map'] ?? parsed['stats_id_map'];
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* ignore */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(list))
|
|
77
|
+
return map;
|
|
78
|
+
for (const item of list) {
|
|
79
|
+
if (!isObj(item))
|
|
80
|
+
continue;
|
|
81
|
+
const id = item['id'] != null ? String(item['id']) : '';
|
|
82
|
+
const name = item['name'] != null ? String(item['name']) : '';
|
|
83
|
+
if (id)
|
|
84
|
+
map.set(id, name || id);
|
|
85
|
+
}
|
|
86
|
+
return map;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 解析无表头 CSV。列数随 testMode 变化;UE 固定 7 列(与 CPU_ONLY 同形)。
|
|
90
|
+
*/
|
|
91
|
+
export function parseStackCsv(csvText, testMode) {
|
|
92
|
+
const lines = csvText
|
|
93
|
+
.replace(/^\uFEFF/, '')
|
|
94
|
+
.split(/\r?\n/)
|
|
95
|
+
.map((l) => l.trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
const nodes = [];
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const cols = splitCsvLine(line);
|
|
100
|
+
if (cols.length < 6)
|
|
101
|
+
continue;
|
|
102
|
+
const depth = Number(cols[0]);
|
|
103
|
+
const sampleId = String(cols[1] ?? '');
|
|
104
|
+
const methodId = String(cols[2] ?? '');
|
|
105
|
+
if (!methodId || Number.isNaN(depth))
|
|
106
|
+
continue;
|
|
107
|
+
const mode = (testMode ?? inferMode(cols.length)).toUpperCase();
|
|
108
|
+
let totalTimeUs = 0;
|
|
109
|
+
let selfTimeUs = 0;
|
|
110
|
+
let callCount = 0;
|
|
111
|
+
let callFrameCount = 0;
|
|
112
|
+
let totalMemoryKb = 0;
|
|
113
|
+
let selfMemoryKb = 0;
|
|
114
|
+
if (mode === 'LUA_MEM_ONLY' || (cols.length === 7 && mode.includes('LUA') && !mode.includes('CPU'))) {
|
|
115
|
+
totalMemoryKb = num(cols[3]);
|
|
116
|
+
selfMemoryKb = num(cols[4]);
|
|
117
|
+
callCount = num(cols[5]);
|
|
118
|
+
callFrameCount = num(cols[6]);
|
|
119
|
+
}
|
|
120
|
+
else if (mode === 'CPU_AND_LUA_MEM' || cols.length >= 9) {
|
|
121
|
+
totalTimeUs = num(cols[3]);
|
|
122
|
+
selfTimeUs = num(cols[4]);
|
|
123
|
+
callCount = num(cols[5]);
|
|
124
|
+
totalMemoryKb = num(cols[6]);
|
|
125
|
+
selfMemoryKb = num(cols[7]);
|
|
126
|
+
callFrameCount = num(cols[8]);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// CPU_ONLY / UE 7 列
|
|
130
|
+
totalTimeUs = num(cols[3]);
|
|
131
|
+
selfTimeUs = num(cols[4]);
|
|
132
|
+
callCount = num(cols[5]);
|
|
133
|
+
callFrameCount = num(cols[6]);
|
|
134
|
+
}
|
|
135
|
+
nodes.push({
|
|
136
|
+
depth,
|
|
137
|
+
sampleId,
|
|
138
|
+
methodId,
|
|
139
|
+
totalTimeUs,
|
|
140
|
+
selfTimeUs,
|
|
141
|
+
callCount,
|
|
142
|
+
callFrameCount,
|
|
143
|
+
totalMemoryKb,
|
|
144
|
+
selfMemoryKb,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return nodes;
|
|
148
|
+
}
|
|
149
|
+
function inferMode(colCount) {
|
|
150
|
+
if (colCount >= 9)
|
|
151
|
+
return 'CPU_AND_LUA_MEM';
|
|
152
|
+
return 'CPU_ONLY';
|
|
153
|
+
}
|
|
154
|
+
function num(v) {
|
|
155
|
+
if (v == null || v === '')
|
|
156
|
+
return 0;
|
|
157
|
+
const n = Number(v);
|
|
158
|
+
return Number.isFinite(n) ? n : 0;
|
|
159
|
+
}
|
|
160
|
+
/** 简单 CSV 分割:堆栈树字段不含引号逗号,按逗号切即可;兼容制表符。 */
|
|
161
|
+
function splitCsvLine(line) {
|
|
162
|
+
if (line.includes('\t') && !line.includes(','))
|
|
163
|
+
return line.split('\t');
|
|
164
|
+
return line.split(',');
|
|
165
|
+
}
|
|
166
|
+
function finalizeAgg(nodes, idMap, topN, metric, testMode, apisUsed, source) {
|
|
167
|
+
const byId = new Map();
|
|
168
|
+
for (const n of nodes) {
|
|
169
|
+
let row = byId.get(n.methodId);
|
|
170
|
+
if (!row) {
|
|
171
|
+
row = {
|
|
172
|
+
methodId: n.methodId,
|
|
173
|
+
name: idMap.get(n.methodId) ?? n.methodId,
|
|
174
|
+
selfTimeUs: 0,
|
|
175
|
+
totalTimeUs: 0,
|
|
176
|
+
callCount: 0,
|
|
177
|
+
callFrameCount: 0,
|
|
178
|
+
selfMemoryKb: 0,
|
|
179
|
+
totalMemoryKb: 0,
|
|
180
|
+
nodeCount: 0,
|
|
181
|
+
};
|
|
182
|
+
byId.set(n.methodId, row);
|
|
183
|
+
}
|
|
184
|
+
row.selfTimeUs += n.selfTimeUs;
|
|
185
|
+
row.totalTimeUs += n.totalTimeUs;
|
|
186
|
+
row.callCount += n.callCount;
|
|
187
|
+
row.callFrameCount += n.callFrameCount;
|
|
188
|
+
row.selfMemoryKb += n.selfMemoryKb;
|
|
189
|
+
row.totalMemoryKb += n.totalMemoryKb;
|
|
190
|
+
row.nodeCount += 1;
|
|
191
|
+
}
|
|
192
|
+
// 同名不同 methodId 再按 name 合并一次(文档要求「同一个函数的节点还需要合并」)
|
|
193
|
+
const byName = new Map();
|
|
194
|
+
for (const row of byId.values()) {
|
|
195
|
+
const key = row.name || row.methodId;
|
|
196
|
+
const exist = byName.get(key);
|
|
197
|
+
if (!exist) {
|
|
198
|
+
byName.set(key, { ...row });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
exist.selfTimeUs += row.selfTimeUs;
|
|
202
|
+
exist.totalTimeUs += row.totalTimeUs;
|
|
203
|
+
exist.callCount += row.callCount;
|
|
204
|
+
exist.callFrameCount += row.callFrameCount;
|
|
205
|
+
exist.selfMemoryKb += row.selfMemoryKb;
|
|
206
|
+
exist.totalMemoryKb += row.totalMemoryKb;
|
|
207
|
+
exist.nodeCount += row.nodeCount;
|
|
208
|
+
// 保留首次 methodId
|
|
209
|
+
}
|
|
210
|
+
const metricOf = (r) => {
|
|
211
|
+
switch (metric) {
|
|
212
|
+
case 'totalTime':
|
|
213
|
+
return r.totalTimeUs;
|
|
214
|
+
case 'callCount':
|
|
215
|
+
return r.callCount;
|
|
216
|
+
case 'selfMemory':
|
|
217
|
+
return r.selfMemoryKb;
|
|
218
|
+
case 'selfTime':
|
|
219
|
+
default:
|
|
220
|
+
return r.selfTimeUs;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const unit = metric === 'selfMemory' ? 'KB' : metric === 'callCount' ? '次' : 'ms';
|
|
224
|
+
const sorted = [...byName.values()].sort((a, b) => metricOf(b) - metricOf(a));
|
|
225
|
+
const rows = sorted.slice(0, topN).map((r) => {
|
|
226
|
+
const value = metric === 'callCount'
|
|
227
|
+
? r.callCount
|
|
228
|
+
: metric === 'selfMemory'
|
|
229
|
+
? round2(r.selfMemoryKb)
|
|
230
|
+
: round2(metricOf(r) / 1000); // μs → ms
|
|
231
|
+
return {
|
|
232
|
+
name: r.name,
|
|
233
|
+
methodId: r.methodId,
|
|
234
|
+
value,
|
|
235
|
+
unit,
|
|
236
|
+
selfTimeMs: round2(r.selfTimeUs / 1000),
|
|
237
|
+
totalTimeMs: round2(r.totalTimeUs / 1000),
|
|
238
|
+
selfMemoryKb: round2(r.selfMemoryKb),
|
|
239
|
+
callCount: r.callCount,
|
|
240
|
+
callFrameCount: r.callFrameCount,
|
|
241
|
+
nodeCount: r.nodeCount,
|
|
242
|
+
source,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
testMode,
|
|
247
|
+
metric,
|
|
248
|
+
unit,
|
|
249
|
+
rows,
|
|
250
|
+
totalNodes: nodes.length,
|
|
251
|
+
uniqueFunctions: byName.size,
|
|
252
|
+
apisUsed,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function round2(n) {
|
|
256
|
+
return Math.round(n * 100) / 100;
|
|
257
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { UwaClient } from '../client.js';
|
|
2
|
+
import type { CompositeTool } from './types.js';
|
|
3
|
+
export interface FunctionRow {
|
|
4
|
+
name: string;
|
|
5
|
+
value: number;
|
|
6
|
+
unit?: string;
|
|
7
|
+
source: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
methodId?: string;
|
|
10
|
+
selfTimeMs?: number;
|
|
11
|
+
totalTimeMs?: number;
|
|
12
|
+
callCount?: number;
|
|
13
|
+
nodeCount?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function runTopFunctions(client: UwaClient, args: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
16
|
+
export declare const topFunctionsTool: CompositeTool;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { z } from './types.js';
|
|
2
|
+
import { errorResult, jsonToolResult, requireReportKey, topNOf } from './helpers.js';
|
|
3
|
+
import { fetchReportIdentity } from './route-overview.js';
|
|
4
|
+
import { aggregateMonoStack, aggregateOverviewStack } from './stack-agg.js';
|
|
5
|
+
function resolveMetric(raw, mode) {
|
|
6
|
+
if (raw === 'totalTime' || raw === 'callCount' || raw === 'selfMemory' || raw === 'selfTime') {
|
|
7
|
+
return raw;
|
|
8
|
+
}
|
|
9
|
+
return mode === 'mono' ? 'selfMemory' : 'selfTime';
|
|
10
|
+
}
|
|
11
|
+
export async function runTopFunctions(client, args) {
|
|
12
|
+
const key = requireReportKey(args);
|
|
13
|
+
const topN = topNOf(args, 20);
|
|
14
|
+
const subtype = typeof args['serviceSubtype'] === 'string' && args['serviceSubtype']
|
|
15
|
+
? String(args['serviceSubtype']).toLowerCase()
|
|
16
|
+
: undefined;
|
|
17
|
+
const identity = await fetchReportIdentity(client, key);
|
|
18
|
+
const apisUsed = ['get_report_detail'];
|
|
19
|
+
const mode = subtype ?? identity.serviceSubtype ?? 'overview';
|
|
20
|
+
const metric = resolveMetric(args['metric'], mode);
|
|
21
|
+
if (mode === 'mono') {
|
|
22
|
+
if (identity.engine !== 'unity') {
|
|
23
|
+
return {
|
|
24
|
+
engine: identity.engine,
|
|
25
|
+
dataKey: identity.dataKey,
|
|
26
|
+
items: [],
|
|
27
|
+
_apisUsed: apisUsed,
|
|
28
|
+
_note: 'Mono 模式仅支持 Unity 报告。',
|
|
29
|
+
_limitations: ['engine!=unity'],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const agg = await aggregateMonoStack(client, identity, topN, metric);
|
|
34
|
+
return {
|
|
35
|
+
engine: identity.engine,
|
|
36
|
+
dataKey: identity.dataKey,
|
|
37
|
+
serviceSubtype: 'mono',
|
|
38
|
+
topN,
|
|
39
|
+
metric: agg.metric,
|
|
40
|
+
testMode: agg.testMode,
|
|
41
|
+
items: agg.rows,
|
|
42
|
+
totalNodes: agg.totalNodes,
|
|
43
|
+
uniqueFunctions: agg.uniqueFunctions,
|
|
44
|
+
_apisUsed: [...apisUsed, ...agg.apisUsed],
|
|
45
|
+
_note: `已下载 Mono 正向堆栈树并按函数名合并同名节点(共 ${agg.totalNodes} 个节点 → ${agg.uniqueFunctions} 个函数),` +
|
|
46
|
+
`按 ${agg.metric} 降序取 Top ${agg.rows.length}。`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return {
|
|
51
|
+
engine: identity.engine,
|
|
52
|
+
dataKey: identity.dataKey,
|
|
53
|
+
serviceSubtype: 'mono',
|
|
54
|
+
items: [],
|
|
55
|
+
_apisUsed: apisUsed,
|
|
56
|
+
_note: `Mono 堆栈树聚合失败:${e instanceof Error ? e.message : String(e)}`,
|
|
57
|
+
_limitations: ['mono_stack_unavailable'],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Overview:必须以堆栈树 + idmap 为准;统计接口里的「卡顿重点函数 / func / 自定义函数组」都不完整。
|
|
62
|
+
try {
|
|
63
|
+
const agg = await aggregateOverviewStack(client, identity, topN, metric);
|
|
64
|
+
return {
|
|
65
|
+
engine: identity.engine,
|
|
66
|
+
dataKey: identity.dataKey,
|
|
67
|
+
createDate: identity.createDate,
|
|
68
|
+
sdkVersion: identity.sdkVersion,
|
|
69
|
+
serviceSubtype: identity.serviceSubtype,
|
|
70
|
+
topN,
|
|
71
|
+
metric: agg.metric,
|
|
72
|
+
testMode: agg.testMode,
|
|
73
|
+
items: agg.rows,
|
|
74
|
+
totalNodes: agg.totalNodes,
|
|
75
|
+
uniqueFunctions: agg.uniqueFunctions,
|
|
76
|
+
_apisUsed: [...apisUsed, ...agg.apisUsed],
|
|
77
|
+
_note: `已下载主线程堆栈树并按函数名合并同名节点(共 ${agg.totalNodes} 个节点 → ${agg.uniqueFunctions} 个函数),` +
|
|
78
|
+
`按 ${agg.metric}(${agg.unit})降序取 Top ${agg.rows.length}。` +
|
|
79
|
+
`默认排序字段为 selfTime(自身耗时),不是统计接口里的抽样指标。`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
return {
|
|
84
|
+
engine: identity.engine,
|
|
85
|
+
dataKey: identity.dataKey,
|
|
86
|
+
createDate: identity.createDate,
|
|
87
|
+
sdkVersion: identity.sdkVersion,
|
|
88
|
+
serviceSubtype: identity.serviceSubtype,
|
|
89
|
+
items: [],
|
|
90
|
+
_apisUsed: apisUsed,
|
|
91
|
+
_note: `堆栈树聚合失败:${e instanceof Error ? e.message : String(e)}。` +
|
|
92
|
+
`Overview 函数 Top 依赖 stack/overall/tree + stack/id/map(通常需 SDK ≥ 2.5.1 且开启 CPU 堆栈)。`,
|
|
93
|
+
_limitations: ['overview_stack_unavailable'],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export const topFunctionsTool = {
|
|
98
|
+
id: 'top_functions',
|
|
99
|
+
title: '函数耗时 Top N',
|
|
100
|
+
engines: ['unity', 'unreal'],
|
|
101
|
+
filterKeys: ['top_functions', 'composite', 'preset.default', 'overview', 'mono'],
|
|
102
|
+
description: [
|
|
103
|
+
'从堆栈树聚合指定报告的函数 Top N(按自身耗时合并同名节点)。',
|
|
104
|
+
'适用引擎:Unity / UE|复合工具',
|
|
105
|
+
'Overview:下载主线程堆栈树(stack/overall/tree/presign)+ 函数 idmap(stack/id/map),',
|
|
106
|
+
'将 methodId 还原为函数名,并把树上同一函数的多个节点合并(selfTime/callCount 相加),再取 Top N。',
|
|
107
|
+
'不要用统计接口里的「卡顿重点函数」或自定义函数组——那不是全量函数耗时。',
|
|
108
|
+
'Mono(仅 Unity):走 Mono 正向堆栈树 + Mono 函数 idmap,默认按自身内存排序。',
|
|
109
|
+
'需要原始树时请直接调对应的 stack/presign 原子工具。',
|
|
110
|
+
].join('\n'),
|
|
111
|
+
inputSchema: {
|
|
112
|
+
dataKey: z.string().optional().describe('报告 dataKey,与 recordId 二选一'),
|
|
113
|
+
recordId: z.union([z.string(), z.number()]).optional().describe('报告 recordId,与 dataKey 二选一'),
|
|
114
|
+
topN: z.number().optional().describe('返回条数,默认 20,最大 200'),
|
|
115
|
+
metric: z
|
|
116
|
+
.enum(['selfTime', 'totalTime', 'callCount', 'selfMemory'])
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('排序指标,默认 Overview 用 selfTime(自身耗时 ms),Mono 用 selfMemory(KB)'),
|
|
119
|
+
serviceSubtype: z
|
|
120
|
+
.enum(['overview', 'mono'])
|
|
121
|
+
.optional()
|
|
122
|
+
.describe('报告模式,默认跟随报告详情;mono 仅 Unity'),
|
|
123
|
+
},
|
|
124
|
+
async handler(args, ctx) {
|
|
125
|
+
try {
|
|
126
|
+
const result = await runTopFunctions(ctx.client, args);
|
|
127
|
+
return jsonToolResult(result, ctx.maxChars);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
return errorResult(err);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UwaClient } from '../client.js';
|
|
2
|
+
import type { CompositeTool } from './types.js';
|
|
3
|
+
export interface ResourceRow {
|
|
4
|
+
name: string;
|
|
5
|
+
assetType: string;
|
|
6
|
+
memoryBytes: number;
|
|
7
|
+
count?: number;
|
|
8
|
+
extras?: Record<string, unknown>;
|
|
9
|
+
_source: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runTopResources(client: UwaClient, args: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
12
|
+
export declare const topResourcesTool: CompositeTool;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { z } from './types.js';
|
|
2
|
+
import { errorResult, isObj, jsonToolResult, requireReportKey, topNOf, } from './helpers.js';
|
|
3
|
+
import { fetchOverviewStatistic, fetchReportIdentity, preferAtResourceTable, } from './route-overview.js';
|
|
4
|
+
import { viewOverview } from './overview-view.js';
|
|
5
|
+
/** 与 OpenAPI memory/manage、AT overall 文档枚举对齐(11 种)。 */
|
|
6
|
+
const DEFAULT_ASSET_TYPES = [
|
|
7
|
+
'Texture',
|
|
8
|
+
'Mesh',
|
|
9
|
+
'AnimationClip',
|
|
10
|
+
'AudioClip',
|
|
11
|
+
'Material',
|
|
12
|
+
'Shader',
|
|
13
|
+
'Font',
|
|
14
|
+
'RenderTexture',
|
|
15
|
+
'ParticleSystem',
|
|
16
|
+
'AssetBundle',
|
|
17
|
+
'TextAsset',
|
|
18
|
+
];
|
|
19
|
+
/** AT 资源总览(Unity / UE 实测均可用)。 */
|
|
20
|
+
async function fetchAtAssetType(client, identity, assetType) {
|
|
21
|
+
const keyQuery = identity.dataKey
|
|
22
|
+
? { dataKey: identity.dataKey }
|
|
23
|
+
: { recordId: identity.recordId };
|
|
24
|
+
const meta = (await client.call('GET', '/openapi/v1/data/gotonline/overview/at/resource/overall/table/presign', 'v1.0.1', { ...keyQuery, assetType }));
|
|
25
|
+
const url = typeof meta['dataPresignUrl'] === 'string' ? meta['dataPresignUrl'] : null;
|
|
26
|
+
if (!url)
|
|
27
|
+
return [];
|
|
28
|
+
const payload = await client.downloadPresign(url);
|
|
29
|
+
const resources = extractAtResources(payload.json);
|
|
30
|
+
return resources.map((r) => ({
|
|
31
|
+
name: String(r['name'] ?? r['id'] ?? ''),
|
|
32
|
+
assetType,
|
|
33
|
+
memoryBytes: Number(r['memMaximum'] ?? 0) || 0,
|
|
34
|
+
count: r['countMaximum'] != null ? Number(r['countMaximum']) : undefined,
|
|
35
|
+
extras: pickExtras(r, ['id', 'typeName', 'tags', 'lifecycle', 'properties']),
|
|
36
|
+
_source: 'at/resource/overall/table/presign',
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
/** Unity 旧路径:memory/manage(仅 Unity,且 AT 不可用时回退)。 */
|
|
40
|
+
async function fetchMemoryManageAssetType(client, identity, assetType) {
|
|
41
|
+
const keyQuery = identity.dataKey
|
|
42
|
+
? { dataKey: identity.dataKey }
|
|
43
|
+
: { recordId: identity.recordId };
|
|
44
|
+
const data = (await client.call('GET', '/openapi/v1/data/gotonline/overview/memory/manage/data/report', 'v1.0.1', { ...keyQuery, assetType }));
|
|
45
|
+
const dict = isObj(data['Asset_dict']) ? data['Asset_dict'] : {};
|
|
46
|
+
return Object.values(dict)
|
|
47
|
+
.filter(isObj)
|
|
48
|
+
.map((r) => ({
|
|
49
|
+
name: String(r['name'] ?? ''),
|
|
50
|
+
assetType,
|
|
51
|
+
memoryBytes: Number(r['max_memory'] ?? 0) || 0,
|
|
52
|
+
count: r['max_count'] != null ? Number(r['max_count']) : undefined,
|
|
53
|
+
extras: pickExtras(r, ['resident_frames', 'last_zero_frame', 'has_rw', 'width', 'height', 'format']),
|
|
54
|
+
_source: 'memory/manage/data/report',
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
function extractAtResources(json) {
|
|
58
|
+
if (!json)
|
|
59
|
+
return [];
|
|
60
|
+
if (Array.isArray(json))
|
|
61
|
+
return json.filter(isObj);
|
|
62
|
+
if (!isObj(json))
|
|
63
|
+
return [];
|
|
64
|
+
if (Array.isArray(json['resources']))
|
|
65
|
+
return json['resources'].filter(isObj);
|
|
66
|
+
for (const v of Object.values(json)) {
|
|
67
|
+
if (Array.isArray(v) && v.length && isObj(v[0]) && ('memMaximum' in v[0] || 'name' in v[0])) {
|
|
68
|
+
return v.filter(isObj);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
function pickExtras(r, keys) {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const k of keys)
|
|
76
|
+
if (r[k] !== undefined)
|
|
77
|
+
out[k] = r[k];
|
|
78
|
+
return Object.keys(out).length ? out : undefined;
|
|
79
|
+
}
|
|
80
|
+
function typeLevelFromOverview(view) {
|
|
81
|
+
return view.memory.map((m) => ({
|
|
82
|
+
name: m.name,
|
|
83
|
+
assetType: m.assetType,
|
|
84
|
+
memoryBytes: m.memoryBytes,
|
|
85
|
+
_source: m._source,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
async function fetchPerAssetRows(client, identity, assetTypes, useAt) {
|
|
89
|
+
const apisUsed = [];
|
|
90
|
+
const errors = [];
|
|
91
|
+
const items = [];
|
|
92
|
+
const settled = await Promise.allSettled(assetTypes.map((t) => useAt ? fetchAtAssetType(client, identity, t) : fetchMemoryManageAssetType(client, identity, t)));
|
|
93
|
+
for (let i = 0; i < settled.length; i++) {
|
|
94
|
+
const r = settled[i];
|
|
95
|
+
const t = assetTypes[i];
|
|
96
|
+
if (r.status === 'fulfilled') {
|
|
97
|
+
items.push(...r.value);
|
|
98
|
+
apisUsed.push(useAt ? `at_resource_overall:${t}` : `memory_manage:${t}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
errors.push(`${t}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
items,
|
|
106
|
+
apisUsed,
|
|
107
|
+
errors,
|
|
108
|
+
sourceKind: useAt ? 'at_resource_overall_presign' : 'memory_manage',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function groupByAssetType(items, assetTypes, topN) {
|
|
112
|
+
const byType = {};
|
|
113
|
+
for (const t of assetTypes) {
|
|
114
|
+
byType[t] = items
|
|
115
|
+
.filter((r) => r.assetType === t)
|
|
116
|
+
.sort((a, b) => b.memoryBytes - a.memoryBytes)
|
|
117
|
+
.slice(0, topN);
|
|
118
|
+
}
|
|
119
|
+
return byType;
|
|
120
|
+
}
|
|
121
|
+
function normalizeGroupBy(raw) {
|
|
122
|
+
return raw === 'assetType' ? 'assetType' : 'merged';
|
|
123
|
+
}
|
|
124
|
+
export async function runTopResources(client, args) {
|
|
125
|
+
const key = requireReportKey(args);
|
|
126
|
+
const topN = topNOf(args, 20);
|
|
127
|
+
const assetTypes = normalizeAssetTypes(args['assetTypes']);
|
|
128
|
+
const groupBy = normalizeGroupBy(args['groupBy']);
|
|
129
|
+
const identity = await fetchReportIdentity(client, key);
|
|
130
|
+
const apisUsed = ['get_report_detail'];
|
|
131
|
+
// 先拉 Overview,用于资源开关判定与最终回退
|
|
132
|
+
const { target, data } = await fetchOverviewStatistic(client, identity);
|
|
133
|
+
apisUsed.push(target.id);
|
|
134
|
+
const view = viewOverview(data);
|
|
135
|
+
const canAt = preferAtResourceTable(identity);
|
|
136
|
+
// Unity 旧报告可回退 memory/manage;UE 没有这条,只能 AT 或类型级峰值
|
|
137
|
+
const useAt = canAt || identity.engine === 'unreal';
|
|
138
|
+
const useMemoryManage = !useAt && identity.engine === 'unity';
|
|
139
|
+
const packResult = (items, extra) => {
|
|
140
|
+
const sorted = [...items].sort((a, b) => b.memoryBytes - a.memoryBytes);
|
|
141
|
+
if (groupBy === 'assetType') {
|
|
142
|
+
const byType = groupByAssetType(sorted, assetTypes, topN);
|
|
143
|
+
const flat = Object.values(byType).flat();
|
|
144
|
+
return {
|
|
145
|
+
engine: identity.engine,
|
|
146
|
+
dataKey: identity.dataKey,
|
|
147
|
+
createDate: identity.createDate,
|
|
148
|
+
sdkVersion: identity.sdkVersion,
|
|
149
|
+
assetTypes,
|
|
150
|
+
topN,
|
|
151
|
+
groupBy,
|
|
152
|
+
byType,
|
|
153
|
+
items: flat,
|
|
154
|
+
_overviewShape: view.shape,
|
|
155
|
+
...extra,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
engine: identity.engine,
|
|
160
|
+
dataKey: identity.dataKey,
|
|
161
|
+
createDate: identity.createDate,
|
|
162
|
+
sdkVersion: identity.sdkVersion,
|
|
163
|
+
assetTypes,
|
|
164
|
+
topN,
|
|
165
|
+
groupBy,
|
|
166
|
+
items: sorted.slice(0, topN),
|
|
167
|
+
_overviewShape: view.shape,
|
|
168
|
+
...extra,
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
if (!useAt && !useMemoryManage) {
|
|
172
|
+
return packResult(typeLevelFromOverview(view), {
|
|
173
|
+
_note: '无法使用 AT 资源列表,已回退为 Overview 类型级峰值。',
|
|
174
|
+
_apisUsed: apisUsed,
|
|
175
|
+
_limitations: ['at_resource_unavailable'],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// Unity:明确未开资源采集时不必打 AT;UE 的 AT 实测可直接用,不依赖 OverviewHasResource 字段
|
|
179
|
+
if (identity.engine === 'unity' && view.hasResource === false && !canAt) {
|
|
180
|
+
return packResult(typeLevelFromOverview(view), {
|
|
181
|
+
_note: '报告未开启资源采集,无法拉逐资源列表;已回退为 Overview 类型级峰值。',
|
|
182
|
+
_apisUsed: apisUsed,
|
|
183
|
+
_limitations: ['未开启资源采集'],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
let fetched = await fetchPerAssetRows(client, identity, assetTypes, useAt);
|
|
187
|
+
// AT 全部失败时,Unity 还可回退 memory/manage
|
|
188
|
+
if (useAt && fetched.items.length === 0 && identity.engine === 'unity') {
|
|
189
|
+
const fallback = await fetchPerAssetRows(client, identity, assetTypes, false);
|
|
190
|
+
if (fallback.items.length > 0 || fallback.errors.length === 0)
|
|
191
|
+
fetched = fallback;
|
|
192
|
+
}
|
|
193
|
+
apisUsed.push(...fetched.apisUsed);
|
|
194
|
+
if (fetched.items.length === 0) {
|
|
195
|
+
const fallbackItems = typeLevelFromOverview(view);
|
|
196
|
+
return packResult(fallbackItems, {
|
|
197
|
+
_sourceKind: fetched.sourceKind,
|
|
198
|
+
_apisUsed: apisUsed,
|
|
199
|
+
...(fetched.errors.length ? { _partialErrors: fetched.errors } : {}),
|
|
200
|
+
_note: fallbackItems.length > 0
|
|
201
|
+
? '逐资源列表为空,已回退为 Overview 类型级峰值。'
|
|
202
|
+
: '逐资源列表与类型级峰值均为空(报告可能未采集资源内存)。',
|
|
203
|
+
_limitations: ['per_asset_empty'],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const note = groupBy === 'assetType'
|
|
207
|
+
? `已按类型各取 Top ${topN}(${fetched.sourceKind},共 ${assetTypes.length} 种)。`
|
|
208
|
+
: `已按内存峰值合并 ${assetTypes.length} 种资源类型(${fetched.sourceKind}),返回全局 Top ${topN}。`;
|
|
209
|
+
return packResult(fetched.items, {
|
|
210
|
+
totalCandidates: fetched.items.length,
|
|
211
|
+
_sourceKind: fetched.sourceKind,
|
|
212
|
+
_apisUsed: apisUsed,
|
|
213
|
+
...(fetched.errors.length ? { _partialErrors: fetched.errors } : {}),
|
|
214
|
+
_note: note,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function normalizeAssetTypes(raw) {
|
|
218
|
+
if (Array.isArray(raw) && raw.length)
|
|
219
|
+
return raw.map(String);
|
|
220
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
221
|
+
return raw
|
|
222
|
+
.split(/[,,]/)
|
|
223
|
+
.map((s) => s.trim())
|
|
224
|
+
.filter(Boolean);
|
|
225
|
+
}
|
|
226
|
+
return [...DEFAULT_ASSET_TYPES];
|
|
227
|
+
}
|
|
228
|
+
export const topResourcesTool = {
|
|
229
|
+
id: 'top_resources',
|
|
230
|
+
title: '资源内存 Top N',
|
|
231
|
+
engines: ['unity', 'unreal'],
|
|
232
|
+
filterKeys: ['top_resources', 'composite', 'preset.default', 'overview'],
|
|
233
|
+
description: [
|
|
234
|
+
'聚合指定报告的资源内存占用 Top N(逐资源)。',
|
|
235
|
+
'适用引擎:Unity / UE|复合工具',
|
|
236
|
+
'Unity / UE 均优先走 AT 资源总览预签名(at/resource/overall/table/presign),按 assetType 并行下载后合并排序。',
|
|
237
|
+
'groupBy=merged(默认):跨类型合并后取全局 Top N;groupBy=assetType:每种类型各取 Top N,返回 byType。',
|
|
238
|
+
'Unity 旧报告(解析日 < 2026-06-25)回退 memory/manage;拉不到逐资源时再回退 Overview 类型级峰值。',
|
|
239
|
+
'不确定报告版本时直接用本工具;需要原始全量列表时再调对应原子工具。',
|
|
240
|
+
].join('\n'),
|
|
241
|
+
inputSchema: {
|
|
242
|
+
dataKey: z.string().optional().describe('报告 dataKey,与 recordId 二选一'),
|
|
243
|
+
recordId: z.union([z.string(), z.number()]).optional().describe('报告 recordId,与 dataKey 二选一'),
|
|
244
|
+
assetTypes: z
|
|
245
|
+
.array(z.string())
|
|
246
|
+
.optional()
|
|
247
|
+
.describe(`资源类型列表,默认全部 11 种:${DEFAULT_ASSET_TYPES.join(',')}。每次内部按类型各查一次`),
|
|
248
|
+
groupBy: z
|
|
249
|
+
.enum(['merged', 'assetType'])
|
|
250
|
+
.optional()
|
|
251
|
+
.describe("分组模式:merged=跨类型全局 Top N(默认);assetType=每种类型各 Top N,返回 byType"),
|
|
252
|
+
topN: z.number().optional().describe('返回条数,默认 20,最大 200;groupBy=assetType 时为每类型条数'),
|
|
253
|
+
},
|
|
254
|
+
async handler(args, ctx) {
|
|
255
|
+
try {
|
|
256
|
+
const result = await runTopResources(ctx.client, args);
|
|
257
|
+
return jsonToolResult(result, ctx.maxChars);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return errorResult(err);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
};
|