feat(protocol): 采集客户端 opt<list> 位痕迹并加分析脚本
This commit is contained in:
1 parent
6a47e684aa
commit
d416ee70ad
2 files changed
+156
No files matched your search
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user