feat(protocol): 位痕迹按 codec 槽位指纹定位字段
This commit is contained in:
1 parent
97917478a4
commit
e0542b2681
2 files changed
+69
-77
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user