sourcetrail/: Sourcetrail 2021.4.19 indexing pipeline for the wasm port — compile-db transform (rsp expansion, PCH strip, libc++-11 pinning, path rewrites), removelist-candidate analysis over the indexed call graph, README with regen steps and the hard-won tricks (relative --project-file hang, SDK-header poisoning). Heavy artifacts (.srctrldb, compile dbs, libcxx headers) stay untracked via the folder's .gitignore. docs/asyncify-allocator-suspend/plan.md: verified plan for the nanosleep-shim zero-duration guard (mimalloc mi_atomic_yield=sleep(0) can suspend malloc on the main thread), the red/green contention test, and the adversarially verified removelist additions with measured payoff (80.9k -> 60.0k instrumented functions). Planned, not yet executed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2NHhbzuEHqP2JmcrSD96D
125 lines
5 KiB
Python
125 lines
5 KiB
Python
#!/usr/bin/env python3
|
|
"""Rank subsystems by asyncify-removelist safety using the Sourcetrail db.
|
|
|
|
Taint = "a suspend point is reachable from this function" (reverse BFS over
|
|
call edges from suspend primitives, with base->override pseudo-edges for
|
|
virtual dispatch). Two levels:
|
|
strict — direct suspend primitives only (Yield/Sleep/ShowModal/progress/
|
|
coroutine/fiber)
|
|
lenient — strict + synchronous event dispatch (ProcessEvent & co), since a
|
|
dispatched handler may suspend and unwind through the dispatcher
|
|
|
|
Functions never tainted even in lenient mode are removelist candidates.
|
|
Blind spots (validate with ASYNCIFY_ADVISE before shipping): std::function /
|
|
event-table indirection, function pointers, wx/libc internals outside the cdb.
|
|
"""
|
|
import re
|
|
import sqlite3
|
|
from collections import defaultdict, deque
|
|
|
|
DB = "kicad-wasm.srctrldb"
|
|
PCBJAM = "/Users/V/IdeaProjects/pcbjam-private/pcbjam/"
|
|
FUNC_TYPES = (4096, 8192)
|
|
|
|
STRICT_PATTERNS = [
|
|
r"\tnShowModal", # every ShowModal incl. DIALOG_SHIM/KIDIALOG wrappers
|
|
r"\tnShowWindowModal",
|
|
r"\tnShowQuasiModal",
|
|
r"mwxYield", r"mwxSafeYield", r"mwxYieldIfNeeded",
|
|
r"\tnYield", r"\tnYieldFor", r"\tnDoYieldFor", r"\tnSafeYield",
|
|
r"\tnSleep", r"mwxSleep", r"mwxMilliSleep", r"mwxMicroSleep",
|
|
r"sleep_for", r"sleep_until", r"nanosleep", r"emscripten_sleep",
|
|
r"emscripten_fiber", r"__asyncjs__",
|
|
r"mwxGenericProgressDialog", r"mwxProgressDialog",
|
|
r"mwxMessageBox", r"mwxExecute",
|
|
r"mCOROUTINE<",
|
|
]
|
|
LENIENT_EXTRA = [
|
|
r"\tnProcessEvent\t", r"\tnSafelyProcessEvent", r"\tnProcessPendingEvents",
|
|
r"\tnHandleEvent\t", r"\tnProcessEventLocally",
|
|
]
|
|
|
|
|
|
def main():
|
|
con = sqlite3.connect(DB)
|
|
|
|
funcs = {} # id -> serialized_name
|
|
for nid, name in con.execute(
|
|
f"SELECT id, serialized_name FROM node WHERE type IN {FUNC_TYPES}"):
|
|
funcs[nid] = name
|
|
|
|
# callee -> callers (reverse call graph)
|
|
rev = defaultdict(list)
|
|
for s, t in con.execute("SELECT source_node_id, target_node_id FROM edge WHERE type=8"):
|
|
rev[t].append(s)
|
|
# virtual dispatch: caller of Base::f may land in Derived::f, so taint of
|
|
# Derived::f must flow to callers of Base::f -> pseudo-edge callee=Derived,
|
|
# caller-side=Base is wrong; we need: if Derived tainted then Base tainted
|
|
# is NOT true. Correct: call to Base::f can dispatch to Derived::f, so if
|
|
# Derived::f suspends, callers of Base::f suspend. Model: rev[Derived] gets
|
|
# nothing; instead treat Base::f as a caller of every override Derived::f.
|
|
for s, t in con.execute("SELECT source_node_id, target_node_id FROM edge WHERE type=32"):
|
|
# override edge: Derived::f (source) -> Base::f (target)
|
|
rev[s].append(t) # taint flows Derived -> Base -> Base's callers
|
|
|
|
def seeds_for(patterns):
|
|
pats = [re.compile(p) for p in patterns]
|
|
return {nid for nid, name in funcs.items() if any(p.search(name) for p in pats)}
|
|
|
|
def closure(seed_ids):
|
|
seen = set(seed_ids)
|
|
q = deque(seed_ids)
|
|
while q:
|
|
n = q.popleft()
|
|
for caller in rev.get(n, ()):
|
|
if caller not in seen:
|
|
seen.add(caller)
|
|
q.append(caller)
|
|
return seen
|
|
|
|
strict_seeds = seeds_for(STRICT_PATTERNS)
|
|
lenient_seeds = strict_seeds | seeds_for(STRICT_PATTERNS + LENIENT_EXTRA)
|
|
print(f"functions: {len(funcs)} strict seeds: {len(strict_seeds)} "
|
|
f"lenient seeds: {len(lenient_seeds)}")
|
|
|
|
tainted_strict = closure(strict_seeds)
|
|
tainted_lenient = closure(lenient_seeds)
|
|
|
|
# function -> file (prefer definition scope locations, type=1)
|
|
loc = {}
|
|
for typ in (1, 0):
|
|
for eid, path in con.execute(
|
|
"SELECT o.element_id, f.path FROM occurrence o "
|
|
"JOIN source_location sl ON sl.id=o.source_location_id "
|
|
"JOIN file f ON f.id=sl.file_node_id WHERE sl.type=?", (typ,)):
|
|
if eid in funcs and eid not in loc:
|
|
loc[eid] = path
|
|
|
|
def module(path):
|
|
if not path.startswith(PCBJAM):
|
|
return None # std/emsdk/deps headers — not our code
|
|
rel = path[len(PCBJAM):]
|
|
parts = rel.split("/")
|
|
depth = 3 if parts[0] in ("kicad", "build-wasm") else 2
|
|
return "/".join(parts[:depth]) if len(parts) > depth else "/".join(parts[:-1])
|
|
|
|
stats = defaultdict(lambda: [0, 0, 0]) # module -> [total, strict, lenient]
|
|
for nid in funcs:
|
|
m = module(loc.get(nid, ""))
|
|
if m is None:
|
|
continue
|
|
stats[m][0] += 1
|
|
if nid in tainted_strict:
|
|
stats[m][1] += 1
|
|
if nid in tainted_lenient:
|
|
stats[m][2] += 1
|
|
|
|
rows = [(m, t, s, l, t - l) for m, (t, s, l) in stats.items() if t >= 20]
|
|
rows.sort(key=lambda r: -r[4])
|
|
print(f"\n{'module':<44}{'funcs':>7}{'strict✗':>9}{'lenient✗':>9}{'clean':>7}{'clean%':>8}")
|
|
for m, t, s, l, clean in rows[:45]:
|
|
print(f"{m:<44}{t:>7}{s:>9}{l:>9}{clean:>7}{100*clean//t:>7}%")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|