diff --git a/3DTank/tools/analyze_nullmap_trace.py b/3DTank/tools/analyze_nullmap_trace.py new file mode 100644 index 0000000..e2e4214 --- /dev/null +++ b/3DTank/tools/analyze_nullmap_trace.py @@ -0,0 +1,119 @@ +# analyze_nullmap_trace.py - 把客户端采到的「可选 + 列表」位痕迹映射回 模块::类.方法 +# +# 数据来源:镜像站 module_24635.js 的 Ht.t7h 补丁(可选读器)。它只对「内层读器是列表」的 +# 可选字段记录一条 {bit, inner, stack},节流 POST 到 serve.js 的 /__dump__/nullmap_trace.json。 +# 目的:确认 opt> 在真实数据里到底占不占 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()) diff --git a/3dtank.com/play/static/js/unpacked/module_24635.js b/3dtank.com/play/static/js/unpacked/module_24635.js index 92631e7..727dfa2 100644 --- a/3dtank.com/play/static/js/unpacked/module_24635.js +++ b/3dtank.com/play/static/js/unpacked/module_24635.js @@ -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> 在真实数据里到底占不占位。 + // 位只读一次:采样失败也只是少一条日志,绝不改变解码结果。 + 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) {