GitNexus/gitnexus/bench/objective-c-resolution/measure.mjs
mengkaka 4154b63131
feat(indexing): add Objective-C semantic indexing support (#3179)
* docs: add Objective-C fork provider notes

* feat(objective-c): add deterministic provider and grammar

* feat(objective-c): finalize provider MVP

* fix(objective-c): harden provider integration

* fix(objective-c): normalize bare macro markers

* docs(objective-c): integrate provider documentation

* fix(objective-c): harden resolution and header classification

* fix(objective-c): complete provider follow-ups

* fix: address Objective-C review follow-ups

* chore: format Objective-C grammar sources

* fix(objective-c): harden review follow-ups

* Address PR review feedback (#3179)

Keep Objective-C chunking and macro recovery aligned with the grammar, and stop Community MEMBER_OF edges from leaking into symbol context.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address follow-up review on ObjC chunking and language fallback.

Keep preprocessor directive text from changing file-scope brace depth, group real ivar nodes, skip header modifiers, and restore Rakefile/Gemfile detection through getLanguageFromFilename.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Parse Objective-C headers with the objc grammar in embeddings.

ensureAndParse and structural extraction now use the same content classifier as ingest, including method snippets from .h files, so Protocol/Category/Class chunks are not re-parsed as C++.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Keep file-scope macro elision off C line splices and @interface/@protocol/@implementation bodies, and attach ivar attributes to the following instance variable when chunking.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(bench): rebaseline Objective-C CSV emit

* feat(objective-c): add workspace resolution and linear emit benches

Plain .h files are classified as C++, so the ObjC pass could not
resolve #import of those headers. Load a C/C#-style workspace once
per pass, and keep protocol-candidate USES linear.

Refs #3179

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

- Compare LadybugDB labels() as a scalar when excluding Community MEMBER_OF edges.
- Walk superclass members, skip file-static C sibling defs, and ignore comments in ObjC header/macro scans.

Note: pre-existing failure in objective-c-provider integration (worker-pool ready timeout) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit Objective-C declaration captures so compilation-unit siblings can share
header/implementation bindings, and keep class vs protocol visibility groups
distinct.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit every comma-separated property/ivar declarator, and count @interface
after a multiline block comment closes so in-declaration macros stay intact.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ximengkai <ximengkai@soyoung.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 09:40:21 +00:00

386 lines
12 KiB
JavaScript

#!/usr/bin/env node
/**
* Build-free scaling and correctness guard for Objective-C workspace
* resolution (#3179).
*
* Import lookup already lives in bench/import-target (the `objc` arm). This
* bench isolates `emitPostResolutionEdges`: heritage, category membership,
* and static message-send dispatch. Two shapes:
*
* spread — typed self / super / sibling receivers. Cost must stay
* linear in class count (the C# "namespaces spread" analog).
* protocol — every class conforms to one protocol and sends one
* protocol-typed message. Implementer USES live on one
* shared node per (protocol, selector); messages only add
* the source→shared hop. Both arms must stay linear.
*
* Usage:
* node --import tsx bench/objective-c-resolution/measure.mjs
* node --import tsx bench/objective-c-resolution/measure.mjs --check
*/
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { performance } from 'node:perf_hooks';
import { createKnowledgeGraph } from '../../src/core/graph/graph.ts';
import {
OBJECTIVE_C_GRAMMAR_PACKAGE,
OBJECTIVE_C_GRAMMAR_VERSION,
OBJECTIVE_C_PROVIDER_VERSION,
objcClassQualifiedName,
objcMethodQualifiedName,
objcProtocolQualifiedName,
} from '../../src/core/ingestion/languages/objective-c/facts.ts';
import { objectiveCScopeResolver } from '../../src/core/ingestion/languages/objective-c/scope-resolver.ts';
const HERE = dirname(fileURLToPath(import.meta.url));
const SMALL_CLASSES = 40;
const LARGE_CLASSES = 160;
const REPS = 7;
const graphNodeId = (label, qualifiedName) => `${label}:${qualifiedName}`;
function emptyFacts(filePath) {
return {
providerVersion: OBJECTIVE_C_PROVIDER_VERSION,
grammarPackage: OBJECTIVE_C_GRAMMAR_PACKAGE,
grammarVersion: OBJECTIVE_C_GRAMMAR_VERSION,
filePath,
containers: [],
methods: [],
members: [],
functions: [],
imports: [],
messages: [],
unresolvedMessages: [],
};
}
function classContainer(filePath, name, extras = {}) {
const qualifiedName = objcClassQualifiedName(name);
return {
kind: 'class',
declarationRole: 'implementation',
name,
qualifiedName,
nodeId: graphNodeId('Class', qualifiedName),
label: 'Class',
filePath,
startLine: 1,
endLine: 20,
protocols: extras.protocols ?? [],
...(extras.superclass !== undefined ? { superclass: extras.superclass } : {}),
};
}
function protocolContainer(filePath, name) {
const qualifiedName = objcProtocolQualifiedName(name);
return {
kind: 'protocol',
declarationRole: 'interface',
name,
qualifiedName,
nodeId: graphNodeId('Protocol', qualifiedName),
label: 'Protocol',
filePath,
startLine: 1,
endLine: 4,
protocols: [],
};
}
function methodFact(filePath, owner, selector, methodKind, startLine) {
const qualifiedName = objcMethodQualifiedName(owner.qualifiedName, methodKind, selector);
return {
name: selector,
selector,
methodKind,
ownerQualifiedName: owner.qualifiedName,
ownerName: owner.name,
ownerKind: owner.kind,
qualifiedName,
nodeId: graphNodeId('Method', qualifiedName),
filePath,
startLine,
endLine: startLine,
declarationRole: 'implementation',
parameterTypes: [],
parameterNames: [],
};
}
function messageFact(method, selector, receiverKind, receiverText, extras = {}) {
return {
selector,
receiverText,
receiverKind,
sourceMethodQualifiedName: method.qualifiedName,
sourceMethodId: method.nodeId,
sourceOwnerQualifiedName: method.ownerQualifiedName,
sourceOwnerName: method.ownerName,
sourceMethodKind: method.methodKind,
filePath: method.filePath,
startLine: extras.startLine ?? method.startLine + 1,
startCol: extras.startCol ?? 2,
...(extras.receiverType !== undefined ? { receiverType: extras.receiverType } : {}),
};
}
function parsedFile(filePath, facts) {
return Object.freeze({
filePath,
moduleScope: 0,
scopes: Object.freeze([]),
parsedImports: Object.freeze([]),
localDefs: Object.freeze([]),
referenceSites: Object.freeze([]),
captureSideChannel: Object.freeze({ kind: 'objective-c', facts }),
});
}
function seedGraph(graph, factsList) {
for (const facts of factsList) {
graph.addNode({
id: graphNodeId('File', facts.filePath),
label: 'File',
properties: {
name: facts.filePath,
qualifiedName: facts.filePath,
filePath: facts.filePath,
startLine: 1,
endLine: 1,
language: 'objective-c',
isExported: false,
},
});
for (const container of facts.containers) {
graph.addNode({
id: container.nodeId,
label: container.label,
properties: {
name: container.name,
qualifiedName: container.qualifiedName,
filePath: facts.filePath,
startLine: container.startLine,
endLine: container.endLine,
language: 'objective-c',
isExported: true,
},
});
}
for (const method of facts.methods) {
graph.addNode({
id: method.nodeId,
label: 'Method',
properties: {
name: method.selector,
qualifiedName: method.qualifiedName,
filePath: facts.filePath,
startLine: method.startLine,
endLine: method.endLine,
language: 'objective-c',
isExported: true,
},
});
}
}
}
function summarize(graph) {
let calls = 0;
let extendsEdges = 0;
let implementsEdges = 0;
let protocolCandidateUses = 0;
for (const rel of graph.iterRelationships()) {
if (rel.type === 'CALLS') calls++;
else if (rel.type === 'EXTENDS') extendsEdges++;
else if (rel.type === 'IMPLEMENTS') implementsEdges++;
else if (rel.type === 'USES' && String(rel.reason).startsWith('objc-protocol-candidate:')) {
protocolCandidateUses++;
}
}
return {
calls,
extends: extendsEdges,
implements: implementsEdges,
protocol_candidate_uses: protocolCandidateUses,
};
}
function spreadCorpus(classCount) {
const factsList = [];
const parsedFiles = [];
for (let i = 0; i < classCount; i++) {
const filePath = `src/Class${i}.m`;
const name = `Class${i}`;
const sibling = `Class${(i + 1) % classCount}`;
const owner = classContainer(filePath, name, i === 0 ? {} : { superclass: 'Class0' });
const run = methodFact(filePath, owner, 'run', '-', 4);
const tick = methodFact(filePath, owner, 'tick', '-', 8);
const messages = [
messageFact(tick, 'run', 'self', 'self', { startLine: 9, startCol: 2 }),
messageFact(tick, 'run', 'local', 'sib', {
startLine: 10,
startCol: 2,
receiverType: { kind: 'class', name: sibling, raw: `${sibling} *` },
}),
];
if (i > 0) {
messages.push(messageFact(tick, 'run', 'super', 'super', { startLine: 11, startCol: 2 }));
}
const facts = {
...emptyFacts(filePath),
containers: [owner],
methods: [run, tick],
messages,
};
factsList.push(facts);
parsedFiles.push(parsedFile(filePath, facts));
}
return { factsList, parsedFiles };
}
function protocolCorpus(classCount) {
const factsList = [];
const parsedFiles = [];
const protocolPath = 'src/Runnable.h';
const protocol = protocolContainer(protocolPath, 'Runnable');
const protocolRun = methodFact(protocolPath, protocol, 'run', '-', 2);
const protocolFacts = {
...emptyFacts(protocolPath),
containers: [protocol],
methods: [protocolRun],
};
factsList.push(protocolFacts);
parsedFiles.push(parsedFile(protocolPath, protocolFacts));
for (let i = 0; i < classCount; i++) {
const filePath = `src/Class${i}.m`;
const owner = classContainer(filePath, `Class${i}`, { protocols: ['Runnable'] });
const run = methodFact(filePath, owner, 'run', '-', 4);
const tick = methodFact(filePath, owner, 'tick:', '-', 8);
const facts = {
...emptyFacts(filePath),
containers: [owner],
methods: [run, tick],
messages: [
messageFact(tick, 'run', 'local', 'runner', {
startLine: 9,
startCol: 2,
receiverType: { kind: 'protocol', name: 'Runnable', raw: 'id<Runnable>' },
}),
],
};
factsList.push(facts);
parsedFiles.push(parsedFile(filePath, facts));
}
return { factsList, parsedFiles };
}
function run(shape, classCount) {
const corpus = shape === 'spread' ? spreadCorpus(classCount) : protocolCorpus(classCount);
const graph = createKnowledgeGraph();
seedGraph(graph, corpus.factsList);
objectiveCScopeResolver.emitPostResolutionEdges(graph, corpus.parsedFiles);
return summarize(graph);
}
function measure(shape, classCount) {
run(shape, classCount);
let bestMs = Infinity;
let counts = { calls: 0, extends: 0, implements: 0, protocol_candidate_uses: 0 };
for (let i = 0; i < REPS; i++) {
const start = performance.now();
counts = run(shape, classCount);
bestMs = Math.min(bestMs, performance.now() - start);
}
return {
classes: classCount,
...counts,
min_ms: Number(bestMs.toFixed(2)),
};
}
function scalingReport(small, large) {
const workloadRatio = large.classes / small.classes;
const scalingRatio = Number((large.min_ms / Math.max(small.min_ms, 0.01)).toFixed(3));
return {
small,
large,
workload_ratio: workloadRatio,
scaling_ratio: scalingRatio,
linear_factor: Number((scalingRatio / workloadRatio).toFixed(3)),
};
}
const report = {
spread: scalingReport(measure('spread', SMALL_CLASSES), measure('spread', LARGE_CLASSES)),
protocol: scalingReport(measure('protocol', SMALL_CLASSES), measure('protocol', LARGE_CLASSES)),
};
if (!process.argv.includes('--check')) {
console.log(JSON.stringify(report, null, 2));
process.exit(0);
}
const baseline = JSON.parse(readFileSync(join(HERE, 'baseline.json'), 'utf8'));
const failures = [];
const requirePositiveNumber = (path, value) => {
if (typeof value !== 'number' || !Number.isFinite(value) || !(value > 0)) {
failures.push(`${path}: expected a finite positive number, got ${JSON.stringify(value)}`);
return false;
}
return true;
};
for (const shape of ['spread', 'protocol']) {
const measured = report[shape];
const expected = baseline[shape];
for (const arm of ['small', 'large']) {
for (const key of ['classes', 'calls', 'extends', 'implements', 'protocol_candidate_uses']) {
if (measured[arm][key] !== expected[arm][key]) {
failures.push(
`${shape}.${arm}.${key}: expected ${expected[arm][key]}, got ${measured[arm][key]}`,
);
}
}
if (
requirePositiveNumber(`${shape}.${arm}.ms_budget`, expected[arm].ms_budget) &&
measured[arm].min_ms > expected[arm].ms_budget
) {
failures.push(
`${shape}.${arm}.min_ms ${measured[arm].min_ms} exceeds budget ${expected[arm].ms_budget}`,
);
}
}
}
if (
requirePositiveNumber('spread.linear_scaling_slack', baseline.spread.linear_scaling_slack) &&
report.spread.linear_factor > baseline.spread.linear_scaling_slack
) {
failures.push(
`spread.linear_factor ${report.spread.linear_factor} exceeds slack ` +
`${baseline.spread.linear_scaling_slack} (runtime ${report.spread.scaling_ratio}x ` +
`for ${report.spread.workload_ratio}x work)`,
);
}
if (
requirePositiveNumber('protocol.linear_scaling_slack', baseline.protocol.linear_scaling_slack) &&
report.protocol.linear_factor > baseline.protocol.linear_scaling_slack
) {
failures.push(
`protocol.linear_factor ${report.protocol.linear_factor} exceeds slack ` +
`${baseline.protocol.linear_scaling_slack} (runtime ${report.protocol.scaling_ratio}x ` +
`for ${report.protocol.workload_ratio}x work)`,
);
}
console.log(JSON.stringify(report, null, 2));
if (failures.length > 0) {
console.error('[objective-c-resolution --check] FAIL');
for (const failure of failures) console.error(` - ${failure}`);
process.exit(1);
}
console.log('[objective-c-resolution --check] PASS');