feat(protocol): 位痕迹按 codec 槽位指纹定位字段

This commit is contained in:
TermiNexus committed 2026-10-21 02:55:25 +08:00
1 parent 97917478a4
commit e0542b2681
2 files changed
+69 -77

No files matched your search

+52 -76
View File
@@ -1,18 +1,20 @@
# analyze_nullmap_trace.py - 把客户端采到的「可选 + 列表」位痕迹映射回 模块::类.方法
# analyze_nullmap_trace.py - 把客户端采到的「可选 + 列表」位痕迹定位到具体字段
#
# 数据来源:镜像站 module_24635.js 的 Ht.t7h 补丁(可选读器)。它只对「内层读器是列表」的
# 可选字段记录一条 {bit, inner, stack},节流 POST 到 serve.js 的 /__dump__/nullmap_trace.json。
# 数据来源:镜像站 module_24635.js 的 Ht.t7h 补丁(可选读器)。它只对「内层读器是列表」的可选字段
# 记录 {hid, bit, inner, elem, elemKeys, stack},节流 POST 到 serve.js 的 /__dump__/nullmap_trace.json。
# 目的:确认 opt<list<…>> 在真实数据里到底占不占 nullmap 位(例如 ParticleEffectItem.colors)。
#
# 映射方法:Node 的 stack 里带 `module_XXXX.js:行:列`;用 src 里 `(<类>).<方法> = function`
# 的起始行建「行号 → 类.方法」索引,取不晚于该行的最近一条。
# 定位方法(不依赖 stack,因为镜像站用 eval 加载模块,栈里没有模块名):
# 1. 每个可选包装器实例在客户端被分配稳定 id(hid),同一字段的所有读取共享同一个 hid;
# 2. elemKeys = 内层列表元素 codec 的槽位名集合。槽位名全局唯一,
# 用它去 names/codec_fields.json 的 Slots 里反查,能唯一确定「元素是哪个 codec」;
# 3. 再找哪些字段的类型是 opt<list<该模型>>,若唯一即定位成功。
#
# 用法: python tools/analyze_nullmap_trace.py [--names names] [--src src]
# 用法: python tools/analyze_nullmap_trace.py [--names names]
import json
import os
import re
import sys
from collections import Counter
from collections import Counter, defaultdict
Root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -26,42 +28,33 @@ def Arg(Name, 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')
FieldsPath = os.path.join(NamesDir, 'codec_fields.json')
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
def LoadSlots():
"""槽位集合 → [(codec 键, 模型名)],用于反查元素 codec。"""
with open(FieldsPath, 'r', encoding='utf-8') as F:
Codecs = json.load(F).get('Codecs') or {}
Out = defaultdict(list)
for Key, C in Codecs.items():
Slots = tuple(sorted(S.get('Slot') for S in (C.get('Slots') or [])))
if Slots:
Out[Slots].append((Key, C.get('ModelName') or C.get('ModelClassLocal') or '?'))
return Out, Codecs
def FindOwners(Codecs, ModelName):
"""类型为 opt<list<ModelName>> 的 (codec 键, 字段名)。"""
Out = []
Needle = 'opt<list<' + ModelName + '>>'
for Key, C in Codecs.items():
for F in C.get('Fields') or []:
if F.get('Type') == Needle:
Out.append((Key, F.get('Name')))
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)
@@ -72,46 +65,29 @@ def Main():
Rows = Data.get('trace') or []
print('位痕迹:%d 条(采集于 %s)' % (len(Rows), Data.get('at', '?')))
LineMap = LoadLineMap()
ByCaller = Counter()
ByBit = Counter()
Samples = {}
Ordered = []
SlotIndex, Codecs = LoadSlots()
ByHid = defaultdict(list)
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))
ByHid[R.get('hid')].append(R)
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])
print(' 可选列表字段 %d 个(按 hid 分组):' % len(ByHid))
for Hid in sorted(ByHid, key=lambda H: (H is None, H)):
Group = ByHid[Hid]
ElemKeys = tuple(sorted(Group[0].get('elemKeys') or []))
Elems = SlotIndex.get(ElemKeys) or []
ElemName = Elems[0][1] if len(Elems) == 1 else Group[0].get('elem')
Owners = FindOwners(Codecs, ElemName) if ElemName else []
Bits = Counter(R.get('bit') for R in Group)
Seq = ''.join(str(R.get('bit')) for R in Group[:40])
Where = (Owners[0][0] + ' . ' + str(Owners[0][1])) if len(Owners) == 1 else '?'
print(' hid=%-4s 次数=%-4d bit0=%-4d bit1=%-4d 元素=%-22s 字段=%s'
% (Hid, len(Group), Bits.get(0, 0), Bits.get(1, 0), ElemName or '?', Where))
if len(Seq) >= 4:
print(' 位序列:' + Seq)
if len(Elems) > 1:
print(' 元素多解:' + ', '.join(K + '(' + N + ')' for K, N in Elems[:5]))
if len(Owners) > 1:
print(' 字段多解:' + ', '.join(K + '.' + str(N) for K, N in Owners[:5]))
return 0
@@ -445,7 +445,23 @@
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 });
// hid:给每个可选包装器实例分配稳定 id(同一字段的多次读取共享 id);
// elem/elemKeys:内层列表的元素 codec 类名与槽位名——槽位名全局唯一,可离线唯一定位字段。
var Elem = Inner.z7h_1;
if (!Inner.__hornetHid) {
Inner.__hornetHid = T.nextId || 1;
T.nextId = Inner.__hornetHid + 1;
}
var ElemName = '';
var ElemKeys = [];
try {
ElemName = (Elem && Elem.constructor && Elem.constructor.name) || '';
ElemKeys = Elem ? Object.keys(Elem) : [];
} catch (E5) {}
T.push({
hid: Inner.__hornetHid, bit: B0, inner: Inner.constructor && Inner.constructor.name,
elem: ElemName, elemKeys: ElemKeys, stack: St,
});
}
} catch (E4) {}
return B0 ? null : Inner.t7h(t);