|
@@ -53,6 +53,7 @@ import type { Edge, Node } from '../types';
|
|
|
import type { QueryBuilder } from '../db/queries';
|
|
import type { QueryBuilder } from '../db/queries';
|
|
|
import type { ResolutionContext } from './types';
|
|
import type { ResolutionContext } from './types';
|
|
|
import type { MaybeYield } from './cooperative-yield';
|
|
import type { MaybeYield } from './cooperative-yield';
|
|
|
|
|
+import { memoryBudgetBytes } from './memory-budget';
|
|
|
import { LRUCache } from './lru-cache';
|
|
import { LRUCache } from './lru-cache';
|
|
|
import { stripCommentsForRegex } from './strip-comments';
|
|
import { stripCommentsForRegex } from './strip-comments';
|
|
|
|
|
|
|
@@ -70,9 +71,13 @@ interface FieldInfo {
|
|
|
type: string;
|
|
type: string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function sliceLines(content: string, startLine?: number, endLine?: number): string {
|
|
|
|
|
|
|
+/** Slice a node's body from a pre-split line array — the per-file sweeps (B/D/E)
|
|
|
|
|
+ * call this once per NODE, and splitting the whole file per node was an
|
|
|
|
|
+ * O(nodes × file-size) term (~1.6M full-file splits on the Linux tree,
|
|
|
|
|
+ * §7a.3 cFnPtr round). Split once per file, slice many times. */
|
|
|
|
|
+function sliceLinesPre(lines: string[], startLine?: number, endLine?: number): string {
|
|
|
if (!startLine) return '';
|
|
if (!startLine) return '';
|
|
|
- return content.split('\n').slice(startLine - 1, endLine ?? startLine).join('\n');
|
|
|
|
|
|
|
+ return lines.slice(startLine - 1, endLine ?? startLine).join('\n');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Index of the `}` matching the `{` at `open` (which must point at a `{`). -1 if unbalanced. */
|
|
/** Index of the `}` matching the `{` at `open` (which must point at a `{`). -1 if unbalanced. */
|
|
@@ -318,6 +323,13 @@ export async function cFnPointerDispatchEdges(
|
|
|
const files = ctx.getAllFiles().filter((f) => C_CPP_EXT.test(f));
|
|
const files = ctx.getAllFiles().filter((f) => C_CPP_EXT.test(f));
|
|
|
if (files.length === 0) return [];
|
|
if (files.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
+ // CODEGRAPH_SYNTH_TIMINGS sub-attribution: this pass is 86% of kernel-scale
|
|
|
|
|
+ // synthesis (306s, §7a.2/§7a.3) — per-sweep walls + read/strip accounting
|
|
|
|
|
+ // name which sweep and which cost class owns it.
|
|
|
|
|
+ const prof = process.env.CODEGRAPH_SYNTH_TIMINGS
|
|
|
|
|
+ ? { A: 0, B: 0, C: 0, D: 0, E: 0, readMs: 0, readN: 0, stripMs: 0, stripN: 0, nodesMs: 0, nodesN: 0 }
|
|
|
|
|
+ : null;
|
|
|
|
|
+
|
|
|
// Within-pass progress: this is the pass that parks the "Linking dynamic
|
|
// Within-pass progress: this is the pass that parks the "Linking dynamic
|
|
|
// dispatch" bar on C-heavy repos, so it reports a real fraction of its
|
|
// dispatch" bar on C-heavy repos, so it reports a real fraction of its
|
|
|
// dominant work. `files` is swept once per file loop below (passes A, C, D,
|
|
// dominant work. `files` is swept once per file loop below (passes A, C, D,
|
|
@@ -337,14 +349,30 @@ export async function cFnPointerDispatchEdges(
|
|
|
// Every sweep below iterates in `files` order, and node-kind scans return
|
|
// Every sweep below iterates in `files` order, and node-kind scans return
|
|
|
// rows in file-commit order, so access is near-sequential and a small LRU
|
|
// rows in file-commit order, so access is near-sequential and a small LRU
|
|
|
// hits; a miss just re-reads + re-strips.
|
|
// hits; a miss just re-reads + re-strips.
|
|
|
- const rawCache = new LRUCache<string, string | null>(128);
|
|
|
|
|
|
|
+ // Cache sizing is memory-budget-aware AND all-or-nothing (§7a.3 cFnPtr
|
|
|
|
|
+ // round): the flat 128 caused 4.4 strips/file across the pass's four file
|
|
|
|
|
+ // sweeps — 71.8s of the kernel-scale wall — and a partial LRU is WORSE than
|
|
|
|
|
+ // useless for cyclic sweeps (a first attempt sized ~61k against 63.8k files
|
|
|
|
|
+ // and thrashed to a ~0% cross-sweep hit rate). Hold every stripped file
|
|
|
|
|
+ // (~24KB each measured on the Linux tree) only when 40% of the live memory
|
|
|
|
|
+ // budget covers it; otherwise keep the old within-sweep-locality 128. The
|
|
|
|
|
+ // cache is pass-scoped — freed on return.
|
|
|
|
|
+ // Slack over files.length: non-indexed includes (.def/.inc, generated
|
|
|
|
|
+ // headers) join the working set mid-pass, and even a handful of keys past
|
|
|
|
|
+ // the cap re-triggers cyclic eviction (measured: cap==files.length still
|
|
|
|
|
+ // stripped 2.25×/file). Pass-scoped transient, freed on return.
|
|
|
|
|
+ const fullCacheCap = Math.ceil(files.length * 1.05) + 512;
|
|
|
|
|
+ const cacheCap = memoryBudgetBytes() * 0.5 >= fullCacheCap * 24_576 ? fullCacheCap : 128;
|
|
|
|
|
+ const rawCache = new LRUCache<string, string | null>(Math.min(cacheCap, 4096));
|
|
|
const raw = (file: string): string | null => {
|
|
const raw = (file: string): string | null => {
|
|
|
if (rawCache.has(file)) return rawCache.get(file)!;
|
|
if (rawCache.has(file)) return rawCache.get(file)!;
|
|
|
|
|
+ const t0 = prof ? Date.now() : 0;
|
|
|
const r = ctx.readFile(file);
|
|
const r = ctx.readFile(file);
|
|
|
|
|
+ if (prof) { prof.readMs += Date.now() - t0; prof.readN++; }
|
|
|
rawCache.set(file, r);
|
|
rawCache.set(file, r);
|
|
|
return r;
|
|
return r;
|
|
|
};
|
|
};
|
|
|
- const srcCache = new LRUCache<string, string>(128);
|
|
|
|
|
|
|
+ const srcCache = new LRUCache<string, string>(cacheCap);
|
|
|
const src = (file: string): string | null => {
|
|
const src = (file: string): string | null => {
|
|
|
// A cached '' (empty or unreadable file) returns '' where the miss path
|
|
// A cached '' (empty or unreadable file) returns '' where the miss path
|
|
|
// returns null for unreadable — every caller falsy-checks, so the two are
|
|
// returns null for unreadable — every caller falsy-checks, so the two are
|
|
@@ -352,7 +380,9 @@ export async function cFnPointerDispatchEdges(
|
|
|
const hit = srcCache.get(file);
|
|
const hit = srcCache.get(file);
|
|
|
if (hit !== undefined) return hit;
|
|
if (hit !== undefined) return hit;
|
|
|
const r = raw(file);
|
|
const r = raw(file);
|
|
|
|
|
+ const t0 = prof ? Date.now() : 0;
|
|
|
const s = r == null ? '' : stripCommentsForRegex(r, 'c');
|
|
const s = r == null ? '' : stripCommentsForRegex(r, 'c');
|
|
|
|
|
+ if (prof) { prof.stripMs += Date.now() - t0; prof.stripN++; }
|
|
|
srcCache.set(file, s);
|
|
srcCache.set(file, s);
|
|
|
return r == null ? null : s;
|
|
return r == null ? null : s;
|
|
|
};
|
|
};
|
|
@@ -375,6 +405,7 @@ export async function cFnPointerDispatchEdges(
|
|
|
// declared as `redisCommandProc *proc;`. Without this, `proc` reads as data.
|
|
// declared as `redisCommandProc *proc;`. Without this, `proc` reads as data.
|
|
|
const fnPtrTypedefs = new Set<string>();
|
|
const fnPtrTypedefs = new Set<string>();
|
|
|
const fnTypeTypedefs = new Set<string>();
|
|
const fnTypeTypedefs = new Set<string>();
|
|
|
|
|
+ let tPass = Date.now();
|
|
|
for (const file of files) {
|
|
for (const file of files) {
|
|
|
await tick();
|
|
await tick();
|
|
|
const s = src(file);
|
|
const s = src(file);
|
|
@@ -390,6 +421,7 @@ export async function cFnPointerDispatchEdges(
|
|
|
if (fm && !C_TYPE_KEYWORDS.has(fm[1]!)) fnTypeTypedefs.add(fm[1]!);
|
|
if (fm && !C_TYPE_KEYWORDS.has(fm[1]!)) fnTypeTypedefs.add(fm[1]!);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ if (prof) { prof.A = Date.now() - tPass; tPass = Date.now(); }
|
|
|
|
|
|
|
|
// ---- Pass B: struct field layouts ----
|
|
// ---- Pass B: struct field layouts ----
|
|
|
// structLayout: struct name → ordered fields, for structs with ≥1 fn-pointer
|
|
// structLayout: struct name → ordered fields, for structs with ≥1 fn-pointer
|
|
@@ -456,17 +488,21 @@ export async function cFnPointerDispatchEdges(
|
|
|
if (fields.some((f) => f.isFnPtr)) structLayout.set(name, fields);
|
|
if (fields.some((f) => f.isFnPtr)) structLayout.set(name, fields);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ let linesFile = '';
|
|
|
|
|
+ let linesArr: string[] = [];
|
|
|
for (const st of (ctx.iterateNodesByKind?.('struct') ?? ctx.getNodesByKind('struct'))) {
|
|
for (const st of (ctx.iterateNodesByKind?.('struct') ?? ctx.getNodesByKind('struct'))) {
|
|
|
if ((++scannedFiles & 255) === 0) await onYield();
|
|
if ((++scannedFiles & 255) === 0) await onYield();
|
|
|
if (!C_CPP_EXT.test(st.filePath)) continue;
|
|
if (!C_CPP_EXT.test(st.filePath)) continue;
|
|
|
const s = src(st.filePath);
|
|
const s = src(st.filePath);
|
|
|
if (!s) continue;
|
|
if (!s) continue;
|
|
|
- const body = sliceLines(s, st.startLine, st.endLine);
|
|
|
|
|
|
|
+ if (linesFile !== st.filePath) { linesFile = st.filePath; linesArr = s.split('\n'); }
|
|
|
|
|
+ const body = sliceLinesPre(linesArr, st.startLine, st.endLine);
|
|
|
const open = body.indexOf('{');
|
|
const open = body.indexOf('{');
|
|
|
const close = open >= 0 ? matchBrace(body, open) : -1;
|
|
const close = open >= 0 ? matchBrace(body, open) : -1;
|
|
|
if (open < 0 || close < 0) continue;
|
|
if (open < 0 || close < 0) continue;
|
|
|
registerStructLayout(st.name, parseStructFields(body.slice(open + 1, close)));
|
|
registerStructLayout(st.name, parseStructFields(body.slice(open + 1, close)));
|
|
|
}
|
|
}
|
|
|
|
|
+ if (prof) { prof.B = Date.now() - tPass; tPass = Date.now(); }
|
|
|
// NB: no early return on an empty structLayout here — an inline `struct TAG
|
|
// NB: no early return on an empty structLayout here — an inline `struct TAG
|
|
|
// { … } var[]` table whose struct never became a node (vim's `cmdname`, broken
|
|
// { … } var[]` table whose struct never became a node (vim's `cmdname`, broken
|
|
|
// up by `#ifdef`) is discovered later during the unit scan. The `reg.size === 0`
|
|
// up by `#ifdef`) is discovered later during the unit scan. The `reg.size === 0`
|
|
@@ -813,12 +849,19 @@ export async function cFnPointerDispatchEdges(
|
|
|
processUnit({ text, file: target, env: incEnv, objEnv: incObjEnv });
|
|
processUnit({ text, file: target, env: incEnv, objEnv: incObjEnv });
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ if (prof) { prof.C = Date.now() - tPass; tPass = Date.now(); }
|
|
|
|
|
|
|
|
// ---- receiver-type resolution within a function's source ----
|
|
// ---- receiver-type resolution within a function's source ----
|
|
|
// `(?:struct )?TYPE [*]recv` declared in the params or body → TYPE (if a known
|
|
// `(?:struct )?TYPE [*]recv` declared in the params or body → TYPE (if a known
|
|
|
// fn-pointer-bearing struct).
|
|
// fn-pointer-bearing struct).
|
|
|
|
|
+ const recvReCache = new Map<string, RegExp>();
|
|
|
const recvTypeIn = (fnSrc: string, recv: string): string | null => {
|
|
const recvTypeIn = (fnSrc: string, recv: string): string | null => {
|
|
|
- const re = new RegExp(`(?:struct\\s+)?(\\w+)\\s*\\*?\\s*\\b${recv}\\b\\s*(?:[,)=;]|\\[)`, 'g');
|
|
|
|
|
|
|
+ let re = recvReCache.get(recv);
|
|
|
|
|
+ if (!re) {
|
|
|
|
|
+ re = new RegExp(`(?:struct\\s+)?(\\w+)\\s*\\*?\\s*\\b${recv}\\b\\s*(?:[,)=;]|\\[)`, 'g');
|
|
|
|
|
+ recvReCache.set(recv, re);
|
|
|
|
|
+ }
|
|
|
|
|
+ re.lastIndex = 0;
|
|
|
let m: RegExpExecArray | null;
|
|
let m: RegExpExecArray | null;
|
|
|
while ((m = re.exec(fnSrc))) {
|
|
while ((m = re.exec(fnSrc))) {
|
|
|
if (structLayout.has(m[1]!)) return m[1]!;
|
|
if (structLayout.has(m[1]!)) return m[1]!;
|
|
@@ -830,8 +873,14 @@ export async function cFnPointerDispatchEdges(
|
|
|
// structs (the base of a chained receiver needn't carry a fn pointer itself).
|
|
// structs (the base of a chained receiver needn't carry a fn pointer itself).
|
|
|
// Falls back to a file-scope table variable (`cmdnames` in `cmdnames[i].fn()`).
|
|
// Falls back to a file-scope table variable (`cmdnames` in `cmdnames[i].fn()`).
|
|
|
const escapeRe = (x: string): string => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const escapeRe = (x: string): string => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
|
+ const varReCache = new Map<string, RegExp>();
|
|
|
const varTypeIn = (fnSrc: string, v: string): string | null => {
|
|
const varTypeIn = (fnSrc: string, v: string): string | null => {
|
|
|
- const re = new RegExp(`(?:struct\\s+)?(\\w+)\\s*\\*?\\s*\\b${escapeRe(v)}\\b\\s*(?:[,)=;]|\\[)`, 'g');
|
|
|
|
|
|
|
+ let re = varReCache.get(v);
|
|
|
|
|
+ if (!re) {
|
|
|
|
|
+ re = new RegExp(`(?:struct\\s+)?(\\w+)\\s*\\*?\\s*\\b${escapeRe(v)}\\b\\s*(?:[,)=;]|\\[)`, 'g');
|
|
|
|
|
+ varReCache.set(v, re);
|
|
|
|
|
+ }
|
|
|
|
|
+ re.lastIndex = 0;
|
|
|
let m: RegExpExecArray | null;
|
|
let m: RegExpExecArray | null;
|
|
|
while ((m = re.exec(fnSrc))) {
|
|
while ((m = re.exec(fnSrc))) {
|
|
|
if (!C_TYPE_KEYWORDS.has(m[1]!)) return m[1]!;
|
|
if (!C_TYPE_KEYWORDS.has(m[1]!)) return m[1]!;
|
|
@@ -867,14 +916,23 @@ export async function cFnPointerDispatchEdges(
|
|
|
await tick();
|
|
await tick();
|
|
|
const s = src(file);
|
|
const s = src(file);
|
|
|
if (!s || !s.includes('=')) continue;
|
|
if (!s || !s.includes('=')) continue;
|
|
|
- for (const fn of ctx.getNodesInFile(file)) {
|
|
|
|
|
|
|
+ const tN = prof ? Date.now() : 0;
|
|
|
|
|
+ const fnsD = ctx.getNodesInFile(file);
|
|
|
|
|
+ if (prof) { prof.nodesMs += Date.now() - tN; prof.nodesN++; }
|
|
|
|
|
+ const dLines = s.split('\n');
|
|
|
|
|
+ for (const fn of fnsD) {
|
|
|
if (!FN_KINDS.has(fn.kind)) continue;
|
|
if (!FN_KINDS.has(fn.kind)) continue;
|
|
|
- const body = sliceLines(s, fn.startLine, fn.endLine);
|
|
|
|
|
|
|
+ const body = sliceLinesPre(dLines, fn.startLine, fn.endLine);
|
|
|
if (!body.includes('=')) continue;
|
|
if (!body.includes('=')) continue;
|
|
|
FIELD_ASSIGN_RE.lastIndex = 0;
|
|
FIELD_ASSIGN_RE.lastIndex = 0;
|
|
|
let m: RegExpExecArray | null;
|
|
let m: RegExpExecArray | null;
|
|
|
while ((m = FIELD_ASSIGN_RE.exec(body))) {
|
|
while ((m = FIELD_ASSIGN_RE.exec(body))) {
|
|
|
const [, lrecv, lfield, rrecv, rfield] = m;
|
|
const [, lrecv, lfield, rrecv, rfield] = m;
|
|
|
|
|
+ // Pre-gate on field NAMES: `a->f = b->g` matches every struct-field
|
|
|
|
|
+ // assignment in the tree (millions on the kernel), but only fields
|
|
|
|
|
+ // that are fn-pointer fields of SOME struct can pass fnPtrFieldOf —
|
|
|
|
|
+ // skip the two regex type resolutions for the ~99% that can't.
|
|
|
|
|
+ if (!fieldToStructs.has(lfield!) || !fieldToStructs.has(rfield!)) continue;
|
|
|
const lt = recvTypeIn(body, lrecv!);
|
|
const lt = recvTypeIn(body, lrecv!);
|
|
|
const rt = recvTypeIn(body, rrecv!);
|
|
const rt = recvTypeIn(body, rrecv!);
|
|
|
if (lt && rt && fnPtrFieldOf(lt, lfield!) && fnPtrFieldOf(rt, rfield!)) {
|
|
if (lt && rt && fnPtrFieldOf(lt, lfield!) && fnPtrFieldOf(rt, rfield!)) {
|
|
@@ -899,6 +957,7 @@ export async function cFnPointerDispatchEdges(
|
|
|
}
|
|
}
|
|
|
if (!changed) break;
|
|
if (!changed) break;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (prof) { prof.D = Date.now() - tPass; tPass = Date.now(); }
|
|
|
if (reg.size === 0 && arrayReg.size === 0) return [];
|
|
if (reg.size === 0 && arrayReg.size === 0) return [];
|
|
|
|
|
|
|
|
// ---- Pass E: dispatch sites → edges ----
|
|
// ---- Pass E: dispatch sites → edges ----
|
|
@@ -918,12 +977,26 @@ export async function cFnPointerDispatchEdges(
|
|
|
await tick();
|
|
await tick();
|
|
|
const s = src(file);
|
|
const s = src(file);
|
|
|
if (!s) continue;
|
|
if (!s) continue;
|
|
|
- for (const fn of ctx.getNodesInFile(file)) {
|
|
|
|
|
|
|
+ const tN = prof ? Date.now() : 0;
|
|
|
|
|
+ const fnsE = ctx.getNodesInFile(file);
|
|
|
|
|
+ if (prof) { prof.nodesMs += Date.now() - tN; prof.nodesN++; }
|
|
|
|
|
+ const eLines = s.split('\n');
|
|
|
|
|
+ for (const fn of fnsE) {
|
|
|
if (!FN_KINDS.has(fn.kind)) continue;
|
|
if (!FN_KINDS.has(fn.kind)) continue;
|
|
|
- const body = sliceLines(s, fn.startLine, fn.endLine);
|
|
|
|
|
|
|
+ const body = sliceLinesPre(eLines, fn.startLine, fn.endLine);
|
|
|
DISPATCH_RE.lastIndex = 0;
|
|
DISPATCH_RE.lastIndex = 0;
|
|
|
let m: RegExpExecArray | null;
|
|
let m: RegExpExecArray | null;
|
|
|
let added = 0;
|
|
let added = 0;
|
|
|
|
|
+ // Incremental line counting: matches arrive in ascending index order, so
|
|
|
|
|
+ // count newlines since the previous match instead of re-splitting the
|
|
|
|
|
+ // whole body prefix per match (O(body) each — real time on god-files).
|
|
|
|
|
+ let lcIdx = 0;
|
|
|
|
|
+ let lcLine = fn.startLine;
|
|
|
|
|
+ const lineAt = (idx: number): number => {
|
|
|
|
|
+ for (let i = lcIdx; i < idx; i++) if (body.charCodeAt(i) === 10) lcLine++;
|
|
|
|
|
+ lcIdx = idx;
|
|
|
|
|
+ return lcLine;
|
|
|
|
|
+ };
|
|
|
while ((m = DISPATCH_RE.exec(body)) && added < FANOUT_CAP) {
|
|
while ((m = DISPATCH_RE.exec(body)) && added < FANOUT_CAP) {
|
|
|
const baseChain = m[1]!.replace(/\s*(?:->|\.)\s*$/, '').trim(); // receiver, minus the trailing arrow
|
|
const baseChain = m[1]!.replace(/\s*(?:->|\.)\s*$/, '').trim(); // receiver, minus the trailing arrow
|
|
|
const field = m[2]!;
|
|
const field = m[2]!;
|
|
@@ -942,7 +1015,7 @@ export async function cFnPointerDispatchEdges(
|
|
|
if (!struct) continue;
|
|
if (!struct) continue;
|
|
|
const targets = reg.get(`${struct}.${field}`);
|
|
const targets = reg.get(`${struct}.${field}`);
|
|
|
if (!targets) continue;
|
|
if (!targets) continue;
|
|
|
- const line = fn.startLine + body.slice(0, m.index).split('\n').length - 1;
|
|
|
|
|
|
|
+ const line = lineAt(m.index);
|
|
|
for (const tid of targets) {
|
|
for (const tid of targets) {
|
|
|
if (tid === fn.id) continue;
|
|
if (tid === fn.id) continue;
|
|
|
const key = `${fn.id}>${tid}`;
|
|
const key = `${fn.id}>${tid}`;
|
|
@@ -966,6 +1039,9 @@ export async function cFnPointerDispatchEdges(
|
|
|
|
|
|
|
|
// ---- bare array-of-fn-pointers dispatch (`tbl[i](…)`) ----
|
|
// ---- bare array-of-fn-pointers dispatch (`tbl[i](…)`) ----
|
|
|
if (arrayReg.size && added < FANOUT_CAP) {
|
|
if (arrayReg.size && added < FANOUT_CAP) {
|
|
|
|
|
+ // Fresh scan from the body's start — rewind the line-count cursor too.
|
|
|
|
|
+ lcIdx = 0;
|
|
|
|
|
+ lcLine = fn.startLine;
|
|
|
ARRAY_DISPATCH_RE.lastIndex = 0;
|
|
ARRAY_DISPATCH_RE.lastIndex = 0;
|
|
|
while ((m = ARRAY_DISPATCH_RE.exec(body)) && added < FANOUT_CAP) {
|
|
while ((m = ARRAY_DISPATCH_RE.exec(body)) && added < FANOUT_CAP) {
|
|
|
const entries = arrayReg.get(m[1]!);
|
|
const entries = arrayReg.get(m[1]!);
|
|
@@ -976,7 +1052,7 @@ export async function cFnPointerDispatchEdges(
|
|
|
? entries[0]!.ids
|
|
? entries[0]!.ids
|
|
|
: (entries.find((e) => e.file === fn.filePath)?.ids ?? null);
|
|
: (entries.find((e) => e.file === fn.filePath)?.ids ?? null);
|
|
|
if (!ids) continue;
|
|
if (!ids) continue;
|
|
|
- const line = fn.startLine + body.slice(0, m.index).split('\n').length - 1;
|
|
|
|
|
|
|
+ const line = lineAt(m.index);
|
|
|
for (const tid of ids) {
|
|
for (const tid of ids) {
|
|
|
if (tid === fn.id) continue;
|
|
if (tid === fn.id) continue;
|
|
|
const key = `${fn.id}>${tid}`;
|
|
const key = `${fn.id}>${tid}`;
|
|
@@ -1000,5 +1076,11 @@ export async function cFnPointerDispatchEdges(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ if (prof) {
|
|
|
|
|
+ prof.E = Date.now() - tPass;
|
|
|
|
|
+ console.error(
|
|
|
|
|
+ `[synth-timing] cFnPtr sub: A=${prof.A}ms B=${prof.B}ms C=${prof.C}ms D=${prof.D}ms E=${prof.E}ms | read n=${prof.readN} ${prof.readMs}ms strip n=${prof.stripN} ${prof.stripMs}ms nodesInFile n=${prof.nodesN} ${prof.nodesMs}ms`
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
return edges;
|
|
return edges;
|
|
|
}
|
|
}
|