feat(protocol): 采集客户端 opt<list> 位痕迹并加分析脚本

This commit is contained in:
TermiNexus committed 2026-10-21 02:49:44 +08:00
1 parent 6a47e684aa
commit d416ee70ad
2 files changed
+156

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
# analyze_nullmap_trace.py - 把客户端采到的「可选 + 列表」位痕迹映射回 模块::类.方法
#
# 数据来源:镜像站 module_24635.js 的 Ht.t7h 补丁(可选读器)。它只对「内层读器是列表」的
# 可选字段记录一条 {bit, inner, stack},节流 POST 到 serve.js 的 /__dump__/nullmap_trace.json。
# 目的:确认 opt<list<…>> 在真实数据里到底占不占 nullmap 位(例如 ParticleEffectItem.colors)。
#
# 映射方法:Node 的 stack 里带 `module_XXXX.js:行:列`;用 src 里 `(<类>).<方法> = function`
# 的起始行建「行号 → 类.方法」索引,取不晚于该行的最近一条。
#
# 用法: python tools/analyze_nullmap_trace.py [--names names] [--src src]
import json
import os
import re
import sys
from collections import Counter
Root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def Arg(Name, Def):
if '--' + Name in sys.argv:
I = sys.argv.index('--' + Name)
if I + 1 < len(sys.argv):
return sys.argv[I + 1]
return Def
NamesDir = os.path.abspath(Arg('names', os.path.join(Root, 'names')))
SrcDir = os.path.abspath(Arg('src', os.path.join(Root, 'src')))
TracePath = os.path.join(NamesDir, 'nullmap_trace.json')
FrameRe = re.compile(r'module_(\d+)\.js:(\d+):(\d+)')
AssignRe = re.compile(r'\(([\w$]+)\)\.(t7h|s7h|u7h)\s*=\s*function')
def LoadLineMap():
"""module 号 → [(起始行, '类.方法')],按行号升序。"""
Out = {}
for Name in os.listdir(SrcDir):
M = re.match(r'module_(\d+)\.js$', Name)
if not M:
continue
Mod = str(int(M.group(1)))
Path = os.path.join(SrcDir, Name)
Rows = []
with open(Path, 'r', encoding='utf-8', errors='replace') as F:
for LineNo, Line in enumerate(F, 1):
for A in AssignRe.finditer(Line):
Rows.append((LineNo, A.group(1) + '.' + A.group(2)))
Rows.sort()
Out[Mod] = Rows
return Out
def Locate(LineMap, Mod, Line):
Rows = LineMap.get(Mod) or []
Best = None
for Start, Name in Rows:
if Start > Line:
break
Best = Name
return Best or '?'
def Main():
if not os.path.exists(TracePath):
print('缺少 ' + TracePath)
print('请先用镜像站进一局战斗(module_24635.js 已打位痕迹补丁,会自动 POST)。')
return 1
with open(TracePath, 'r', encoding='utf-8') as F:
Data = json.load(F)
Rows = Data.get('trace') or []
print('位痕迹:%d 条(采集于 %s)' % (len(Rows), Data.get('at', '?')))
LineMap = LoadLineMap()
ByCaller = Counter()
ByBit = Counter()
Samples = {}
Ordered = []
for R in Rows:
Bit = R.get('bit')
Stack = R.get('stack') or ''
Caller = '?'
CallerMod = '?'
# 取第一个能定位的栈帧(跳过 Ht.t7h 自己)
for M in FrameRe.finditer(Stack):
Mod, Line = M.group(1), int(M.group(2))
Name = Locate(LineMap, str(int(Mod)), Line)
if Name.startswith('Ht.'):
continue
Caller = Name
CallerMod = 'module_' + str(int(Mod))
break
Key = CallerMod + '::' + Caller
ByCaller[(Key, R.get('inner'))] += 1
ByBit[(Key, R.get('inner'), Bit)] += 1
if Key not in Samples:
Samples[Key] = Stack
Ordered.append((Key, R.get('inner'), Bit))
print(' 调用者分布(按次数):')
for (Key, Inner), N in ByCaller.most_common(20):
Ones = ByBit.get((Key, Inner, 1), 0)
Zeros = ByBit.get((Key, Inner, 0), 0)
print(' %6d %-40s inner=%-6s bit=1:%d bit=0:%d' % (N, Key, Inner, Ones, Zeros))
# 关注 ParticleEffectItem(85183::Ll):把它的位序列按出现顺序打出来
Focus = [(K, B) for K, I, B in Ordered if K == 'module_85183::Ll.t7h']
if Focus:
print(' ParticleEffectItem(module_85183::Ll)位序列,前 60 条:')
print(' ' + ''.join(str(B) for _, B in Focus[:60]))
print(' 样例栈:')
for Key, S in list(Samples.items())[:3]:
print(' [' + Key + '] ' + S[:160])
return 0
if __name__ == '__main__':
sys.exit(Main())
@@ -413,6 +413,43 @@
null == n ? t.f7h_1.b7h(!0) : (t.f7h_1.b7h(!1), this.r7h_1.s7h(t, n));
}),
(z(Ht).t7h = function (t) {
// [Hornet 调试] 可选读器的位痕迹采样:只对「可选 + 列表」这类罕见的组合记录
// (判据:内层读器带列表槽位 z7h_1),把位值、内层读器类名、调用栈节流 POST 到
// /__dump__/nullmap_trace.json,用于确认 opt<list<…>> 在真实数据里到底占不占位。
// 位只读一次:采样失败也只是少一条日志,绝不改变解码结果。
var Inner = this.r7h_1;
if (Inner && Inner.z7h_1 !== undefined) {
var B0 = t.f7h_1.xy();
try {
if (typeof window !== "undefined") {
var T = (window.__HornetBitTrace__ = window.__HornetBitTrace__ || {
rows: [], sent: 0, timer: 0,
push: function (Row) {
if (this.rows.length >= 4000) return;
this.rows.push(Row);
if (!this.timer) {
var Me = this;
this.timer = setTimeout(function () {
Me.timer = 0;
try {
fetch("/__dump__/nullmap_trace.json", {
method: "POST",
body: JSON.stringify({ at: new Date().toISOString(), count: Me.rows.length, trace: Me.rows }),
});
} catch (E2) {}
}, 1500);
}
},
});
var St = "";
try {
St = String(new Error().stack || "").split("\n").slice(1, 4).join(" | ");
} catch (E3) {}
T.push({ bit: B0, inner: Inner.constructor && Inner.constructor.name, stack: St });
}
} catch (E4) {}
return B0 ? null : Inner.t7h(t);
}
return t.f7h_1.xy() ? null : this.r7h_1.t7h(t);
}),
(z(Ht).u7h = function (t) {