From bdca6a8fff588a8aa1bb7bcafb8d85c88f683706 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 21 Oct 2026 04:21:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(tools):=20=E4=BF=AE=E6=AD=A3=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E4=BD=8D=E6=B8=B8=E6=A0=87=E5=B0=91=E5=8A=A0=E5=BD=A2?= =?UTF-8?q?=E6=80=81=E4=BD=8D=E7=9A=84=E4=B8=80=E5=A4=84=E5=81=8F=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/tools/analyze_opt_trace.py | 76 ++++++++++++++++++----------- 3DTank/tools/audit_pt_boundaries.js | 4 +- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/3DTank/tools/analyze_opt_trace.py b/3DTank/tools/analyze_opt_trace.py index f58058a..7e3c8a6 100644 --- a/3DTank/tools/analyze_opt_trace.py +++ b/3DTank/tools/analyze_opt_trace.py @@ -1,12 +1,14 @@ -# analyze_opt_trace.py - 分析客户端「全量 opt 读取」打点,按帧还原每个模型消费了几位 +# analyze_opt_trace.py - 还原客户端每帧的 opt 位序列,按「模型片段」汇总 # -# 数据来源:镜像站 module_24635.js 的 Ht.t7h 补丁。每条记录: -# rid = 位读取器实例 id(同一帧共享 → 同 rid 即同帧) -# pos = 读位前的绝对位索引;bit = 读到的位 -# slots = 内层读器槽位名(模型字段);elemKeys = 列表元素读器槽位名 -# 槽位名全局唯一,所以能用 names/codec_fields.json 的 Slots 反查出具体模型。 +# 数据来源:镜像站 module_24635.js 的 Ht.t7h 打点,每条记录: +# rid = 位读取器 id(同一帧共享)、pos = 读位前绝对位索引、bit = 读到的位、 +# slots/elemKeys = 内层读器(或列表元素)的槽位名(全局唯一,可反查模型)。 # -# 用法: python tools/analyze_opt_trace.py [--names names] [--only bs,Tu,tu] +# 输出:把同一帧里「连续属于同一个模型」的读位合并成片段: +# 模型@起始位 × 位数 = 位串 +# 这样可以直观看出某个模型到底消费了几位(例如 Item3DCC 是 4 位还是 5 位)。 +# +# 用法: python tools/analyze_opt_trace.py [--only bs,Tu,tu] [--frames 5] import json import os import sys @@ -28,10 +30,15 @@ TracePath = os.path.join(NamesDir, 'opt_trace.json') FieldsPath = os.path.join(NamesDir, 'codec_fields.json') -def LoadSlotIndex(): +def LoadIndex(): + """槽位集合 → codec 键。用 Fields[].Slot(比 Slots 更全),再补 Slots。""" with open(FieldsPath, 'r', encoding='utf-8') as F: Codecs = json.load(F).get('Codecs') or {} Index = {} + for Key, C in Codecs.items(): + Slots = tuple(sorted({str(F2.get('Slot')) for F2 in (C.get('Fields') or []) if F2.get('Slot')})) + if Slots and Slots not in Index: + Index[Slots] = Key for Key, C in Codecs.items(): Slots = tuple(sorted(S.get('Slot') for S in (C.get('Slots') or []))) if Slots and Slots not in Index: @@ -41,44 +48,55 @@ def LoadSlotIndex(): def Main(): if not os.path.exists(TracePath): - print('缺少 ' + TracePath) - print('请先用镜像站进一局战斗(Ht.t7h 已打全量 opt 打点)。') + print('缺少 ' + TracePath + '(先让镜像站进一局)') return 1 with open(TracePath, 'r', encoding='utf-8') as F: Data = json.load(F) Rows = Data.get('trace') or [] print('opt 打点:%d 条(采集于 %s)' % (len(Rows), Data.get('at', '?'))) - Index = LoadSlotIndex() - Only = [S for S in (Arg('only', '') or '').split(',') if S] + Index = LoadIndex() + Only = [S for S in (Arg('only', 'Tu,tu,bs') or '').split(',') if S] + MaxFrames = int(Arg('frames', '5') or 5) Frames = defaultdict(list) for R in Rows: Frames[R.get('rid')].append(R) - print(' 帧数:%d' % len(Frames)) + def NameOf(R): + Slots = tuple(sorted(R.get('slots') or [])) + Key = Index.get(Slots) + if Key: + return Key.split('::')[-1] + Elem = tuple(sorted(R.get('elemKeys') or [])) + if Elem: + K2 = Index.get(Elem) + return 'list<' + (K2.split('::')[-1] if K2 else '?') + '>' + return '?' + ''.join(Slots) + + def Segments(Seq): + Out = [] + for R in Seq: + N = NameOf(R) + if Out and Out[-1][0] == N and Out[-1][2] == R.get('pos'): + Out[-1][1] += str(R.get('bit') and 1 or 0) + Out[-1][2] = R.get('pos', 0) + 1 + else: + Out.append([N, str(R.get('bit') and 1 or 0), R.get('pos', 0) + 1, R.get('pos', 0)]) + return Out + Shown = 0 for Rid in sorted(Frames, key=lambda X: (X is None, X)): Seq = Frames[Rid] - Items = [] - Names = [] - for R in Seq: - Slots = tuple(sorted(R.get('slots') or [])) - Key = Index.get(Slots) - if not Key: - ElemKeys = tuple(sorted(R.get('elemKeys') or [])) - Key = ('list<' + (Index.get(ElemKeys) or '?') + '>') if ElemKeys else ('?' + str(Slots)) - Short = Key.split('::')[-1] - Names.append(Short) - Items.append('%s@%s=%s' % (Short, R.get('pos'), R.get('bit'))) - if Only and not any(N in Only for N in Names): + Segs = Segments(Seq) + if Only and not any(S[0] in Only for S in Segs): continue Shown += 1 - if Shown > 12: + if Shown > MaxFrames: continue - print(' rid=%s(%d 位):%s' % (Rid, len(Seq), ' '.join(Items))) - if Shown > 12: - print(' …还有 %d 帧匹配' % (Shown - 12)) + Line = ' '.join('%s@%d×%d=%s' % (S[0], S[3], len(S[1]), S[1]) for S in Segs[:40]) + print(' rid=%s(%d 位):%s' % (Rid, len(Seq), Line)) + print(' 匹配帧数:%d' % Shown) return 0 diff --git a/3DTank/tools/audit_pt_boundaries.js b/3DTank/tools/audit_pt_boundaries.js index d8ce3a4..17092e8 100644 --- a/3DTank/tools/audit_pt_boundaries.js +++ b/3DTank/tools/audit_pt_boundaries.js @@ -151,7 +151,9 @@ function AuditPt(Body, Off, File, BitStart, Label) { const Bits = NmSize > 0 && NmSize <= Body.length ? NullmapBits(Body.slice(0, NmSize)) : null; // 位游标由 semantic 的顺序推进给出(BitStart = 本记录形态位之后的绝对位索引), // 不再假设「pt 是帧内第一条记录」——多记录帧里那样会整体错位。 - if (Bits) Reader.AttachBits(Bits, typeof BitStart === 'number' ? BitStart : 1); + // BitStart 是「本记录形态位」的索引(semantic 在消费形态位之前记录), + // 负载里的 opt 位从它的下一位开始 —— 少加这个 1 会让整条记录的位游标整体早一位。 + if (Bits) Reader.AttachBits(Bits, (typeof BitStart === 'number' ? BitStart : 0) + 1); try { for (let I = 0; I < D; I++) { if (At + 8 > Buf.length) return;