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/__init__.py +9 -0
- cseq/acceptance.py +162 -0
- cseq/api.py +328 -0
- cseq/binary.py +122 -0
- cseq/cache.py +420 -0
- cseq/cli.py +895 -0
- cseq/compile_db.py +98 -0
- cseq/config.py +138 -0
- cseq/cpu_rules.py +48 -0
- cseq/dependencies.py +202 -0
- cseq/docs.py +21 -0
- cseq/dwarf.py +115 -0
- cseq/event_store.py +378 -0
- cseq/explain.py +113 -0
- cseq/fast_parser.py +286 -0
- cseq/html.py +485 -0
- cseq/html_bundle.py +464 -0
- cseq/hybrid_parser.py +120 -0
- cseq/linker.py +117 -0
- cseq/marker.py +294 -0
- cseq/model.py +367 -0
- cseq/parser.py +797 -0
- cseq/parser_contract_cases.json +69 -0
- cseq/parser_dependency_lock.json +59 -0
- cseq/parser_environment.py +186 -0
- cseq/parser_migration.py +126 -0
- cseq/plugin.py +216 -0
- cseq/project.py +305 -0
- cseq/query.py +62 -0
- cseq/resources/README.md +171 -0
- cseq/resources/design.md +6197 -0
- cseq/resources/verification_report.html +26 -0
- cseq/runtime.py +177 -0
- cseq/runtime_address.py +79 -0
- cseq/runtime_cpu.py +87 -0
- cseq/scanner.py +54 -0
- cseq/sequence.py +281 -0
- cseq/server.py +104 -0
- cseq/source_index.py +227 -0
- cseq/static_store.py +280 -0
- cseq/trace_analysis.py +336 -0
- cseq/trace_diff.py +56 -0
- cseq/tree_sitter_parser.py +468 -0
- cseq/valueflow.py +179 -0
- cseq-0.0.1.dist-info/METADATA +181 -0
- cseq-0.0.1.dist-info/RECORD +49 -0
- cseq-0.0.1.dist-info/WHEEL +5 -0
- cseq-0.0.1.dist-info/entry_points.txt +2 -0
- cseq-0.0.1.dist-info/top_level.txt +1 -0
cseq/event_store.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
import json
|
|
5
|
+
import sqlite3
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from .runtime import RuntimeEvent, TraceSession
|
|
10
|
+
|
|
11
|
+
SCHEMA_VERSION = 4
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventStore:
|
|
15
|
+
"""Small SQLite-backed runtime event store.
|
|
16
|
+
|
|
17
|
+
This is the first persistent Event Store layer. It preserves ingest order and
|
|
18
|
+
indexes marker/cpu/task for later viewport/query work. It does not infer a
|
|
19
|
+
cross-CPU total order.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, path: str | Path):
|
|
23
|
+
self.path = Path(path)
|
|
24
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
self.db = sqlite3.connect(self.path)
|
|
26
|
+
self.db.row_factory = sqlite3.Row
|
|
27
|
+
self._init_schema()
|
|
28
|
+
|
|
29
|
+
def _init_schema(self) -> None:
|
|
30
|
+
self.db.executescript(
|
|
31
|
+
"""
|
|
32
|
+
CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
33
|
+
CREATE TABLE IF NOT EXISTS events(
|
|
34
|
+
session TEXT NOT NULL,
|
|
35
|
+
event_index INTEGER NOT NULL,
|
|
36
|
+
marker_id TEXT NOT NULL,
|
|
37
|
+
raw_line TEXT NOT NULL,
|
|
38
|
+
timestamp_raw TEXT,
|
|
39
|
+
cpu_hint TEXT,
|
|
40
|
+
task_hint TEXT,
|
|
41
|
+
clock_domain TEXT,
|
|
42
|
+
message TEXT,
|
|
43
|
+
payload_json TEXT NOT NULL,
|
|
44
|
+
PRIMARY KEY(session, event_index)
|
|
45
|
+
);
|
|
46
|
+
CREATE TABLE IF NOT EXISTS sessions(
|
|
47
|
+
session TEXT PRIMARY KEY,
|
|
48
|
+
status TEXT NOT NULL,
|
|
49
|
+
event_count INTEGER NOT NULL DEFAULT 0
|
|
50
|
+
);
|
|
51
|
+
CREATE TABLE IF NOT EXISTS event_chunks(
|
|
52
|
+
session TEXT NOT NULL,
|
|
53
|
+
chunk_index INTEGER NOT NULL,
|
|
54
|
+
start_event_index INTEGER NOT NULL,
|
|
55
|
+
end_event_index INTEGER NOT NULL,
|
|
56
|
+
min_timestamp_raw TEXT,
|
|
57
|
+
max_timestamp_raw TEXT,
|
|
58
|
+
cpu_json TEXT NOT NULL,
|
|
59
|
+
task_json TEXT NOT NULL,
|
|
60
|
+
PRIMARY KEY(session, chunk_index)
|
|
61
|
+
);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_range ON event_chunks(session,start_event_index,end_event_index);
|
|
63
|
+
"""
|
|
64
|
+
)
|
|
65
|
+
self.db.execute("INSERT OR REPLACE INTO meta(key,value) VALUES('schema_version',?)", (str(SCHEMA_VERSION),))
|
|
66
|
+
importing = self.db.execute("SELECT COUNT(*) FROM sessions WHERE status='IMPORTING'").fetchone()[0]
|
|
67
|
+
if not importing:
|
|
68
|
+
self._create_secondary_indexes()
|
|
69
|
+
self.db.commit()
|
|
70
|
+
|
|
71
|
+
def _drop_secondary_indexes(self) -> None:
|
|
72
|
+
self.db.executescript(
|
|
73
|
+
"""
|
|
74
|
+
DROP INDEX IF EXISTS idx_events_marker;
|
|
75
|
+
DROP INDEX IF EXISTS idx_events_cpu;
|
|
76
|
+
DROP INDEX IF EXISTS idx_events_task;
|
|
77
|
+
"""
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def _create_secondary_indexes(self) -> None:
|
|
81
|
+
self.db.executescript(
|
|
82
|
+
"""
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_events_marker ON events(session, marker_id, event_index);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_events_cpu ON events(session, cpu_hint, event_index);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_events_task ON events(session, task_hint, event_index);
|
|
86
|
+
"""
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def close(self) -> None:
|
|
90
|
+
self.db.close()
|
|
91
|
+
|
|
92
|
+
def __enter__(self) -> "EventStore":
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def __exit__(self, *_exc) -> None:
|
|
96
|
+
self.close()
|
|
97
|
+
|
|
98
|
+
def replace_session(self, session: TraceSession, *, chunk_size: int = 1000, batch_size: int = 10000) -> int:
|
|
99
|
+
return self.replace_session_events(
|
|
100
|
+
session.name, session.events, chunk_size=chunk_size, batch_size=batch_size
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def replace_session_events(
|
|
104
|
+
self,
|
|
105
|
+
session: str,
|
|
106
|
+
events: Iterable[RuntimeEvent],
|
|
107
|
+
*,
|
|
108
|
+
chunk_size: int = 1000,
|
|
109
|
+
batch_size: int = 10000,
|
|
110
|
+
bulk_rebuild_indexes: bool = False,
|
|
111
|
+
) -> int:
|
|
112
|
+
"""Replace one session from an iterable without materializing all events.
|
|
113
|
+
|
|
114
|
+
This is the large-runtime ingest path. Rows are inserted in bounded batches
|
|
115
|
+
while chunk metadata is accumulated independently. The caller may pass a
|
|
116
|
+
generator for multi-million-event imports.
|
|
117
|
+
"""
|
|
118
|
+
chunk_size = max(1, int(chunk_size))
|
|
119
|
+
batch_size = max(1, int(batch_size))
|
|
120
|
+
count = 0
|
|
121
|
+
batch: list[tuple[object, ...]] = []
|
|
122
|
+
chunk_index = 0
|
|
123
|
+
chunk_count = 0
|
|
124
|
+
chunk_start_index: int | None = None
|
|
125
|
+
chunk_end_index: int | None = None
|
|
126
|
+
chunk_min_time: str | None = None
|
|
127
|
+
chunk_max_time: str | None = None
|
|
128
|
+
chunk_cpus: set[str] = set()
|
|
129
|
+
chunk_tasks: set[str] = set()
|
|
130
|
+
|
|
131
|
+
def flush_batch() -> None:
|
|
132
|
+
nonlocal batch
|
|
133
|
+
if not batch:
|
|
134
|
+
return
|
|
135
|
+
self.db.executemany(
|
|
136
|
+
"""INSERT INTO events(session,event_index,marker_id,raw_line,timestamp_raw,cpu_hint,task_hint,clock_domain,message,payload_json)
|
|
137
|
+
VALUES(?,?,?,?,?,?,?,?,?,?)""",
|
|
138
|
+
batch,
|
|
139
|
+
)
|
|
140
|
+
batch = []
|
|
141
|
+
|
|
142
|
+
def flush_chunk() -> None:
|
|
143
|
+
nonlocal chunk_index, chunk_count, chunk_start_index, chunk_end_index
|
|
144
|
+
nonlocal chunk_min_time, chunk_max_time, chunk_cpus, chunk_tasks
|
|
145
|
+
if chunk_count == 0 or chunk_start_index is None or chunk_end_index is None:
|
|
146
|
+
return
|
|
147
|
+
self.db.execute(
|
|
148
|
+
"""INSERT INTO event_chunks(session,chunk_index,start_event_index,end_event_index,min_timestamp_raw,max_timestamp_raw,cpu_json,task_json)
|
|
149
|
+
VALUES(?,?,?,?,?,?,?,?)""",
|
|
150
|
+
(
|
|
151
|
+
session, chunk_index, chunk_start_index, chunk_end_index,
|
|
152
|
+
chunk_min_time, chunk_max_time,
|
|
153
|
+
json.dumps(sorted(chunk_cpus)), json.dumps(sorted(chunk_tasks)),
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
chunk_index += 1
|
|
157
|
+
chunk_count = 0
|
|
158
|
+
chunk_start_index = None
|
|
159
|
+
chunk_end_index = None
|
|
160
|
+
chunk_min_time = None
|
|
161
|
+
chunk_max_time = None
|
|
162
|
+
chunk_cpus = set()
|
|
163
|
+
chunk_tasks = set()
|
|
164
|
+
|
|
165
|
+
if bulk_rebuild_indexes:
|
|
166
|
+
self._drop_secondary_indexes()
|
|
167
|
+
try:
|
|
168
|
+
with self.db:
|
|
169
|
+
self.db.execute("DELETE FROM events WHERE session=?", (session,))
|
|
170
|
+
self.db.execute("DELETE FROM event_chunks WHERE session=?", (session,))
|
|
171
|
+
for e in events:
|
|
172
|
+
batch.append((
|
|
173
|
+
session, e.event_index, e.marker_id, e.raw_line, e.timestamp_raw,
|
|
174
|
+
e.cpu_hint, e.task_hint, e.clock_domain, e.message,
|
|
175
|
+
json.dumps(e.payload_dict(), ensure_ascii=False, sort_keys=True),
|
|
176
|
+
))
|
|
177
|
+
if chunk_count == 0:
|
|
178
|
+
chunk_start_index = e.event_index
|
|
179
|
+
chunk_end_index = e.event_index
|
|
180
|
+
chunk_count += 1
|
|
181
|
+
if e.timestamp_raw is not None:
|
|
182
|
+
chunk_min_time = e.timestamp_raw if chunk_min_time is None else min(chunk_min_time, e.timestamp_raw)
|
|
183
|
+
chunk_max_time = e.timestamp_raw if chunk_max_time is None else max(chunk_max_time, e.timestamp_raw)
|
|
184
|
+
if e.cpu_hint:
|
|
185
|
+
chunk_cpus.add(e.cpu_hint)
|
|
186
|
+
if e.task_hint:
|
|
187
|
+
chunk_tasks.add(e.task_hint)
|
|
188
|
+
count += 1
|
|
189
|
+
if len(batch) >= batch_size:
|
|
190
|
+
flush_batch()
|
|
191
|
+
if chunk_count >= chunk_size:
|
|
192
|
+
flush_chunk()
|
|
193
|
+
flush_batch()
|
|
194
|
+
flush_chunk()
|
|
195
|
+
finally:
|
|
196
|
+
if bulk_rebuild_indexes:
|
|
197
|
+
self._create_secondary_indexes()
|
|
198
|
+
self.db.commit()
|
|
199
|
+
return count
|
|
200
|
+
|
|
201
|
+
def replace_session_events_chunked(
|
|
202
|
+
self,
|
|
203
|
+
session: str,
|
|
204
|
+
events: Iterable[RuntimeEvent],
|
|
205
|
+
*,
|
|
206
|
+
chunk_size: int = 10000,
|
|
207
|
+
batch_size: int = 50000,
|
|
208
|
+
commit_every: int = 250000,
|
|
209
|
+
rebuild_indexes: bool = True,
|
|
210
|
+
) -> int:
|
|
211
|
+
"""Import a very large session using bounded transactions.
|
|
212
|
+
|
|
213
|
+
Partial data is marked IMPORTING and only becomes READY after the full
|
|
214
|
+
import (and optional secondary-index rebuild) completes.
|
|
215
|
+
"""
|
|
216
|
+
chunk_size=max(1,int(chunk_size)); batch_size=max(1,int(batch_size)); commit_every=max(batch_size,int(commit_every))
|
|
217
|
+
if rebuild_indexes:
|
|
218
|
+
self._drop_secondary_indexes()
|
|
219
|
+
self.db.execute("INSERT OR REPLACE INTO sessions(session,status,event_count) VALUES(?, 'IMPORTING', 0)",(session,))
|
|
220
|
+
self.db.execute("DELETE FROM events WHERE session=?",(session,))
|
|
221
|
+
self.db.execute("DELETE FROM event_chunks WHERE session=?",(session,))
|
|
222
|
+
self.db.commit()
|
|
223
|
+
count=0; since_commit=0; batch=[]
|
|
224
|
+
chunk_index=0; chunk_count=0; chunk_start=None; chunk_end=None; min_time=None; max_time=None; cpus=set(); tasks=set()
|
|
225
|
+
def flush_batch():
|
|
226
|
+
nonlocal batch
|
|
227
|
+
if batch:
|
|
228
|
+
self.db.executemany("INSERT INTO events(session,event_index,marker_id,raw_line,timestamp_raw,cpu_hint,task_hint,clock_domain,message,payload_json) VALUES(?,?,?,?,?,?,?,?,?,?)",batch); batch=[]
|
|
229
|
+
def flush_chunk():
|
|
230
|
+
nonlocal chunk_index,chunk_count,chunk_start,chunk_end,min_time,max_time,cpus,tasks
|
|
231
|
+
if not chunk_count:return
|
|
232
|
+
self.db.execute("INSERT INTO event_chunks(session,chunk_index,start_event_index,end_event_index,min_timestamp_raw,max_timestamp_raw,cpu_json,task_json) VALUES(?,?,?,?,?,?,?,?)",(session,chunk_index,chunk_start,chunk_end,min_time,max_time,json.dumps(sorted(cpus)),json.dumps(sorted(tasks))))
|
|
233
|
+
chunk_index+=1; chunk_count=0; chunk_start=None; chunk_end=None; min_time=None; max_time=None; cpus=set(); tasks=set()
|
|
234
|
+
try:
|
|
235
|
+
for e in events:
|
|
236
|
+
batch.append((session,e.event_index,e.marker_id,e.raw_line,e.timestamp_raw,e.cpu_hint,e.task_hint,e.clock_domain,e.message,json.dumps(e.payload_dict(),ensure_ascii=False,sort_keys=True)))
|
|
237
|
+
if chunk_count==0: chunk_start=e.event_index
|
|
238
|
+
chunk_end=e.event_index; chunk_count+=1; count+=1; since_commit+=1
|
|
239
|
+
if e.timestamp_raw is not None:
|
|
240
|
+
min_time=e.timestamp_raw if min_time is None else min(min_time,e.timestamp_raw); max_time=e.timestamp_raw if max_time is None else max(max_time,e.timestamp_raw)
|
|
241
|
+
if e.cpu_hint: cpus.add(e.cpu_hint)
|
|
242
|
+
if e.task_hint: tasks.add(e.task_hint)
|
|
243
|
+
if len(batch)>=batch_size: flush_batch()
|
|
244
|
+
if chunk_count>=chunk_size: flush_chunk()
|
|
245
|
+
if since_commit>=commit_every:
|
|
246
|
+
flush_batch(); self.db.execute("UPDATE sessions SET event_count=? WHERE session=?",(count,session)); self.db.commit(); since_commit=0
|
|
247
|
+
flush_batch(); flush_chunk(); self.db.execute("UPDATE sessions SET event_count=? WHERE session=?",(count,session)); self.db.commit()
|
|
248
|
+
if rebuild_indexes:
|
|
249
|
+
self._create_secondary_indexes(); self.db.commit()
|
|
250
|
+
self.db.execute("UPDATE sessions SET status='READY', event_count=? WHERE session=?",(count,session)); self.db.commit()
|
|
251
|
+
return count
|
|
252
|
+
except BaseException:
|
|
253
|
+
try:
|
|
254
|
+
flush_batch(); self.db.execute("UPDATE sessions SET status='IMPORTING', event_count=? WHERE session=?",(count,session)); self.db.commit()
|
|
255
|
+
finally:
|
|
256
|
+
pass
|
|
257
|
+
raise
|
|
258
|
+
|
|
259
|
+
def resume_session_events_chunked(
|
|
260
|
+
self,
|
|
261
|
+
session: str,
|
|
262
|
+
events: Iterable[RuntimeEvent],
|
|
263
|
+
*,
|
|
264
|
+
chunk_size: int = 10000,
|
|
265
|
+
batch_size: int = 50000,
|
|
266
|
+
commit_every: int = 250000,
|
|
267
|
+
finalize: bool = False,
|
|
268
|
+
) -> int:
|
|
269
|
+
"""Append to an IMPORTING session and optionally finalize it."""
|
|
270
|
+
row=self.db.execute("SELECT status,event_count FROM sessions WHERE session=?",(session,)).fetchone()
|
|
271
|
+
if row is None or row[0] != 'IMPORTING':
|
|
272
|
+
raise ValueError(f"session {session!r} is not IMPORTING")
|
|
273
|
+
existing=int(row[1])
|
|
274
|
+
chunk_size=max(1,int(chunk_size)); batch_size=max(1,int(batch_size)); commit_every=max(batch_size,int(commit_every))
|
|
275
|
+
chunk_row=self.db.execute("SELECT COALESCE(MAX(chunk_index),-1) FROM event_chunks WHERE session=?",(session,)).fetchone()
|
|
276
|
+
chunk_index=int(chunk_row[0])+1
|
|
277
|
+
count=existing; added=0; since_commit=0; batch=[]
|
|
278
|
+
chunk_count=0; chunk_start=None; chunk_end=None; min_time=None; max_time=None; cpus=set(); tasks=set()
|
|
279
|
+
def flush_batch():
|
|
280
|
+
nonlocal batch
|
|
281
|
+
if batch:
|
|
282
|
+
self.db.executemany("INSERT INTO events(session,event_index,marker_id,raw_line,timestamp_raw,cpu_hint,task_hint,clock_domain,message,payload_json) VALUES(?,?,?,?,?,?,?,?,?,?)",batch); batch=[]
|
|
283
|
+
def flush_chunk():
|
|
284
|
+
nonlocal chunk_index,chunk_count,chunk_start,chunk_end,min_time,max_time,cpus,tasks
|
|
285
|
+
if not chunk_count:return
|
|
286
|
+
self.db.execute("INSERT INTO event_chunks(session,chunk_index,start_event_index,end_event_index,min_timestamp_raw,max_timestamp_raw,cpu_json,task_json) VALUES(?,?,?,?,?,?,?,?)",(session,chunk_index,chunk_start,chunk_end,min_time,max_time,json.dumps(sorted(cpus)),json.dumps(sorted(tasks))))
|
|
287
|
+
chunk_index+=1; chunk_count=0; chunk_start=None; chunk_end=None; min_time=None; max_time=None; cpus=set(); tasks=set()
|
|
288
|
+
for e in events:
|
|
289
|
+
batch.append((session,e.event_index,e.marker_id,e.raw_line,e.timestamp_raw,e.cpu_hint,e.task_hint,e.clock_domain,e.message,json.dumps(e.payload_dict(),ensure_ascii=False,sort_keys=True)))
|
|
290
|
+
if chunk_count==0: chunk_start=e.event_index
|
|
291
|
+
chunk_end=e.event_index; chunk_count+=1; added+=1; count+=1; since_commit+=1
|
|
292
|
+
if e.timestamp_raw is not None:
|
|
293
|
+
min_time=e.timestamp_raw if min_time is None else min(min_time,e.timestamp_raw); max_time=e.timestamp_raw if max_time is None else max(max_time,e.timestamp_raw)
|
|
294
|
+
if e.cpu_hint: cpus.add(e.cpu_hint)
|
|
295
|
+
if e.task_hint: tasks.add(e.task_hint)
|
|
296
|
+
if len(batch)>=batch_size: flush_batch()
|
|
297
|
+
if chunk_count>=chunk_size: flush_chunk()
|
|
298
|
+
if since_commit>=commit_every:
|
|
299
|
+
flush_batch(); self.db.execute("UPDATE sessions SET event_count=? WHERE session=?",(count,session)); self.db.commit(); since_commit=0
|
|
300
|
+
flush_batch(); flush_chunk(); self.db.execute("UPDATE sessions SET event_count=? WHERE session=?",(count,session)); self.db.commit()
|
|
301
|
+
if finalize:
|
|
302
|
+
self._create_secondary_indexes(); self.db.commit()
|
|
303
|
+
self.db.execute("UPDATE sessions SET status='READY' WHERE session=?",(session,)); self.db.commit()
|
|
304
|
+
return added
|
|
305
|
+
|
|
306
|
+
def session_status(self, session: str) -> str | None:
|
|
307
|
+
row=self.db.execute("SELECT status FROM sessions WHERE session=?",(session,)).fetchone()
|
|
308
|
+
return None if row is None else str(row[0])
|
|
309
|
+
|
|
310
|
+
def count(self, session: str) -> int:
|
|
311
|
+
return int(self.db.execute("SELECT COUNT(*) FROM events WHERE session=?", (session,)).fetchone()[0])
|
|
312
|
+
|
|
313
|
+
def fetch_window(self, session: str, *, offset: int = 0, limit: int = 500) -> list[RuntimeEvent]:
|
|
314
|
+
rows = self.db.execute(
|
|
315
|
+
"SELECT * FROM events WHERE session=? ORDER BY event_index LIMIT ? OFFSET ?",
|
|
316
|
+
(session, int(limit), int(offset)),
|
|
317
|
+
).fetchall()
|
|
318
|
+
return [self._row_to_event(r) for r in rows]
|
|
319
|
+
|
|
320
|
+
def by_marker(self, session: str, marker_id: str, *, limit: int = 500) -> list[RuntimeEvent]:
|
|
321
|
+
rows = self.db.execute(
|
|
322
|
+
"SELECT * FROM events WHERE session=? AND marker_id=? ORDER BY event_index LIMIT ?",
|
|
323
|
+
(session, marker_id, int(limit)),
|
|
324
|
+
).fetchall()
|
|
325
|
+
return [self._row_to_event(r) for r in rows]
|
|
326
|
+
|
|
327
|
+
def summary(self, session: str) -> dict[str, object]:
|
|
328
|
+
count = self.count(session)
|
|
329
|
+
cpus = [r[0] for r in self.db.execute("SELECT DISTINCT cpu_hint FROM events WHERE session=? AND cpu_hint IS NOT NULL ORDER BY cpu_hint", (session,))]
|
|
330
|
+
tasks = [r[0] for r in self.db.execute("SELECT DISTINCT task_hint FROM events WHERE session=? AND task_hint IS NOT NULL ORDER BY task_hint", (session,))]
|
|
331
|
+
return {"session": session, "count": count, "cpus": cpus, "tasks": tasks, "schema_version": SCHEMA_VERSION}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def chunk_summary(self, session: str) -> list[dict[str, object]]:
|
|
335
|
+
rows = self.db.execute(
|
|
336
|
+
"SELECT * FROM event_chunks WHERE session=? ORDER BY chunk_index", (session,)
|
|
337
|
+
).fetchall()
|
|
338
|
+
return [
|
|
339
|
+
{
|
|
340
|
+
"chunk_index": r["chunk_index"],
|
|
341
|
+
"start_event_index": r["start_event_index"],
|
|
342
|
+
"end_event_index": r["end_event_index"],
|
|
343
|
+
"min_timestamp_raw": r["min_timestamp_raw"],
|
|
344
|
+
"max_timestamp_raw": r["max_timestamp_raw"],
|
|
345
|
+
"cpus": json.loads(r["cpu_json"]),
|
|
346
|
+
"tasks": json.loads(r["task_json"]),
|
|
347
|
+
}
|
|
348
|
+
for r in rows
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
def fetch_event_index_range(self, session: str, start_index: int, end_index: int) -> list[RuntimeEvent]:
|
|
352
|
+
rows = self.db.execute(
|
|
353
|
+
"SELECT * FROM events WHERE session=? AND event_index BETWEEN ? AND ? ORDER BY event_index",
|
|
354
|
+
(session, int(start_index), int(end_index)),
|
|
355
|
+
).fetchall()
|
|
356
|
+
return [self._row_to_event(r) for r in rows]
|
|
357
|
+
|
|
358
|
+
def iter_events(self, session: str, *, batch_size: int = 5000):
|
|
359
|
+
"""Stream events in ingest order without OFFSET-based pagination."""
|
|
360
|
+
cur = self.db.execute(
|
|
361
|
+
"SELECT * FROM events WHERE session=? ORDER BY event_index", (session,)
|
|
362
|
+
)
|
|
363
|
+
size = max(1, int(batch_size))
|
|
364
|
+
while True:
|
|
365
|
+
rows = cur.fetchmany(size)
|
|
366
|
+
if not rows:
|
|
367
|
+
break
|
|
368
|
+
for row in rows:
|
|
369
|
+
yield self._row_to_event(row)
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def _row_to_event(r: sqlite3.Row) -> RuntimeEvent:
|
|
373
|
+
payload = tuple(sorted(json.loads(r["payload_json"]).items()))
|
|
374
|
+
return RuntimeEvent(
|
|
375
|
+
event_index=r["event_index"], marker_id=r["marker_id"], raw_line=r["raw_line"],
|
|
376
|
+
timestamp_raw=r["timestamp_raw"], cpu_hint=r["cpu_hint"], task_hint=r["task_hint"],
|
|
377
|
+
clock_domain=r["clock_domain"], message=r["message"], payload=payload,
|
|
378
|
+
)
|
cseq/explain.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
|
|
5
|
+
from .model import ProjectIndex
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def explain_call(index: ProjectIndex, *, caller: str, callee_text: str | None = None) -> dict:
|
|
9
|
+
matches = [c for c in index.callsites if c.caller_name == caller]
|
|
10
|
+
if callee_text is not None:
|
|
11
|
+
matches = [c for c in matches if c.callee_name == callee_text or callee_text in c.raw_text]
|
|
12
|
+
if not matches:
|
|
13
|
+
return {"found": False, "caller": caller, "query": callee_text, "calls": []}
|
|
14
|
+
|
|
15
|
+
functions = {f.qualified_id: f for f in index.functions()}
|
|
16
|
+
flow_by_target: dict[str, list] = {}
|
|
17
|
+
for edge in index.flow_edges:
|
|
18
|
+
flow_by_target.setdefault(edge.target_key, []).append(edge)
|
|
19
|
+
|
|
20
|
+
calls = []
|
|
21
|
+
for call in matches:
|
|
22
|
+
targets = []
|
|
23
|
+
for tid in call.target_function_ids:
|
|
24
|
+
fn = functions.get(tid)
|
|
25
|
+
targets.append({
|
|
26
|
+
"function_id": tid,
|
|
27
|
+
"name": fn.name if fn else None,
|
|
28
|
+
"source": fn.source_path if fn else None,
|
|
29
|
+
})
|
|
30
|
+
if call.target_function_id and call.target_function_id not in call.target_function_ids:
|
|
31
|
+
fn = functions.get(call.target_function_id)
|
|
32
|
+
targets.append({
|
|
33
|
+
"function_id": call.target_function_id,
|
|
34
|
+
"name": fn.name if fn else None,
|
|
35
|
+
"source": fn.source_path if fn else None,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
flow = []
|
|
39
|
+
if call.callee_slot_key:
|
|
40
|
+
queue = [call.callee_slot_key]
|
|
41
|
+
seen = set()
|
|
42
|
+
while queue and len(flow) < 64:
|
|
43
|
+
target = queue.pop(0)
|
|
44
|
+
if target in seen:
|
|
45
|
+
continue
|
|
46
|
+
seen.add(target)
|
|
47
|
+
for edge in flow_by_target.get(target, []):
|
|
48
|
+
flow.append({
|
|
49
|
+
"kind": edge.kind.value,
|
|
50
|
+
"from": edge.source_key,
|
|
51
|
+
"to": edge.target_key,
|
|
52
|
+
"source": edge.source_path,
|
|
53
|
+
})
|
|
54
|
+
queue.append(edge.source_key)
|
|
55
|
+
|
|
56
|
+
cpu = []
|
|
57
|
+
target_ids = set(call.target_function_ids)
|
|
58
|
+
if call.target_function_id:
|
|
59
|
+
target_ids.add(call.target_function_id)
|
|
60
|
+
for ev in index.cpu_evidence:
|
|
61
|
+
if ev.target_function_id in target_ids:
|
|
62
|
+
cpu.append({
|
|
63
|
+
"function_id": ev.target_function_id,
|
|
64
|
+
"layer": ev.layer.value,
|
|
65
|
+
"kind": ev.value_kind.value,
|
|
66
|
+
"value": ev.value,
|
|
67
|
+
"provenance": ev.provenance,
|
|
68
|
+
"explicit_override": ev.explicit_override,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
linker = []
|
|
72
|
+
for ev in index.linker_evidence:
|
|
73
|
+
if ev.symbol_name == (call.callee_name or "") or target_ids.intersection(ev.matched_function_ids):
|
|
74
|
+
linker.append({
|
|
75
|
+
"symbol": ev.symbol_name, "address": ev.address,
|
|
76
|
+
"object_file": ev.object_file, "section": ev.section,
|
|
77
|
+
"matched_function_ids": list(ev.matched_function_ids),
|
|
78
|
+
"provenance": ev.provenance,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
binary = []
|
|
82
|
+
for ev in index.binary_evidence:
|
|
83
|
+
if ev.symbol_name == (call.callee_name or "") or target_ids.intersection(ev.matched_function_ids):
|
|
84
|
+
binary.append({
|
|
85
|
+
"symbol": ev.symbol_name, "address": ev.address, "size": ev.size,
|
|
86
|
+
"kind": ev.symbol_kind, "matched_function_ids": list(ev.matched_function_ids),
|
|
87
|
+
"binary_hash": ev.binary_hash, "provenance": ev.provenance,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
dwarf = []
|
|
91
|
+
for ev in index.dwarf_evidence:
|
|
92
|
+
if ev.function_id in target_ids:
|
|
93
|
+
dwarf.append({
|
|
94
|
+
"function_id": ev.function_id, "source": ev.source_path,
|
|
95
|
+
"line": ev.line, "address": ev.address,
|
|
96
|
+
"dwarf_file": ev.dwarf_file, "provenance": ev.provenance,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
calls.append({
|
|
100
|
+
"source": call.source_path,
|
|
101
|
+
"line": call.source_range.start_line,
|
|
102
|
+
"raw": call.raw_text,
|
|
103
|
+
"resolution": call.target_kind.value,
|
|
104
|
+
"unknown_possible": call.unknown_possible,
|
|
105
|
+
"callee_slot": call.callee_slot_key,
|
|
106
|
+
"targets": targets,
|
|
107
|
+
"value_flow": flow,
|
|
108
|
+
"cpu_evidence": cpu,
|
|
109
|
+
"linker_evidence": linker,
|
|
110
|
+
"binary_evidence": binary,
|
|
111
|
+
"dwarf_evidence": dwarf,
|
|
112
|
+
})
|
|
113
|
+
return {"found": True, "caller": caller, "query": callee_text, "calls": calls}
|