Files
maidong-ktv/tools/measure_ktv_js.js

173 lines
6.7 KiB
JavaScript
Raw Permalink 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.
/**
* 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));
})();