173 lines
6.7 KiB
JavaScript
173 lines
6.7 KiB
JavaScript
/**
|
||
* JS 取址链路(maidong-ktv/app/src/main/assets/mobile/ktv_api.js)**真实脚本**的
|
||
* 127.0.0.1 stub 对照测量工具(优化前 / 优化后跑同一份脚本)。
|
||
*
|
||
* 为什么能这么测:ktv_api.js 走 `global.android.httpGet(url, headers, timeout)` 与
|
||
* `global.android.appToken(host)`(Android JavascriptInterface 是**同步**调用),
|
||
* 且 `android.appToken` 背后就是 Kotlin 的 `AppTokenClient`(按 host 缓存 10 分钟)。
|
||
* Node 里把这两个方法换成"记录时间戳 + 同步 sleep(一次往返耗时) + 按 host 缓存"的 stub,
|
||
* 就能在**不改一字**的真实脚本上量出:
|
||
* ① 点歌 → 首个请求发出;
|
||
* ② 点歌 → 拿到地址(= 用户在界面上"点了没反应"的时长);
|
||
* ③ 每首歌实际打几次 /i.php、几次 /music/do.php。
|
||
*
|
||
* 用法:node maidong-ktv/tools/measure_ktv_js.js (仓库内)
|
||
* STUB_LATENCY_MS=200 node maidong-ktv/tools/measure_ktv_js.js
|
||
* STUB_PREHEAT=1 node maidong-ktv/tools/measure_ktv_js.js (额外模拟一轮后台预热)
|
||
*/
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
// 直接跑(仓库内 tools/measure_ktv_js.js)与从工作区根跑(tmp/measure_ktv_js.js)都要能找到脚本。
|
||
const JS_FILE = [
|
||
path.resolve(__dirname, '../app/src/main/assets/mobile/ktv_api.js'),
|
||
path.resolve(__dirname, '../maidong-ktv/app/src/main/assets/mobile/ktv_api.js'),
|
||
].find((candidate) => fs.existsSync(candidate));
|
||
|
||
function syncSleep(ms) {
|
||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
||
}
|
||
|
||
function hostOf(u) {
|
||
const m = /^[a-z]+:\/\/([^/]+)/i.exec(String(u));
|
||
return m ? m[1] : '';
|
||
}
|
||
|
||
const RTT_MS = Number(process.env.STUB_LATENCY_MS || 150);
|
||
const OFFICIAL = 'http://zk.ktv1.top:6677';
|
||
const SELF = 'https://mdmuse.kasugano.cn';
|
||
const MUSICNO = '7303235';
|
||
const SELF_URL = 'http://md.kasugano.cn/media/' + MUSICNO + '.ts';
|
||
const OFFICIAL_URL = 'http://txog.ktvsky.com/E/ts/xxx/' + MUSICNO + '.ts';
|
||
|
||
function makeScenarios() {
|
||
const forbidden = { status: 200, body: JSON.stringify({ code: 403, msg: '请求被拒绝:检测到异常行为' }) };
|
||
const adOnly = { status: 200, body: JSON.stringify({ code: 200, data: 'https://wb66.cn/f/d/7b7c9/' + MUSICNO + '.ts' }) };
|
||
const notFound = { status: 200, body: JSON.stringify({ code: 404, msg: 'not in library' }) };
|
||
return {
|
||
forbidden: { officialOkTimes: 0, officialFail: forbidden },
|
||
adOnly: { officialOkTimes: 0, officialFail: adOnly },
|
||
notFound: { officialOkTimes: 0, officialFail: notFound },
|
||
quota2: { officialOkTimes: 2, officialFail: forbidden },
|
||
};
|
||
}
|
||
|
||
function newHarness(scenario) {
|
||
const log = [];
|
||
const calls = { iphp: 0, dophp: 0, appToken: 0, appTokenMisses: 0 };
|
||
const tokenCache = new Map();
|
||
let officialDoPhpCount = 0;
|
||
const t0 = Date.now();
|
||
const rel = () => Date.now() - t0;
|
||
|
||
const api = {
|
||
httpGet(url) {
|
||
const u = String(url);
|
||
const at = rel();
|
||
if (u.indexOf('/i.php') >= 0) {
|
||
calls.iphp++;
|
||
log.push({ t: at, kind: 'i.php', host: hostOf(u) });
|
||
syncSleep(RTT_MS);
|
||
return JSON.stringify({ status: 200, body: JSON.stringify({ code: 200, token: 'tok-' + hostOf(u), heart_time: 5400 }) });
|
||
}
|
||
if (u.indexOf('/music/do.php') >= 0) {
|
||
calls.dophp++;
|
||
const host = hostOf(u);
|
||
log.push({ t: at, kind: 'do.php', host: host });
|
||
syncSleep(RTT_MS);
|
||
if (host === hostOf(OFFICIAL)) {
|
||
officialDoPhpCount++;
|
||
if (officialDoPhpCount <= scenario.officialOkTimes) {
|
||
return JSON.stringify({ status: 200, body: JSON.stringify({ code: 200, msg: 'SUCCESS', data: OFFICIAL_URL }) });
|
||
}
|
||
const r = scenario.officialFail;
|
||
return JSON.stringify({ status: r.status, body: r.body });
|
||
}
|
||
return JSON.stringify({ status: 200, body: JSON.stringify({ code: 200, msg: 'SUCCESS', data: SELF_URL }) });
|
||
}
|
||
throw new Error('unexpected url ' + u);
|
||
},
|
||
appToken(host) {
|
||
const key = String(host);
|
||
calls.appToken++;
|
||
if (tokenCache.has(key)) return tokenCache.get(key);
|
||
calls.appTokenMisses++;
|
||
log.push({ t: rel(), kind: 'appToken(首次)', host: key });
|
||
syncSleep(RTT_MS);
|
||
tokenCache.set(key, 'app-token');
|
||
return 'app-token';
|
||
},
|
||
invalidateAppToken(host) { tokenCache.delete(String(host)); },
|
||
onSongSource() {},
|
||
reportSongResult() {},
|
||
log() {},
|
||
};
|
||
return { api, log, calls, rel };
|
||
}
|
||
|
||
async function runScenario(name, scenario, songs) {
|
||
delete require.cache[require.resolve(JS_FILE)];
|
||
const fakeWindow = {};
|
||
const prev = global.window;
|
||
global.window = fakeWindow;
|
||
const mod = require(JS_FILE);
|
||
if (prev === undefined) delete global.window; else global.window = prev;
|
||
|
||
const KtvApi = fakeWindow.KtvApi || mod.KtvApi;
|
||
const h = newHarness(scenario);
|
||
fakeWindow.android = h.api;
|
||
mod.android = h.api;
|
||
|
||
const api = new KtvApi({
|
||
mac: '1ca410000001', sn: 'aabbccddeeff', maxDeviceRetries: 0, debug: false,
|
||
hosts: [OFFICIAL, SELF],
|
||
});
|
||
|
||
// 可选:模拟 App 启动 / 进入点歌列表时的**后台预热**(SourcePreheat 在 JS 路径上做的事):
|
||
// ① 构造 window.api 并逐个源取一次 /i.php token(同时热好 DNS/TCP/TLS);
|
||
// ② 逐个源问一次 App 令牌(android.appToken → Kotlin 侧按 host 缓存)。
|
||
// 用 STUB_PREHEAT=1 打开,用于量"预热把首次点歌的哪些往返省掉了"。
|
||
let preheatMs = null;
|
||
if (process.env.STUB_PREHEAT) {
|
||
const preheatAt = h.rel();
|
||
const warmHosts = [OFFICIAL, SELF];
|
||
for (const host of warmHosts) {
|
||
await api.getToken(host, false);
|
||
h.api.appToken(host);
|
||
}
|
||
preheatMs = h.rel() - preheatAt;
|
||
}
|
||
|
||
const songsOut = [];
|
||
for (let i = 0; i < songs; i++) {
|
||
const start = h.rel();
|
||
const before = h.log.length;
|
||
const url = await api.getSongUrl(MUSICNO, '720', false);
|
||
const end = h.rel();
|
||
const reqs = h.log.slice(before);
|
||
const okReq = reqs.filter(r => r.kind === 'do.php' && r.host === hostOf(SELF))[0];
|
||
songsOut.push({
|
||
song: i + 1,
|
||
totalMs: end - start,
|
||
firstRequestSentAtMs: reqs.length ? reqs[0].t - start : null,
|
||
successSourceRequestSentAtMs: okReq ? okReq.t - start : null,
|
||
url: url,
|
||
requests: reqs.map(r => ((r.t - start) + 'ms ' + r.kind + ' ' + r.host)),
|
||
});
|
||
}
|
||
return {
|
||
scenario: name,
|
||
stub: { httpRoundTripMs: RTT_MS, preheat: !!process.env.STUB_PREHEAT, preheatMs: preheatMs },
|
||
totals: h.calls,
|
||
songs: songsOut,
|
||
};
|
||
}
|
||
|
||
(async () => {
|
||
const scenarios = makeScenarios();
|
||
const out = [];
|
||
for (const name of Object.keys(scenarios)) out.push(await runScenario(name, scenarios[name], 4));
|
||
console.log(JSON.stringify({ jsFile: JS_FILE, results: out }, null, 2));
|
||
})();
|