cseq 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cseq/html.py ADDED
@@ -0,0 +1,485 @@
1
+ from __future__ import annotations
2
+
3
+ from html import escape
4
+ import json
5
+ from typing import Iterable
6
+
7
+ from .model import CpuEvidenceRecord
8
+ from .runtime import TraceSession
9
+ from .sequence import SequenceModel, build_fold_plan
10
+ from .trace_diff import TraceDiffResult
11
+ from .trace_analysis import MarkerSuggestion, discover_correlations, fit_clock_alignments
12
+
13
+
14
+ def _why_next(model: SequenceModel) -> dict[str, dict[str, list[dict[str, str]]]]:
15
+ functions = {model.entry}
16
+ for m in model.messages:
17
+ functions.add(m.caller); functions.add(m.callee)
18
+ result: dict[str, dict[str, list[dict[str, str]]]] = {}
19
+ for name in sorted(functions):
20
+ why = [
21
+ {"function": m.caller, "label": m.label}
22
+ for m in model.messages if m.callee == name and m.caller != name
23
+ ]
24
+ nxt = [
25
+ {"function": m.callee, "label": m.label}
26
+ for m in model.messages if m.caller == name and m.callee != name
27
+ ]
28
+ result[name] = {"why": why, "next": nxt}
29
+ return result
30
+
31
+
32
+ def _cpu_evidence_rows(cpu_evidence: Iterable[CpuEvidenceRecord]) -> list[dict[str, object]]:
33
+ return [
34
+ {
35
+ "function_id": e.target_function_id,
36
+ "layer": e.layer.value,
37
+ "kind": e.value_kind.value,
38
+ "value": e.value,
39
+ "provenance": e.provenance,
40
+ "explicit_override": e.explicit_override,
41
+ }
42
+ for e in cpu_evidence
43
+ ]
44
+
45
+
46
+ def _runtime_rows(runtime_session: TraceSession | None) -> list[dict[str, object]]:
47
+ if runtime_session is None:
48
+ return []
49
+ return [
50
+ {
51
+ "event_index": e.event_index,
52
+ "marker_id": e.marker_id,
53
+ "timestamp": e.timestamp_raw,
54
+ "cpu": e.cpu_hint,
55
+ "task": e.task_hint,
56
+ "clock_domain": e.clock_domain,
57
+ "message": e.message,
58
+ "payload": e.payload_dict(),
59
+ }
60
+ for e in runtime_session.events
61
+ ]
62
+
63
+
64
+ def build_html_payload(
65
+ model: SequenceModel,
66
+ *,
67
+ runtime_session: TraceSession | None = None,
68
+ cpu_evidence: Iterable[CpuEvidenceRecord] = (),
69
+ explanations: dict[str, list[dict[str, object]]] | None = None,
70
+ trace_diff: TraceDiffResult | None = None,
71
+ marker_suggestions: list[MarkerSuggestion] | None = None,
72
+ viewport_size: int = 200,
73
+ max_sequence_messages: int = 300,
74
+ ) -> dict[str, object]:
75
+ size = max(1, int(viewport_size))
76
+ rows = [
77
+ {
78
+ "caller": m.caller,
79
+ "callee": m.callee,
80
+ "label": m.label,
81
+ "external_as_self": m.external_as_self,
82
+ "control_context": list(m.control_context),
83
+ "recursive": m.recursive,
84
+ }
85
+ for m in model.messages
86
+ ]
87
+ event_count = len(runtime_session.events) if runtime_session else 0
88
+ correlations = discover_correlations(runtime_session.events) if runtime_session else []
89
+ clock_alignments = fit_clock_alignments(runtime_session.events, correlations) if runtime_session else []
90
+ fold_plan = build_fold_plan(model, max_sequence_messages)
91
+ return {
92
+ "entry": model.entry,
93
+ "messages": rows,
94
+ "sequence_regions": [
95
+ {"context": list(r.context), "start_message": r.start_message, "end_message": r.end_message}
96
+ for r in model.regions
97
+ ],
98
+ "sequence_fold": {
99
+ "max_messages": max(1, int(max_sequence_messages)),
100
+ "visible_indices": list(fold_plan.visible_indices),
101
+ "collapsed": [
102
+ {"start_message": x.start_message, "end_message": x.end_message, "hidden_count": x.hidden_count, "reason": x.reason}
103
+ for x in fold_plan.collapsed
104
+ ],
105
+ },
106
+ "why_next": _why_next(model),
107
+ "cpu_evidence": _cpu_evidence_rows(cpu_evidence),
108
+ "explanations": explanations or {},
109
+ "runtime": {
110
+ "session": runtime_session.name if runtime_session else None,
111
+ "events": _runtime_rows(runtime_session),
112
+ "correlations": [
113
+ {
114
+ "left_event_index": r.left_event_index,
115
+ "right_event_index": r.right_event_index,
116
+ "relation_type": r.relation_type.value,
117
+ "keys": list(r.keys),
118
+ "values": list(r.values),
119
+ "score": r.score,
120
+ } for r in correlations
121
+ ],
122
+ "clock_alignments": [
123
+ {
124
+ "source_domain": a.source_domain,
125
+ "target_domain": a.target_domain,
126
+ "scale": a.scale,
127
+ "offset_seconds": a.offset_seconds,
128
+ "anchor_count": a.anchor_count,
129
+ "max_abs_residual_seconds": a.max_abs_residual_seconds,
130
+ "confidence": a.confidence,
131
+ } for a in clock_alignments
132
+ ],
133
+ "tiles": [
134
+ {
135
+ "tile_index": tile_index,
136
+ "start_offset": tile_start,
137
+ "end_offset": tile_start + len(chunk) - 1,
138
+ "start_event_index": chunk[0].event_index,
139
+ "end_event_index": chunk[-1].event_index,
140
+ "cpus": sorted({e.cpu_hint for e in chunk if e.cpu_hint}),
141
+ "tasks": sorted({e.task_hint for e in chunk if e.task_hint}),
142
+ "marker_count": len({e.marker_id for e in chunk}),
143
+ }
144
+ for tile_index, tile_start in enumerate(range(0, event_count, size))
145
+ for chunk in [runtime_session.events[tile_start:tile_start + size]]
146
+ ] if runtime_session else [],
147
+ },
148
+ "trace_diff": None if trace_diff is None else {
149
+ "left_session": trace_diff.left_session,
150
+ "right_session": trace_diff.right_session,
151
+ "added_markers": list(trace_diff.added_markers),
152
+ "missing_markers": list(trace_diff.missing_markers),
153
+ "changed_cpu": list(trace_diff.changed_cpu),
154
+ "changed_task": list(trace_diff.changed_task),
155
+ "order_changes": list(trace_diff.order_changes),
156
+ "timing_changes": list(trace_diff.timing_changes),
157
+ },
158
+ "lod": "summary" if event_count > 10000 else ("compact" if event_count > 1000 else "detail"),
159
+ "marker_suggestions": [
160
+ {
161
+ "function_name": x.function_name,
162
+ "score": x.information_gain_score,
163
+ "distinguished_path_pairs": x.distinguished_path_pairs,
164
+ "total_path_pairs": x.total_path_pairs,
165
+ "reason": x.reason,
166
+ } for x in (marker_suggestions or [])
167
+ ],
168
+ "viewport_size": size,
169
+ }
170
+
171
+
172
+ def render_html(
173
+ model: SequenceModel,
174
+ *,
175
+ title: str = "cseq sequence",
176
+ runtime_session: TraceSession | None = None,
177
+ cpu_evidence: Iterable[CpuEvidenceRecord] = (),
178
+ explanations: dict[str, list[dict[str, object]]] | None = None,
179
+ trace_diff: TraceDiffResult | None = None,
180
+ marker_suggestions: list[MarkerSuggestion] | None = None,
181
+ viewport_size: int = 200,
182
+ max_sequence_messages: int = 300,
183
+ ) -> str:
184
+ payload = build_html_payload(
185
+ model, runtime_session=runtime_session, cpu_evidence=cpu_evidence,
186
+ explanations=explanations, trace_diff=trace_diff,
187
+ marker_suggestions=marker_suggestions, viewport_size=viewport_size,
188
+ max_sequence_messages=max_sequence_messages,
189
+ )
190
+ data = json.dumps(payload, ensure_ascii=False).replace("</", "<\\/")
191
+ safe_title = escape(title)
192
+ return f'''<!doctype html>
193
+ <html lang="ja">
194
+ <head>
195
+ <meta charset="utf-8">
196
+ <meta name="viewport" content="width=device-width,initial-scale=1">
197
+ <title>{safe_title}</title>
198
+ <style>
199
+ :root {{ color-scheme: light dark; font-family: system-ui, sans-serif; }}
200
+ body {{ margin:0; }}
201
+ header {{ position:sticky; top:0; z-index:5; padding:12px; border-bottom:1px solid #8886; background:Canvas; }}
202
+ main {{ padding:12px; display:grid; gap:16px; }}
203
+ input,select {{ padding:8px; max-width:42rem; }}
204
+ .panel {{ border:1px solid #8885; border-radius:8px; overflow:hidden; }}
205
+ .panel h2 {{ margin:0; padding:10px 12px; font-size:1rem; border-bottom:1px solid #8884; }}
206
+ .panel-body {{ padding:10px 12px; }}
207
+ .message {{ display:grid; grid-template-columns:minmax(8rem,1fr) 2rem minmax(8rem,1fr) 3fr; gap:8px; padding:7px 4px; border-bottom:1px solid #8883; align-items:start; }}
208
+ .external {{ opacity:.78; }}
209
+ .recursive {{ font-weight:600; }}
210
+ .fold-summary {{ padding:7px 4px; border-bottom:1px dashed #8885; }}
211
+ .fold-summary summary {{ cursor:pointer; }}
212
+ .context {{ font-size:.78em; opacity:.65; grid-column:1 / -1; }}
213
+ .meta {{ font-size:.85em; opacity:.7; }}
214
+ .hidden {{ display:none !important; }}
215
+ .timeline-row {{ display:grid; grid-template-columns:5rem 9rem 10rem 10rem 1fr; gap:8px; padding:6px 0; border-bottom:1px solid #8883; }}
216
+ .lanes {{ display:flex; flex-wrap:wrap; gap:8px; }}
217
+ .lane {{ border:1px solid #8885; border-radius:6px; padding:7px 9px; min-width:9rem; }}
218
+ .why-next {{ display:grid; grid-template-columns:1fr 1fr; gap:12px; }}
219
+ .tile-summary {{ font-size:.85em; opacity:.8; margin:.4rem 0; }}
220
+ .timeline-canvas {{ width:100%; height:360px; border:1px solid #8883; border-radius:6px; display:block; }}
221
+ .path-controls {{ display:flex; flex-wrap:wrap; gap:8px; align-items:center; }}
222
+ .search-status {{ font-size:.85em; opacity:.8; margin-left:.5rem; }}
223
+ ul {{ margin:.4rem 0; padding-left:1.2rem; }}
224
+ @media (max-width:720px) {{
225
+ .message,.timeline-row {{ grid-template-columns:1fr; }}
226
+ .arrow {{ display:none; }}
227
+ .why-next {{ grid-template-columns:1fr; }}
228
+ }}
229
+ </style>
230
+ </head>
231
+ <body>
232
+ <header>
233
+ <strong>{safe_title}</strong>
234
+ <div class="meta">entry: <span id="entry"></span> / messages: <span id="count"></span> / runtime: <span id="runtime-count"></span></div>
235
+ <input id="search" type="search" placeholder="function / call / runtime text を検索" autocomplete="off"><span id="search-status" class="search-status"></span>
236
+ </header>
237
+ <main>
238
+ <section class="panel"><h2>Sequence</h2><div class="panel-body" id="messages"></div></section>
239
+ <section class="panel"><h2>CPU / Task</h2><div class="panel-body"><div id="lanes" class="lanes"></div><div id="cpu-evidence"></div></div></section>
240
+ <section class="panel"><h2>Runtime Timeline</h2><div class="panel-body"><div id="timeline-controls"></div><canvas id="timeline-canvas" class="timeline-canvas hidden" width="1200" height="360"></canvas><div id="timeline"></div></div></section>
241
+ <section class="panel"><h2>Runtime Correlation / Clock</h2><div class="panel-body" id="runtime-correlation"></div></section>
242
+ <section class="panel"><h2>Trace Diff</h2><div class="panel-body" id="trace-diff"></div></section>
243
+ <section class="panel"><h2>Marker Suggest</h2><div class="panel-body" id="marker-suggest"></div></section>
244
+ <section class="panel"><h2>Why / Next</h2><div class="panel-body"><select id="why-function"></select><div id="why-next" class="why-next"></div></div></section>
245
+ <section class="panel"><h2>Path Query</h2><div class="panel-body"><div class="path-controls"><select id="path-start"></select><span>→</span><select id="path-end"></select><button id="path-run">検索</button></div><pre id="path-result"></pre></div></section>
246
+ </main>
247
+ <script type="application/json" id="cseq-data">{data}</script>
248
+ <script>
249
+ const data = JSON.parse(document.getElementById('cseq-data').textContent);
250
+ const searchable = [];
251
+ document.getElementById('entry').textContent = data.entry;
252
+ document.getElementById('count').textContent = data.messages.length;
253
+ document.getElementById('runtime-count').textContent = data.runtime.events.length;
254
+ document.querySelector('header .meta').append(` / LOD: ${{data.lod}}`);
255
+
256
+ const root = document.getElementById('messages');
257
+ function makeMessageRow(m) {{
258
+ const row = document.createElement('div');
259
+ row.className = 'message' + (m.external_as_self ? ' external' : '') + (m.recursive ? ' recursive' : '');
260
+ row.dataset.search = `${{m.caller}} ${{m.callee}} ${{m.label}} ${{(m.control_context||[]).join(' ')}}`.toLowerCase();
261
+ for (const [cls, value] of [['caller',m.caller],['arrow','→'],['callee',m.callee],['label',m.label + (m.recursive ? ' [recursive]' : '')]]) {{
262
+ const el = document.createElement('div'); el.className = cls; el.textContent = value; row.appendChild(el);
263
+ }}
264
+ if ((m.control_context||[]).length) {{
265
+ const ctx=document.createElement('div');ctx.className='context';ctx.textContent=(m.control_context||[]).join(' / ');row.appendChild(ctx);
266
+ }}
267
+ return row;
268
+ }}
269
+ const visibleSet=new Set(data.sequence_fold.visible_indices||[]);
270
+ const collapsedByStart=new Map((data.sequence_fold.collapsed||[]).map(x=>[x.start_message,x]));
271
+ for (let i=0;i<data.messages.length;) {{
272
+ if (visibleSet.has(i)) {{
273
+ const row=makeMessageRow(data.messages[i]);root.appendChild(row);searchable.push(row);i++;continue;
274
+ }}
275
+ const fold=collapsedByStart.get(i);
276
+ if (!fold) {{ i++; continue; }}
277
+ const details=document.createElement('details');details.className='fold-summary';
278
+ const summary=document.createElement('summary');summary.textContent=`${{fold.hidden_count}} messages collapsed (#${{fold.start_message+1}}-${{fold.end_message+1}})`;details.appendChild(summary);
279
+ let loaded=false;
280
+ details.addEventListener('toggle',()=>{{
281
+ if(!details.open||loaded)return;loaded=true;
282
+ for(let j=fold.start_message;j<=fold.end_message;j++){{const row=makeMessageRow(data.messages[j]);details.appendChild(row);searchable.push(row);}}
283
+ }});
284
+ root.appendChild(details);i=fold.end_message+1;
285
+ }}
286
+
287
+ const laneRoot = document.getElementById('lanes');
288
+ const laneMap = new Map();
289
+ for (const e of data.runtime.events) {{
290
+ const cpu = e.cpu || 'CPU?'; const task = e.task || 'Task?'; const key = `${{cpu}} / ${{task}}`;
291
+ laneMap.set(key, (laneMap.get(key) || 0) + 1);
292
+ }}
293
+ for (const [key,count] of laneMap) {{
294
+ const el = document.createElement('div'); el.className='lane'; el.textContent=`${{key}} (${{count}} events)`; laneRoot.appendChild(el);
295
+ }}
296
+ if (!laneMap.size) laneRoot.textContent = 'Runtime eventなし';
297
+ const evRoot = document.getElementById('cpu-evidence');
298
+ if (data.cpu_evidence.length) {{
299
+ const ul=document.createElement('ul');
300
+ for (const e of data.cpu_evidence) {{ const li=document.createElement('li'); li.textContent=`${{e.layer}}: ${{e.value}} (${{e.provenance}})`; ul.appendChild(li); }}
301
+ evRoot.appendChild(ul);
302
+ }}
303
+
304
+ const timeline = document.getElementById('timeline');
305
+ const timelineCanvas = document.getElementById('timeline-canvas');
306
+ const timelineControls = document.getElementById('timeline-controls');
307
+ let timelineOffset = 0;
308
+ function renderTimelineCanvas(events, startOffset) {{
309
+ timelineCanvas.classList.remove('hidden');
310
+ timeline.classList.add('hidden');
311
+ const ctx=timelineCanvas.getContext('2d');
312
+ const cssWidth=Math.max(600,timelineCanvas.clientWidth||1200); const dpr=window.devicePixelRatio||1;
313
+ timelineCanvas.width=Math.floor(cssWidth*dpr); timelineCanvas.height=Math.floor(360*dpr);
314
+ ctx.setTransform(dpr,0,0,dpr,0,0); ctx.clearRect(0,0,cssWidth,360);
315
+ ctx.font='12px system-ui'; ctx.textBaseline='middle';
316
+ const rowH=Math.max(3,Math.min(18,330/Math.max(1,events.length)));
317
+ const cpuNames=[...new Set(events.map(e=>e.cpu||'CPU?'))];
318
+ const cpuX=new Map(cpuNames.map((x,i)=>[x,80+i*Math.max(90,(cssWidth-100)/Math.max(1,cpuNames.length))]));
319
+ ctx.fillText(`LOD=${{data.lod}} / ${{events.length}} events`,8,14);
320
+ for(let i=0;i<events.length;i++){{
321
+ const e=events[i]; const y=28+i*rowH; const x=cpuX.get(e.cpu||'CPU?')||80;
322
+ ctx.fillRect(x,y,Math.max(2,Math.min(14,rowH)),Math.max(2,rowH-1));
323
+ if(data.lod==='compact' && rowH>=8 && i%Math.max(1,Math.floor(events.length/25))===0) ctx.fillText(`#${{e.event_index}} ${{e.marker_id}}`,x+18,y+rowH/2);
324
+ }}
325
+ for(const [cpu,x] of cpuX) ctx.fillText(cpu,x,350);
326
+ }}
327
+ function renderTimeline() {{
328
+ timeline.replaceChildren();
329
+ // Remove previously registered timeline rows from the search set.
330
+ for (let i=searchable.length-1; i>=0; --i) if (searchable[i].classList?.contains('timeline-row')) searchable.splice(i,1);
331
+ if (!data.runtime.events.length) {{ timelineCanvas.classList.add('hidden'); timeline.classList.remove('hidden'); timeline.textContent='Runtime eventなし'; timelineControls.textContent=''; return; }}
332
+ const size = data.viewport_size || 200;
333
+ const end = Math.min(data.runtime.events.length, timelineOffset + size);
334
+ const visibleEvents=data.runtime.events.slice(timelineOffset,end);
335
+ if(data.lod==='detail') {{
336
+ timelineCanvas.classList.add('hidden'); timeline.classList.remove('hidden');
337
+ for (const e of visibleEvents) {{
338
+ const row=document.createElement('div'); row.className='timeline-row';
339
+ row.dataset.search = `${{e.marker_id}} ${{e.cpu||''}} ${{e.task||''}} ${{e.message||''}}`.toLowerCase();
340
+ for (const v of [String(e.event_index), e.timestamp||'', e.cpu||'?', e.task||'?', `${{e.marker_id}} ${{e.message||''}}`]) {{
341
+ const el=document.createElement('div'); el.textContent=v; row.appendChild(el);
342
+ }}
343
+ timeline.appendChild(row); searchable.push(row);
344
+ }}
345
+ }} else {{
346
+ renderTimelineCanvas(visibleEvents,timelineOffset);
347
+ }}
348
+ timelineControls.replaceChildren();
349
+ const prev=document.createElement('button'); prev.textContent='前'; prev.disabled=timelineOffset===0;
350
+ prev.onclick=()=>{{ timelineOffset=Math.max(0,timelineOffset-size); renderTimeline(); }};
351
+ const next=document.createElement('button'); next.textContent='次'; next.disabled=end>=data.runtime.events.length;
352
+ next.onclick=()=>{{ timelineOffset=end; renderTimeline(); }};
353
+ const label=document.createElement('span'); label.textContent=` ${{timelineOffset+1}}-${{end}} / ${{data.runtime.events.length}} `;
354
+ const tile=document.createElement('select'); tile.title='Viewport Tile';
355
+ for (const t of (data.runtime.tiles||[])) {{
356
+ const o=document.createElement('option'); o.value=String(t.start_offset);
357
+ o.textContent=`Tile ${{t.tile_index+1}}: #${{t.start_event_index}}-${{t.end_event_index}}`;
358
+ if (timelineOffset>=t.start_offset && timelineOffset<=t.end_offset) o.selected=true;
359
+ tile.appendChild(o);
360
+ }}
361
+ tile.onchange=()=>{{ timelineOffset=Number(tile.value)||0; renderTimeline(); }};
362
+ timelineControls.append(prev,label,next,tile);
363
+ const active=(data.runtime.tiles||[]).find(t=>timelineOffset>=t.start_offset && timelineOffset<=t.end_offset);
364
+ if (active) {{
365
+ const meta=document.createElement('span'); meta.className='tile-summary';
366
+ meta.textContent=` CPU=${{active.cpus.join('|')||'?'}} Task=${{active.tasks.join('|')||'?'}} Markers=${{active.marker_count}}`;
367
+ timelineControls.append(meta);
368
+ }}
369
+ }}
370
+ renderTimeline();
371
+
372
+ const correlationRoot=document.getElementById('runtime-correlation');
373
+ if (!data.runtime.correlations.length) correlationRoot.textContent='Correlationなし';
374
+ else {{
375
+ const exact=data.runtime.correlations.filter(x=>x.relation_type==='CORRELATED_EXACT');
376
+ const possible=data.runtime.correlations.filter(x=>x.relation_type==='CORRELATED_POSSIBLE');
377
+ const summary=document.createElement('div'); summary.textContent=`Exact=${{exact.length}} / Possible=${{possible.length}}`; correlationRoot.appendChild(summary);
378
+ for (const r of data.runtime.correlations.slice(0,100)) {{
379
+ const row=document.createElement('div'); row.className='meta';
380
+ row.textContent=`#${{r.left_event_index}} ↔ #${{r.right_event_index}} ${{r.relation_type}} [${{r.keys.join(', ')}}] score=${{r.score.toFixed(3)}}`;
381
+ correlationRoot.appendChild(row);
382
+ }}
383
+ if (data.runtime.correlations.length>100) {{ const more=document.createElement('div'); more.textContent=`… ${{data.runtime.correlations.length-100}} more`; correlationRoot.appendChild(more); }}
384
+ }}
385
+ if (data.runtime.clock_alignments.length) {{
386
+ const h=document.createElement('h3'); h.textContent='Clock alignment'; correlationRoot.appendChild(h);
387
+ for (const a of data.runtime.clock_alignments) {{
388
+ const row=document.createElement('div'); row.className='meta';
389
+ row.textContent=`${{a.source_domain}} → ${{a.target_domain}}: t'=${{a.scale.toFixed(9)}}t ${{a.offset_seconds>=0?'+':'-'}} ${{Math.abs(a.offset_seconds).toFixed(6)}}s / anchors=${{a.anchor_count}} / ${{a.confidence}}`;
390
+ correlationRoot.appendChild(row);
391
+ }}
392
+ }}
393
+
394
+ const diffRoot=document.getElementById('trace-diff');
395
+ if (!data.trace_diff) diffRoot.textContent='比較Sessionなし';
396
+ else {{
397
+ const d=data.trace_diff; const h=document.createElement('div'); h.textContent=`${{d.left_session}} → ${{d.right_session}}`; diffRoot.appendChild(h);
398
+ for (const [title,items] of [['追加Marker',d.added_markers],['欠落Marker',d.missing_markers]]) {{
399
+ const p=document.createElement('div'); p.textContent=`${{title}}: ` + (items.length ? items.map(x=>`${{x[0]}} ×${{x[1]}}`).join(', ') : 'なし'); diffRoot.appendChild(p);
400
+ }}
401
+ for (const [title,items] of [['CPU変化',d.changed_cpu],['Task変化',d.changed_task]]) {{
402
+ const p=document.createElement('div'); p.textContent=`${{title}}: ` + (items.length ? items.map(x=>`${{x[0]}}: ${{x[1].join('|')||'?'}} → ${{x[2].join('|')||'?'}}`).join(', ') : 'なし'); diffRoot.appendChild(p);
403
+ }}
404
+
405
+ for (const [title,items] of [['順序変化',d.order_changes],['時刻変化',d.timing_changes]]) {{
406
+ const p=document.createElement('div'); p.textContent=`${{title}}: ` + (items.length ? items.map(x=>`${{x[0]}}: ${{x[1]}} → ${{x[2]}}`).join(', ') : 'なし'); diffRoot.appendChild(p);
407
+ }}
408
+ }}
409
+
410
+ const suggestRoot=document.getElementById('marker-suggest');
411
+ if (!data.marker_suggestions.length) suggestRoot.textContent='Marker候補なし';
412
+ else {{
413
+ for (const s of data.marker_suggestions) {{
414
+ const d=document.createElement('details'); const summary=document.createElement('summary'); summary.textContent=`${{s.function_name}} score=${{s.score.toFixed(3)}}`; d.appendChild(summary);
415
+ const p=document.createElement('div'); p.textContent=s.reason; d.appendChild(p); suggestRoot.appendChild(d);
416
+ }}
417
+ }}
418
+
419
+ const select=document.getElementById('why-function');
420
+ for (const name of Object.keys(data.why_next)) {{ const o=document.createElement('option'); o.value=name; o.textContent=name; select.appendChild(o); }}
421
+ function renderWhyNext() {{
422
+ const name=select.value; const x=data.why_next[name] || {{why:[],next:[]}}; const box=document.getElementById('why-next'); box.replaceChildren();
423
+ for (const [title,items] of [['Why',x.why],['Next',x.next]]) {{
424
+ const part=document.createElement('div'); const h=document.createElement('h3'); h.textContent=title; part.appendChild(h); const ul=document.createElement('ul');
425
+ if (!items.length) {{ const li=document.createElement('li'); li.textContent='なし'; ul.appendChild(li); }}
426
+ for (const item of items) {{ const li=document.createElement('li'); li.textContent=`${{item.function}} — ${{item.label}}`; ul.appendChild(li); }}
427
+ part.appendChild(ul); box.appendChild(part);
428
+ }}
429
+ const evidence=document.createElement('div'); evidence.style.gridColumn='1 / -1';
430
+ const h=document.createElement('h3'); h.textContent='Resolution Evidence'; evidence.appendChild(h);
431
+ const calls=data.explanations[name] || [];
432
+ if (!calls.length) {{ const p=document.createElement('div'); p.textContent='呼び出しEvidenceなし'; evidence.appendChild(p); }}
433
+ for (const c of calls) {{
434
+ const d=document.createElement('details'); const summary=document.createElement('summary'); summary.textContent=`${{c.raw}} — ${{c.resolution}}`; d.appendChild(summary);
435
+ const ul=document.createElement('ul');
436
+ for (const t of (c.targets||[])) {{ const li=document.createElement('li'); li.textContent=`target: ${{t.name||t.function_id}}`; ul.appendChild(li); }}
437
+ for (const f of (c.value_flow||[])) {{ const li=document.createElement('li'); li.textContent=`flow: ${{f.kind}} ${{f.from}} → ${{f.to}}`; ul.appendChild(li); }}
438
+ for (const e of (c.cpu_evidence||[])) {{ const li=document.createElement('li'); li.textContent=`cpu: ${{e.layer}} ${{e.value}} (${{e.provenance}})`; ul.appendChild(li); }}
439
+ for (const e of (c.linker_evidence||[])) {{ const li=document.createElement('li'); li.textContent=`linker: ${{e.symbol}} @0x${{Number(e.address).toString(16)}} ${{e.object_file||''}}`; ul.appendChild(li); }}
440
+ for (const e of (c.binary_evidence||[])) {{ const li=document.createElement('li'); li.textContent=`binary: ${{e.symbol}} @0x${{Number(e.address).toString(16)}} size=${{e.size}}`; ul.appendChild(li); }}
441
+ for (const e of (c.dwarf_evidence||[]).slice(0,20)) {{ const li=document.createElement('li'); li.textContent=`dwarf: ${{e.source}}:${{e.line}} @0x${{Number(e.address).toString(16)}}`; ul.appendChild(li); }}
442
+ if (c.unknown_possible) {{ const li=document.createElement('li'); li.textContent='unknown target may also exist'; ul.appendChild(li); }}
443
+ d.appendChild(ul); evidence.appendChild(d);
444
+ }}
445
+ box.appendChild(evidence);
446
+ }}
447
+ select.addEventListener('change',renderWhyNext); renderWhyNext();
448
+
449
+ const searchInput=document.getElementById('search');
450
+ const searchStatus=document.getElementById('search-status');
451
+ let searchWorker=null;
452
+ if (window.Worker && window.Blob) {{
453
+ const workerCode=`let runtime=[];let messages=[];function paths(start,end,maxPaths=8,maxDepth=64){{const g=new Map();for(const m of messages){{if(m.external_as_self)continue;if(!g.has(m.caller))g.set(m.caller,new Set());g.get(m.caller).add(m.callee);}}const q=[[start,[start]]],out=[];let shortest=null;while(q.length&&out.length<maxPaths){{const [node,path]=q.shift();if(shortest!==null&&path.length>shortest)continue;if(path.length>maxDepth)continue;if(node===end){{out.push(path);shortest=shortest??path.length;continue;}}for(const n of [...(g.get(node)||[])].sort()){{if(!path.includes(n))q.push([n,[...path,n]]);}}}}return out;}}onmessage=e=>{{const m=e.data||{{}};if(m.type==='init'){{runtime=m.runtime||[];messages=m.messages||[];return;}}if(m.type==='search'){{const q=(m.q||'').toLowerCase();const hits=[];if(q){{for(const e of runtime){{const s=(String(e.marker_id||'')+' '+String(e.cpu||'')+' '+String(e.task||'')+' '+String(e.message||'')).toLowerCase();if(s.includes(q)){{hits.push(e.event_index);if(hits.length>=200)break;}}}}}}postMessage({{type:'result',q,hits}});return;}}if(m.type==='paths'){{postMessage({{type:'paths',start:m.start,end:m.end,paths:paths(m.start,m.end)}});}}}}`;
454
+ searchWorker=new Worker(URL.createObjectURL(new Blob([workerCode],{{type:'text/javascript'}})));
455
+ searchWorker.postMessage({{type:'init',runtime:data.runtime.events,messages:data.messages}});
456
+ searchWorker.onmessage=e=>{{
457
+ const m=e.data||{{}};
458
+ if(m.type==='result'){{
459
+ searchStatus.textContent=m.q ? `runtime hits: ${{m.hits.length}}${{m.hits.length>=200?'+':''}}` : '';
460
+ if(m.hits.length){{ const idx=data.runtime.events.findIndex(x=>x.event_index===m.hits[0]); if(idx>=0){{ timelineOffset=Math.floor(idx/(data.viewport_size||200))*(data.viewport_size||200); renderTimeline(); }} }}
461
+ }} else if(m.type==='paths') {{
462
+ const box=document.getElementById('path-result'); box.textContent=m.paths.length ? m.paths.map(p=>p.join(' → ')).join('\\n') : '経路なし';
463
+ }}
464
+ }};
465
+ }}
466
+
467
+ const pathStart=document.getElementById('path-start');
468
+ const pathEnd=document.getElementById('path-end');
469
+ const pathNames=[...new Set([data.entry,...data.messages.flatMap(m=>[m.caller,m.callee])])].sort();
470
+ for(const name of pathNames){{for(const el of [pathStart,pathEnd]){{const o=document.createElement('option');o.value=name;o.textContent=name;el.appendChild(o);}}}}
471
+ pathStart.value=data.entry;if(pathNames.length)pathEnd.value=pathNames[pathNames.length-1];
472
+ document.getElementById('path-run').onclick=()=>{{
473
+ const box=document.getElementById('path-result');
474
+ if(searchWorker){{box.textContent='検索中…';searchWorker.postMessage({{type:'paths',start:pathStart.value,end:pathEnd.value}});}}
475
+ else box.textContent='Web Workerを利用できません';
476
+ }};
477
+ searchInput.addEventListener('input', e => {{
478
+ const q=e.target.value.toLowerCase().trim();
479
+ for (const row of searchable) row.classList.toggle('hidden', q && !row.dataset.search.includes(q));
480
+ if(searchWorker) searchWorker.postMessage({{type:'search',q}}); else searchStatus.textContent='';
481
+ }});
482
+ </script>
483
+ </body>
484
+ </html>
485
+ '''