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/static_store.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable, Iterator
|
|
7
|
+
|
|
8
|
+
from .model import CompactFunctionRecord, FunctionRecord, SourceRange
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StaticSymbolStore:
|
|
12
|
+
"""Disk-backed function metadata store for very large projects.
|
|
13
|
+
|
|
14
|
+
The store preserves the FunctionRecord-facing contract while allowing parse
|
|
15
|
+
artifacts to release their per-TU function tables after ingestion.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
SCHEMA_VERSION = 7
|
|
19
|
+
|
|
20
|
+
def __init__(self, path: str | Path) -> None:
|
|
21
|
+
self.path = Path(path)
|
|
22
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
self.connection = sqlite3.connect(self.path)
|
|
24
|
+
# The Static Symbol Store is a reproducible analysis cache, not the
|
|
25
|
+
# authoritative source. Keep writes fast on million-function projects.
|
|
26
|
+
# Reuse only requires that we avoid EXCLUSIVE locking; journaling can be
|
|
27
|
+
# disabled because an interrupted/corrupt store is safely rebuilt from
|
|
28
|
+
# source fingerprints on the next run.
|
|
29
|
+
self.connection.execute("PRAGMA busy_timeout=5000")
|
|
30
|
+
self.connection.execute("PRAGMA journal_mode=OFF")
|
|
31
|
+
self.connection.execute("PRAGMA synchronous=OFF")
|
|
32
|
+
self.connection.execute("PRAGMA temp_store=MEMORY")
|
|
33
|
+
self.connection.executescript(
|
|
34
|
+
"""
|
|
35
|
+
CREATE TABLE IF NOT EXISTS meta(
|
|
36
|
+
key TEXT PRIMARY KEY,
|
|
37
|
+
value TEXT NOT NULL
|
|
38
|
+
);
|
|
39
|
+
CREATE TABLE IF NOT EXISTS functions(
|
|
40
|
+
qid TEXT NOT NULL,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
source_path TEXT NOT NULL,
|
|
43
|
+
start_line INTEGER,
|
|
44
|
+
start_column INTEGER,
|
|
45
|
+
end_line INTEGER,
|
|
46
|
+
end_column INTEGER,
|
|
47
|
+
storage_class TEXT,
|
|
48
|
+
type_qualifier TEXT,
|
|
49
|
+
parameters_json TEXT NOT NULL
|
|
50
|
+
);
|
|
51
|
+
CREATE TABLE IF NOT EXISTS translation_units(
|
|
52
|
+
source_path TEXT PRIMARY KEY,
|
|
53
|
+
fingerprint TEXT NOT NULL,
|
|
54
|
+
function_count INTEGER NOT NULL,
|
|
55
|
+
artifact_json TEXT NOT NULL DEFAULT '{}'
|
|
56
|
+
) WITHOUT ROWID;
|
|
57
|
+
"""
|
|
58
|
+
)
|
|
59
|
+
row = self.connection.execute(
|
|
60
|
+
"SELECT value FROM meta WHERE key='schema_version'"
|
|
61
|
+
).fetchone()
|
|
62
|
+
old_schema = int(row[0]) if row and str(row[0]).isdigit() else None
|
|
63
|
+
columns = {r[1] for r in self.connection.execute("PRAGMA table_info(translation_units)")}
|
|
64
|
+
if "artifact_json" not in columns:
|
|
65
|
+
self.connection.execute(
|
|
66
|
+
"ALTER TABLE translation_units ADD COLUMN artifact_json TEXT NOT NULL DEFAULT '{}'"
|
|
67
|
+
)
|
|
68
|
+
self.connection.execute("DROP INDEX IF EXISTS idx_functions_source")
|
|
69
|
+
if old_schema != self.SCHEMA_VERSION:
|
|
70
|
+
# Schema v7 changes the function table from a random TEXT-primary-key
|
|
71
|
+
# B-tree into an append-oriented rowid table. The store is a rebuildable
|
|
72
|
+
# cache, so recreate it rather than paying a migration cost.
|
|
73
|
+
self.connection.execute("DROP TABLE IF EXISTS functions")
|
|
74
|
+
self.connection.execute(
|
|
75
|
+
"""CREATE TABLE functions(
|
|
76
|
+
qid TEXT NOT NULL, name TEXT NOT NULL, source_path TEXT NOT NULL,
|
|
77
|
+
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
|
|
78
|
+
storage_class TEXT, type_qualifier TEXT, parameters_json TEXT NOT NULL
|
|
79
|
+
)"""
|
|
80
|
+
)
|
|
81
|
+
self.connection.execute("DELETE FROM translation_units")
|
|
82
|
+
self.connection.execute("DROP INDEX IF EXISTS idx_functions_name")
|
|
83
|
+
# A permanent name index is deliberately avoided. Project-wide call
|
|
84
|
+
# resolution asks only for names that actually appear at CallSites.
|
|
85
|
+
self.connection.execute(
|
|
86
|
+
"INSERT OR REPLACE INTO meta(key,value) VALUES('schema_version',?)",
|
|
87
|
+
(str(self.SCHEMA_VERSION),),
|
|
88
|
+
)
|
|
89
|
+
self.connection.commit()
|
|
90
|
+
|
|
91
|
+
def reset(self) -> None:
|
|
92
|
+
self.connection.execute("DELETE FROM functions")
|
|
93
|
+
self.connection.execute("DELETE FROM translation_units")
|
|
94
|
+
self.connection.execute("DROP INDEX IF EXISTS idx_functions_name")
|
|
95
|
+
self.connection.commit()
|
|
96
|
+
|
|
97
|
+
def ensure_project(self, root: str | Path) -> bool:
|
|
98
|
+
"""Bind the store to one project root.
|
|
99
|
+
|
|
100
|
+
Returns True when an existing compatible project binding is reused.
|
|
101
|
+
A different root invalidates relative-path keyed contents safely.
|
|
102
|
+
"""
|
|
103
|
+
root_text = str(Path(root).resolve())
|
|
104
|
+
row = self.connection.execute(
|
|
105
|
+
"SELECT value FROM meta WHERE key='project_root'"
|
|
106
|
+
).fetchone()
|
|
107
|
+
reused = bool(row and row[0] == root_text)
|
|
108
|
+
if row and row[0] != root_text:
|
|
109
|
+
self.reset()
|
|
110
|
+
self.connection.execute(
|
|
111
|
+
"INSERT OR REPLACE INTO meta(key,value) VALUES('project_root',?)",
|
|
112
|
+
(root_text,),
|
|
113
|
+
)
|
|
114
|
+
self.connection.commit()
|
|
115
|
+
return reused
|
|
116
|
+
|
|
117
|
+
def has_translation_unit(self, source_path: str, fingerprint: str) -> bool:
|
|
118
|
+
row = self.connection.execute(
|
|
119
|
+
"SELECT fingerprint FROM translation_units WHERE source_path=?",
|
|
120
|
+
(source_path,),
|
|
121
|
+
).fetchone()
|
|
122
|
+
return bool(row and row[0] == fingerprint)
|
|
123
|
+
|
|
124
|
+
def translation_unit_state_raw(self) -> dict[str, tuple[str, str]]:
|
|
125
|
+
"""Return the small TU reuse index without materializing functions."""
|
|
126
|
+
return {
|
|
127
|
+
source_path: (fingerprint, artifact_json)
|
|
128
|
+
for source_path, fingerprint, artifact_json in self.connection.execute(
|
|
129
|
+
"SELECT source_path,fingerprint,artifact_json FROM translation_units"
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def replace_translation_unit(
|
|
134
|
+
self,
|
|
135
|
+
source_path: str,
|
|
136
|
+
fingerprint: str,
|
|
137
|
+
functions: Iterable[FunctionRecord | CompactFunctionRecord],
|
|
138
|
+
artifact_payload: dict | None = None,
|
|
139
|
+
) -> int:
|
|
140
|
+
rows = [self._row(fn) for fn in functions]
|
|
141
|
+
old = self.connection.execute(
|
|
142
|
+
"SELECT 1 FROM translation_units WHERE source_path=?", (source_path,)
|
|
143
|
+
).fetchone()
|
|
144
|
+
if old:
|
|
145
|
+
# Changed/removed TUs are rare compared with cold insertion. Avoid a
|
|
146
|
+
# permanent source_path secondary index and scan only on mutation.
|
|
147
|
+
self.connection.execute("DELETE FROM functions WHERE source_path=?", (source_path,))
|
|
148
|
+
if rows:
|
|
149
|
+
self.connection.executemany(
|
|
150
|
+
"""
|
|
151
|
+
INSERT INTO functions(
|
|
152
|
+
qid,name,source_path,start_line,start_column,end_line,end_column,
|
|
153
|
+
storage_class,type_qualifier,parameters_json
|
|
154
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?)
|
|
155
|
+
""",
|
|
156
|
+
rows,
|
|
157
|
+
)
|
|
158
|
+
artifact_json = json.dumps(artifact_payload or {}, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
159
|
+
self.connection.execute(
|
|
160
|
+
"""INSERT OR REPLACE INTO translation_units(source_path,fingerprint,function_count,artifact_json)
|
|
161
|
+
VALUES(?,?,?,?)""",
|
|
162
|
+
(source_path, fingerprint, len(rows), artifact_json),
|
|
163
|
+
)
|
|
164
|
+
return len(rows)
|
|
165
|
+
|
|
166
|
+
def remove_missing_translation_units(self, current_source_paths: set[str]) -> int:
|
|
167
|
+
rows = [r[0] for r in self.connection.execute("SELECT source_path FROM translation_units")]
|
|
168
|
+
stale = [path for path in rows if path not in current_source_paths]
|
|
169
|
+
for path in stale:
|
|
170
|
+
self.connection.execute("DELETE FROM functions WHERE source_path=?", (path,))
|
|
171
|
+
self.connection.execute("DELETE FROM translation_units WHERE source_path=?", (path,))
|
|
172
|
+
return len(stale)
|
|
173
|
+
|
|
174
|
+
def translation_unit_count(self) -> int:
|
|
175
|
+
return int(self.connection.execute("SELECT COUNT(*) FROM translation_units").fetchone()[0])
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _row(fn: FunctionRecord | CompactFunctionRecord):
|
|
179
|
+
r = fn.source_range
|
|
180
|
+
return (
|
|
181
|
+
fn.qualified_id,
|
|
182
|
+
fn.name,
|
|
183
|
+
fn.source_path,
|
|
184
|
+
r.start_line,
|
|
185
|
+
r.start_column,
|
|
186
|
+
r.end_line,
|
|
187
|
+
r.end_column,
|
|
188
|
+
fn.storage_class,
|
|
189
|
+
fn.type_qualifier,
|
|
190
|
+
json.dumps(list(fn.parameters), ensure_ascii=False, separators=(",", ":")),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def add_functions(self, functions: Iterable[FunctionRecord | CompactFunctionRecord]) -> int:
|
|
194
|
+
rows = [self._row(fn) for fn in functions]
|
|
195
|
+
if not rows:
|
|
196
|
+
return 0
|
|
197
|
+
self.connection.executemany(
|
|
198
|
+
"""
|
|
199
|
+
INSERT INTO functions(
|
|
200
|
+
qid,name,source_path,start_line,start_column,end_line,end_column,
|
|
201
|
+
storage_class,type_qualifier,parameters_json
|
|
202
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?)
|
|
203
|
+
""",
|
|
204
|
+
rows,
|
|
205
|
+
)
|
|
206
|
+
return len(rows)
|
|
207
|
+
|
|
208
|
+
def flush(self) -> None:
|
|
209
|
+
"""Commit current row batches without forcing secondary-index creation."""
|
|
210
|
+
self.connection.commit()
|
|
211
|
+
|
|
212
|
+
def commit(self) -> None:
|
|
213
|
+
self.connection.commit()
|
|
214
|
+
|
|
215
|
+
def count(self) -> int:
|
|
216
|
+
return int(self.connection.execute("SELECT COUNT(*) FROM functions").fetchone()[0])
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _record(row) -> FunctionRecord:
|
|
220
|
+
return FunctionRecord(
|
|
221
|
+
name=row[1],
|
|
222
|
+
qualified_id=row[0],
|
|
223
|
+
source_path=row[2],
|
|
224
|
+
source_range=SourceRange(row[3], row[4], row[5], row[6]),
|
|
225
|
+
storage_class=row[7],
|
|
226
|
+
type_qualifier=row[8],
|
|
227
|
+
parameters=tuple(json.loads(row[9]) if row[9] else ()),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def iter_functions(self, fetch_size: int = 4096) -> Iterator[FunctionRecord]:
|
|
231
|
+
cur = self.connection.execute(
|
|
232
|
+
"""SELECT qid,name,source_path,start_line,start_column,end_line,end_column,
|
|
233
|
+
storage_class,type_qualifier,parameters_json
|
|
234
|
+
FROM functions ORDER BY source_path,start_line,start_column"""
|
|
235
|
+
)
|
|
236
|
+
while True:
|
|
237
|
+
rows = cur.fetchmany(fetch_size)
|
|
238
|
+
if not rows:
|
|
239
|
+
break
|
|
240
|
+
for row in rows:
|
|
241
|
+
yield self._record(row)
|
|
242
|
+
|
|
243
|
+
def iter_named(self, names: set[str]) -> Iterator[FunctionRecord]:
|
|
244
|
+
if not names:
|
|
245
|
+
return
|
|
246
|
+
ordered = sorted(names)
|
|
247
|
+
# No persistent name index and no TEMP-table writes: each chunk performs
|
|
248
|
+
# one sequential scan of the disk-backed store. Most real resolution
|
|
249
|
+
# queries fit in a single chunk because they contain only names observed
|
|
250
|
+
# at reachable CallSites.
|
|
251
|
+
for start in range(0, len(ordered), 800):
|
|
252
|
+
chunk = ordered[start:start + 800]
|
|
253
|
+
placeholders = ",".join("?" for _ in chunk)
|
|
254
|
+
cur = self.connection.execute(
|
|
255
|
+
f"""SELECT qid,name,source_path,start_line,start_column,end_line,end_column,
|
|
256
|
+
storage_class,type_qualifier,parameters_json
|
|
257
|
+
FROM functions WHERE name IN ({placeholders})""",
|
|
258
|
+
chunk,
|
|
259
|
+
)
|
|
260
|
+
for row in cur:
|
|
261
|
+
yield self._record(row)
|
|
262
|
+
|
|
263
|
+
def iter_ids(self, qids: set[str]) -> Iterator[FunctionRecord]:
|
|
264
|
+
if not qids:
|
|
265
|
+
return
|
|
266
|
+
ordered = sorted(qids)
|
|
267
|
+
for start in range(0, len(ordered), 400):
|
|
268
|
+
chunk = ordered[start:start + 400]
|
|
269
|
+
placeholders = ",".join("?" for _ in chunk)
|
|
270
|
+
cur = self.connection.execute(
|
|
271
|
+
f"""SELECT qid,name,source_path,start_line,start_column,end_line,end_column,
|
|
272
|
+
storage_class,type_qualifier,parameters_json
|
|
273
|
+
FROM functions WHERE qid IN ({placeholders})""",
|
|
274
|
+
chunk,
|
|
275
|
+
)
|
|
276
|
+
for row in cur:
|
|
277
|
+
yield self._record(row)
|
|
278
|
+
|
|
279
|
+
def close(self) -> None:
|
|
280
|
+
self.connection.close()
|
cseq/trace_analysis.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from collections import defaultdict, deque
|
|
6
|
+
|
|
7
|
+
from .model import CallTargetKind, ProjectIndex
|
|
8
|
+
from .runtime import RuntimeEvent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RuntimeRelationType(str, Enum):
|
|
12
|
+
CORRELATED_EXACT = "CORRELATED_EXACT"
|
|
13
|
+
CORRELATED_POSSIBLE = "CORRELATED_POSSIBLE"
|
|
14
|
+
TEMPORAL_ONLY = "TEMPORAL_ONLY"
|
|
15
|
+
CONCURRENT_UNKNOWN = "CONCURRENT_UNKNOWN"
|
|
16
|
+
UNKNOWN_GAP = "UNKNOWN_GAP"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class CorrelationRelation:
|
|
21
|
+
left_event_index: int
|
|
22
|
+
right_event_index: int
|
|
23
|
+
relation_type: RuntimeRelationType
|
|
24
|
+
keys: tuple[str, ...]
|
|
25
|
+
values: tuple[str, ...]
|
|
26
|
+
score: float
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class ClockAlignment:
|
|
31
|
+
source_domain: str
|
|
32
|
+
target_domain: str
|
|
33
|
+
scale: float
|
|
34
|
+
offset_seconds: float
|
|
35
|
+
anchor_count: int
|
|
36
|
+
max_abs_residual_seconds: float
|
|
37
|
+
confidence: str
|
|
38
|
+
|
|
39
|
+
def transform_seconds(self, source_seconds: float) -> float:
|
|
40
|
+
return self.scale * source_seconds + self.offset_seconds
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ReconstructionState(str, Enum):
|
|
44
|
+
EXACT_PATH = "EXACT_PATH"
|
|
45
|
+
AMBIGUOUS_PATH = "AMBIGUOUS_PATH"
|
|
46
|
+
UNKNOWN_GAP = "UNKNOWN_GAP"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class ReconstructedPath:
|
|
51
|
+
start_function: str
|
|
52
|
+
end_function: str
|
|
53
|
+
state: ReconstructionState
|
|
54
|
+
paths: tuple[tuple[str, ...], ...]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def discover_correlations(
|
|
58
|
+
events: list[RuntimeEvent],
|
|
59
|
+
*,
|
|
60
|
+
exact_min_repetitions: int = 3,
|
|
61
|
+
) -> list[CorrelationRelation]:
|
|
62
|
+
"""Find conservative payload correlations and optionally promote stable ones.
|
|
63
|
+
|
|
64
|
+
A relation is promoted to CORRELATED_EXACT only when the *same field* links
|
|
65
|
+
the same marker pair across at least ``exact_min_repetitions`` distinct values,
|
|
66
|
+
and each of those values occurs in exactly two events. A single matching value
|
|
67
|
+
or time proximity is never enough. Set ``exact_min_repetitions <= 0`` to
|
|
68
|
+
disable automatic exact promotion.
|
|
69
|
+
"""
|
|
70
|
+
occurrence: dict[tuple[str, str], list[int]] = defaultdict(list)
|
|
71
|
+
for e in events:
|
|
72
|
+
for key, value in e.payload:
|
|
73
|
+
occurrence[(key, value)].append(e.event_index)
|
|
74
|
+
|
|
75
|
+
by_index = {e.event_index: e for e in events}
|
|
76
|
+
pair_fields: dict[tuple[int, int], list[tuple[str, str, float]]] = defaultdict(list)
|
|
77
|
+
for (key, value), indexes in occurrence.items():
|
|
78
|
+
# Very common values are weak and create too many false edges.
|
|
79
|
+
if len(indexes) < 2 or len(indexes) > 8:
|
|
80
|
+
continue
|
|
81
|
+
uniqueness = 1.0 / len(indexes)
|
|
82
|
+
for i, left in enumerate(indexes):
|
|
83
|
+
for right in indexes[i + 1:]:
|
|
84
|
+
a, b = by_index[left], by_index[right]
|
|
85
|
+
# Same marker repeated is useful for repetition, not transaction linking.
|
|
86
|
+
if a.marker_id == b.marker_id:
|
|
87
|
+
continue
|
|
88
|
+
pair_fields[(left, right)].append((key, value, uniqueness))
|
|
89
|
+
|
|
90
|
+
stable_exact: set[tuple[str, tuple[str, str]]] = set()
|
|
91
|
+
if exact_min_repetitions > 0:
|
|
92
|
+
# key + marker-pair -> distinct values that uniquely link exactly two events
|
|
93
|
+
stable_values: dict[tuple[str, tuple[str, str]], set[str]] = defaultdict(set)
|
|
94
|
+
for (key, value), indexes in occurrence.items():
|
|
95
|
+
if len(indexes) != 2:
|
|
96
|
+
continue
|
|
97
|
+
a, b = by_index[indexes[0]], by_index[indexes[1]]
|
|
98
|
+
if a.marker_id == b.marker_id:
|
|
99
|
+
continue
|
|
100
|
+
marker_pair = tuple(sorted((a.marker_id, b.marker_id)))
|
|
101
|
+
stable_values[(key, marker_pair)].add(value)
|
|
102
|
+
stable_exact = {
|
|
103
|
+
group for group, values in stable_values.items()
|
|
104
|
+
if len(values) >= exact_min_repetitions
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
out: list[CorrelationRelation] = []
|
|
108
|
+
for (left, right), fields in sorted(pair_fields.items()):
|
|
109
|
+
a, b = by_index[left], by_index[right]
|
|
110
|
+
keys = tuple(x[0] for x in fields)
|
|
111
|
+
values = tuple(x[1] for x in fields)
|
|
112
|
+
score = sum(x[2] for x in fields)
|
|
113
|
+
if a.cpu_hint and b.cpu_hint and a.cpu_hint != b.cpu_hint:
|
|
114
|
+
score += 0.25
|
|
115
|
+
if a.task_hint and b.task_hint and a.task_hint != b.task_hint:
|
|
116
|
+
score += 0.10
|
|
117
|
+
marker_pair = tuple(sorted((a.marker_id, b.marker_id)))
|
|
118
|
+
exact_keys = [key for key, _value, _u in fields if (key, marker_pair) in stable_exact]
|
|
119
|
+
relation_type = (
|
|
120
|
+
RuntimeRelationType.CORRELATED_EXACT
|
|
121
|
+
if exact_keys
|
|
122
|
+
else RuntimeRelationType.CORRELATED_POSSIBLE
|
|
123
|
+
)
|
|
124
|
+
out.append(CorrelationRelation(
|
|
125
|
+
left_event_index=left,
|
|
126
|
+
right_event_index=right,
|
|
127
|
+
relation_type=relation_type,
|
|
128
|
+
keys=keys,
|
|
129
|
+
values=values,
|
|
130
|
+
score=score,
|
|
131
|
+
))
|
|
132
|
+
return sorted(out, key=lambda r: (-r.score, r.left_event_index, r.right_event_index))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def fit_clock_alignments(
|
|
136
|
+
events: list[RuntimeEvent],
|
|
137
|
+
relations: list[CorrelationRelation],
|
|
138
|
+
*,
|
|
139
|
+
min_anchors: int = 3,
|
|
140
|
+
) -> list[ClockAlignment]:
|
|
141
|
+
"""Fit affine clock mappings using CORRELATED_EXACT anchors only.
|
|
142
|
+
|
|
143
|
+
Possible correlations are deliberately excluded to avoid circularly turning a
|
|
144
|
+
weak payload/time coincidence into stronger temporal evidence.
|
|
145
|
+
"""
|
|
146
|
+
by_index = {e.event_index: e for e in events}
|
|
147
|
+
pairs: dict[tuple[str, str], list[tuple[float, float]]] = defaultdict(list)
|
|
148
|
+
for rel in relations:
|
|
149
|
+
if rel.relation_type != RuntimeRelationType.CORRELATED_EXACT:
|
|
150
|
+
continue
|
|
151
|
+
left, right = by_index.get(rel.left_event_index), by_index.get(rel.right_event_index)
|
|
152
|
+
if left is None or right is None or left.timestamp is None or right.timestamp is None:
|
|
153
|
+
continue
|
|
154
|
+
ld = left.clock_domain or left.cpu_hint or "default"
|
|
155
|
+
rd = right.clock_domain or right.cpu_hint or "default"
|
|
156
|
+
if ld == rd:
|
|
157
|
+
continue
|
|
158
|
+
pairs[(ld, rd)].append((_datetime_seconds(left.timestamp), _datetime_seconds(right.timestamp)))
|
|
159
|
+
|
|
160
|
+
out: list[ClockAlignment] = []
|
|
161
|
+
for (source, target), anchors in sorted(pairs.items()):
|
|
162
|
+
if len(anchors) < min_anchors:
|
|
163
|
+
continue
|
|
164
|
+
xs = [x for x, _ in anchors]
|
|
165
|
+
ys = [y for _, y in anchors]
|
|
166
|
+
xbar, ybar = sum(xs) / len(xs), sum(ys) / len(ys)
|
|
167
|
+
denom = sum((x - xbar) ** 2 for x in xs)
|
|
168
|
+
scale = 1.0 if denom == 0 else sum((x - xbar) * (y - ybar) for x, y in anchors) / denom
|
|
169
|
+
offset = ybar - scale * xbar
|
|
170
|
+
residuals = [abs((scale * x + offset) - y) for x, y in anchors]
|
|
171
|
+
max_residual = max(residuals, default=0.0)
|
|
172
|
+
if max_residual <= 0.001:
|
|
173
|
+
confidence = "HIGH"
|
|
174
|
+
elif max_residual <= 0.100:
|
|
175
|
+
confidence = "MEDIUM"
|
|
176
|
+
else:
|
|
177
|
+
confidence = "LOW"
|
|
178
|
+
out.append(ClockAlignment(
|
|
179
|
+
source_domain=source, target_domain=target, scale=scale,
|
|
180
|
+
offset_seconds=offset, anchor_count=len(anchors),
|
|
181
|
+
max_abs_residual_seconds=max_residual, confidence=confidence,
|
|
182
|
+
))
|
|
183
|
+
return out
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _datetime_seconds(value) -> float:
|
|
187
|
+
# Stable naive-clock numeric representation without local-timezone conversion.
|
|
188
|
+
return (
|
|
189
|
+
value.toordinal() * 86400.0
|
|
190
|
+
+ value.hour * 3600.0
|
|
191
|
+
+ value.minute * 60.0
|
|
192
|
+
+ value.second
|
|
193
|
+
+ value.microsecond / 1_000_000.0
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def reconstruct_static_paths(
|
|
198
|
+
index: ProjectIndex,
|
|
199
|
+
start_function: str,
|
|
200
|
+
end_function: str,
|
|
201
|
+
*,
|
|
202
|
+
max_paths: int = 8,
|
|
203
|
+
max_depth: int = 64,
|
|
204
|
+
) -> ReconstructedPath:
|
|
205
|
+
graph: dict[str, set[str]] = defaultdict(set)
|
|
206
|
+
functions_by_id = {f.qualified_id: f for f in index.functions()}
|
|
207
|
+
|
|
208
|
+
for call in index.callsites:
|
|
209
|
+
targets: list[str] = []
|
|
210
|
+
if call.target_kind == CallTargetKind.INTERNAL_EXACT and call.target_function_id:
|
|
211
|
+
target = functions_by_id.get(call.target_function_id)
|
|
212
|
+
if target:
|
|
213
|
+
targets.append(target.name)
|
|
214
|
+
elif call.target_kind in {CallTargetKind.INTERNAL_POSSIBLE, CallTargetKind.INDIRECT_EXACT, CallTargetKind.INDIRECT_POSSIBLE}:
|
|
215
|
+
for tid in call.target_function_ids:
|
|
216
|
+
target = functions_by_id.get(tid)
|
|
217
|
+
if target:
|
|
218
|
+
targets.append(target.name)
|
|
219
|
+
for target_name in targets:
|
|
220
|
+
graph[call.caller_name].add(target_name)
|
|
221
|
+
|
|
222
|
+
paths: list[tuple[str, ...]] = []
|
|
223
|
+
q: deque[tuple[str, tuple[str, ...]]] = deque([(start_function, (start_function,))])
|
|
224
|
+
shortest_len: int | None = None
|
|
225
|
+
while q and len(paths) < max_paths:
|
|
226
|
+
node, path = q.popleft()
|
|
227
|
+
if shortest_len is not None and len(path) > shortest_len:
|
|
228
|
+
continue
|
|
229
|
+
if len(path) > max_depth:
|
|
230
|
+
continue
|
|
231
|
+
if node == end_function:
|
|
232
|
+
paths.append(path)
|
|
233
|
+
shortest_len = len(path) if shortest_len is None else shortest_len
|
|
234
|
+
continue
|
|
235
|
+
for nxt in sorted(graph.get(node, ())):
|
|
236
|
+
if nxt in path: # avoid recursion/SCC infinite expansion in the PoC path search
|
|
237
|
+
continue
|
|
238
|
+
q.append((nxt, path + (nxt,)))
|
|
239
|
+
|
|
240
|
+
if not paths:
|
|
241
|
+
state = ReconstructionState.UNKNOWN_GAP
|
|
242
|
+
elif len(paths) == 1:
|
|
243
|
+
state = ReconstructionState.EXACT_PATH
|
|
244
|
+
else:
|
|
245
|
+
state = ReconstructionState.AMBIGUOUS_PATH
|
|
246
|
+
return ReconstructedPath(start_function, end_function, state, tuple(paths))
|
|
247
|
+
|
|
248
|
+
@dataclass(frozen=True, slots=True)
|
|
249
|
+
class MarkerSuggestion:
|
|
250
|
+
function_name: str
|
|
251
|
+
distinguished_path_pairs: int
|
|
252
|
+
total_path_pairs: int
|
|
253
|
+
information_gain_score: float
|
|
254
|
+
reason: str
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def suggest_markers_for_paths(paths: tuple[tuple[str, ...], ...], *, count: int = 1) -> list[MarkerSuggestion]:
|
|
258
|
+
"""Rank function nodes by how many ambiguous path pairs they distinguish.
|
|
259
|
+
|
|
260
|
+
This is the PoC core of marker suggestion. Entry/end nodes and nodes shared
|
|
261
|
+
by every path have zero discrimination and are excluded. Runtime cost and
|
|
262
|
+
existing-log reuse are intentionally separate scoring factors to add later.
|
|
263
|
+
"""
|
|
264
|
+
if len(paths) < 2 or count <= 0:
|
|
265
|
+
return []
|
|
266
|
+
path_sets = [set(p) for p in paths]
|
|
267
|
+
endpoints = {paths[0][0], paths[0][-1]}
|
|
268
|
+
candidates = sorted(set().union(*path_sets) - endpoints)
|
|
269
|
+
total_pairs = len(paths) * (len(paths) - 1) // 2
|
|
270
|
+
ranked: list[MarkerSuggestion] = []
|
|
271
|
+
for fn in candidates:
|
|
272
|
+
distinguished = 0
|
|
273
|
+
for i in range(len(paths)):
|
|
274
|
+
for j in range(i + 1, len(paths)):
|
|
275
|
+
if (fn in path_sets[i]) != (fn in path_sets[j]):
|
|
276
|
+
distinguished += 1
|
|
277
|
+
if distinguished == 0:
|
|
278
|
+
continue
|
|
279
|
+
score = distinguished / total_pairs if total_pairs else 0.0
|
|
280
|
+
ranked.append(MarkerSuggestion(
|
|
281
|
+
function_name=fn,
|
|
282
|
+
distinguished_path_pairs=distinguished,
|
|
283
|
+
total_path_pairs=total_pairs,
|
|
284
|
+
information_gain_score=score,
|
|
285
|
+
reason=f"present on a subset of ambiguous paths; distinguishes {distinguished}/{total_pairs} path pairs",
|
|
286
|
+
))
|
|
287
|
+
ranked.sort(key=lambda x: (-x.information_gain_score, x.function_name))
|
|
288
|
+
return ranked[:count]
|
|
289
|
+
|
|
290
|
+
@dataclass(frozen=True, slots=True)
|
|
291
|
+
class MarkerCandidateContext:
|
|
292
|
+
has_existing_log: bool = False
|
|
293
|
+
hotness: float = 0.0
|
|
294
|
+
source_stability: float = 1.0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def score_marker_suggestions(
|
|
298
|
+
paths: tuple[tuple[str, ...], ...],
|
|
299
|
+
*,
|
|
300
|
+
count: int = 1,
|
|
301
|
+
contexts: dict[str, MarkerCandidateContext] | None = None,
|
|
302
|
+
) -> list[MarkerSuggestion]:
|
|
303
|
+
"""Rank ambiguous-path marker points with transparent multi-factor scoring.
|
|
304
|
+
|
|
305
|
+
Information gain remains the dominant factor. Existing-log reuse is a bonus;
|
|
306
|
+
hot-path and unstable-source locations are penalties. All factors are bounded
|
|
307
|
+
so a low-information point cannot outrank a clearly discriminating point only
|
|
308
|
+
because it already has a log call.
|
|
309
|
+
"""
|
|
310
|
+
contexts = contexts or {}
|
|
311
|
+
base = suggest_markers_for_paths(paths, count=10_000)
|
|
312
|
+
rescored: list[MarkerSuggestion] = []
|
|
313
|
+
for item in base:
|
|
314
|
+
ctx = contexts.get(item.function_name, MarkerCandidateContext())
|
|
315
|
+
hotness = min(max(ctx.hotness, 0.0), 1.0)
|
|
316
|
+
stability = min(max(ctx.source_stability, 0.0), 1.0)
|
|
317
|
+
reuse_bonus = 0.12 if ctx.has_existing_log else 0.0
|
|
318
|
+
hot_penalty = 0.20 * hotness
|
|
319
|
+
instability_penalty = 0.10 * (1.0 - stability)
|
|
320
|
+
total = max(0.0, item.information_gain_score + reuse_bonus - hot_penalty - instability_penalty)
|
|
321
|
+
reason = (
|
|
322
|
+
f"information_gain={item.information_gain_score:.3f}; "
|
|
323
|
+
f"existing_log_bonus={reuse_bonus:.3f}; "
|
|
324
|
+
f"hotness_penalty={hot_penalty:.3f}; "
|
|
325
|
+
f"source_instability_penalty={instability_penalty:.3f}; "
|
|
326
|
+
f"total={total:.3f}"
|
|
327
|
+
)
|
|
328
|
+
rescored.append(MarkerSuggestion(
|
|
329
|
+
function_name=item.function_name,
|
|
330
|
+
distinguished_path_pairs=item.distinguished_path_pairs,
|
|
331
|
+
total_path_pairs=item.total_path_pairs,
|
|
332
|
+
information_gain_score=total,
|
|
333
|
+
reason=reason,
|
|
334
|
+
))
|
|
335
|
+
rescored.sort(key=lambda x: (-x.information_gain_score, x.function_name))
|
|
336
|
+
return rescored[:count]
|
cseq/trace_diff.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from .runtime import TraceSession
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class TraceDiffResult:
|
|
10
|
+
left_session: str
|
|
11
|
+
right_session: str
|
|
12
|
+
added_markers: tuple[tuple[str, int], ...]
|
|
13
|
+
missing_markers: tuple[tuple[str, int], ...]
|
|
14
|
+
changed_cpu: tuple[tuple[str, tuple[str, ...], tuple[str, ...]], ...]
|
|
15
|
+
changed_task: tuple[tuple[str, tuple[str, ...], tuple[str, ...]], ...]
|
|
16
|
+
order_changes: tuple[tuple[str, int, int], ...] = ()
|
|
17
|
+
timing_changes: tuple[tuple[str, str | None, str | None], ...] = ()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def diff_sessions(left: TraceSession, right: TraceSession) -> TraceDiffResult:
|
|
21
|
+
lc = Counter(e.marker_id for e in left.events)
|
|
22
|
+
rc = Counter(e.marker_id for e in right.events)
|
|
23
|
+
added = tuple(sorted((m, rc[m] - lc[m]) for m in rc if rc[m] > lc[m]))
|
|
24
|
+
missing = tuple(sorted((m, lc[m] - rc[m]) for m in lc if lc[m] > rc[m]))
|
|
25
|
+
|
|
26
|
+
def domains(session: TraceSession, attr: str) -> dict[str, tuple[str, ...]]:
|
|
27
|
+
out: dict[str, set[str]] = {}
|
|
28
|
+
for e in session.events:
|
|
29
|
+
value = getattr(e, attr)
|
|
30
|
+
if value:
|
|
31
|
+
out.setdefault(e.marker_id, set()).add(value)
|
|
32
|
+
return {k: tuple(sorted(v)) for k, v in out.items()}
|
|
33
|
+
|
|
34
|
+
lcpu, rcpu = domains(left, "cpu_hint"), domains(right, "cpu_hint")
|
|
35
|
+
ltask, rtask = domains(left, "task_hint"), domains(right, "task_hint")
|
|
36
|
+
shared = sorted(set(lc) & set(rc))
|
|
37
|
+
changed_cpu = tuple((m, lcpu.get(m, ()), rcpu.get(m, ())) for m in shared if lcpu.get(m, ()) != rcpu.get(m, ()))
|
|
38
|
+
changed_task = tuple((m, ltask.get(m, ()), rtask.get(m, ())) for m in shared if ltask.get(m, ()) != rtask.get(m, ()))
|
|
39
|
+
|
|
40
|
+
def first_positions(session: TraceSession) -> dict[str, int]:
|
|
41
|
+
out: dict[str, int] = {}
|
|
42
|
+
for e in session.events:
|
|
43
|
+
out.setdefault(e.marker_id, e.event_index)
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
def first_times(session: TraceSession) -> dict[str, str | None]:
|
|
47
|
+
out: dict[str, str | None] = {}
|
|
48
|
+
for e in session.events:
|
|
49
|
+
out.setdefault(e.marker_id, e.timestamp_raw)
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
lp, rp = first_positions(left), first_positions(right)
|
|
53
|
+
lt, rt = first_times(left), first_times(right)
|
|
54
|
+
order_changes = tuple((m, lp[m], rp[m]) for m in shared if lp[m] != rp[m])
|
|
55
|
+
timing_changes = tuple((m, lt.get(m), rt.get(m)) for m in shared if lt.get(m) != rt.get(m))
|
|
56
|
+
return TraceDiffResult(left.name, right.name, added, missing, changed_cpu, changed_task, order_changes, timing_changes)
|