mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* 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>
247 lines
8.8 KiB
TypeScript
247 lines
8.8 KiB
TypeScript
/**
|
|
* Objective-C ingestion pipeline benchmark.
|
|
*
|
|
* Generates synthetic .h/.m pairs at increasing scales and measures
|
|
* wall-clock time and peak heap through the full pipeline — header
|
|
* classification, parsing, provider-owned semantic facts, import
|
|
* suffix resolution, and static message-send edges.
|
|
*
|
|
* Mirrors test/integration/csharp-pipeline-benchmark.test.ts and
|
|
* test/integration/php-pipeline-benchmark.test.ts. Two shapes:
|
|
* 1. "spread" — each class imports a fixed sibling (constant fan-out;
|
|
* see cpp-pipeline-benchmark.test.ts). This is the linear file-count
|
|
* gate.
|
|
* 2. "protocol" — every class conforms to one shared protocol and
|
|
* sends one protocol-typed message. Implementer USES are shared
|
|
* per (protocol, selector); the dedicated
|
|
* bench/objective-c-resolution protocol arm pins that evidence
|
|
* shape at O(implementers).
|
|
*
|
|
* Run: GITNEXUS_BENCH=1 npx vitest run test/integration/objective-c-pipeline-benchmark.test.ts
|
|
*/
|
|
import { beforeAll, describe, it, expect, vi } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
|
|
|
|
const BENCH_ENABLED = process.env.GITNEXUS_BENCH === '1';
|
|
|
|
beforeAll(() => vi.stubEnv('GITNEXUS_WORKER_READY_TIMEOUT_MS', '60000'));
|
|
|
|
interface BenchResult {
|
|
classCount: number;
|
|
fileCount: number;
|
|
elapsedMs: number;
|
|
peakHeapMB: number;
|
|
nodeCount: number;
|
|
edgeCount: number;
|
|
}
|
|
|
|
type FixtureShape = 'spread' | 'protocol';
|
|
|
|
function generateObjectiveCFixture(
|
|
classCount: number,
|
|
shape: FixtureShape,
|
|
): { dir: string; fileCount: number } {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `objc-bench-${shape}-${classCount}-`));
|
|
|
|
if (shape === 'protocol') {
|
|
fs.writeFileSync(
|
|
path.join(dir, 'Runnable.h'),
|
|
[
|
|
'#import <Foundation/Foundation.h>',
|
|
'',
|
|
'@protocol Runnable',
|
|
'- (void)run;',
|
|
'@end',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
}
|
|
|
|
for (let f = 0; f < classCount; f++) {
|
|
const className = `Class${f}`;
|
|
const sibling = `Class${(f + 1) % classCount}`;
|
|
const header =
|
|
shape === 'protocol'
|
|
? [
|
|
'#import <Foundation/Foundation.h>',
|
|
'#import "Runnable.h"',
|
|
'',
|
|
`@interface ${className} : NSObject <Runnable>`,
|
|
'- (void)run;',
|
|
'- (void)tick:(id<Runnable>)runner;',
|
|
'@end',
|
|
'',
|
|
].join('\n')
|
|
: [
|
|
'#import <Foundation/Foundation.h>',
|
|
'',
|
|
`@interface ${className} : NSObject`,
|
|
'- (void)process;',
|
|
'@end',
|
|
'',
|
|
].join('\n');
|
|
|
|
const impl =
|
|
shape === 'protocol'
|
|
? [
|
|
`#import "${className}.h"`,
|
|
'',
|
|
`@implementation ${className}`,
|
|
'- (void)run {}',
|
|
'- (void)tick:(id<Runnable>)runner',
|
|
'{',
|
|
' [self run];',
|
|
' [runner run];',
|
|
'}',
|
|
'@end',
|
|
'',
|
|
].join('\n')
|
|
: [
|
|
`#import "${className}.h"`,
|
|
`#import "${sibling}.h"`,
|
|
'',
|
|
`@implementation ${className}`,
|
|
'- (void)process',
|
|
'{',
|
|
' [self process];',
|
|
` ${sibling} *sib = [${sibling} new];`,
|
|
' [sib process];',
|
|
'}',
|
|
'@end',
|
|
'',
|
|
].join('\n');
|
|
|
|
fs.writeFileSync(path.join(dir, `${className}.h`), header);
|
|
fs.writeFileSync(path.join(dir, `${className}.m`), impl);
|
|
}
|
|
|
|
const fileCount = classCount * 2 + (shape === 'protocol' ? 1 : 0);
|
|
return { dir, fileCount };
|
|
}
|
|
|
|
async function runBenchmark(
|
|
classCount: number,
|
|
shape: FixtureShape,
|
|
budgetMs: number,
|
|
): Promise<BenchResult> {
|
|
const { dir, fileCount } = generateObjectiveCFixture(classCount, shape);
|
|
|
|
let peakHeapMB = 0;
|
|
const heapSampler = setInterval(() => {
|
|
const heap = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
if (heap > peakHeapMB) peakHeapMB = heap;
|
|
}, 50);
|
|
|
|
let budgetTimer: ReturnType<typeof setTimeout> | undefined;
|
|
try {
|
|
const start = Date.now();
|
|
const result = await Promise.race([
|
|
runPipelineFromRepo(dir, () => {}, { skipGraphPhases: true }),
|
|
new Promise<never>((_, reject) => {
|
|
budgetTimer = setTimeout(
|
|
() =>
|
|
reject(
|
|
new Error(`Pipeline exceeded ${budgetMs}ms at ${classCount} classes (${shape})`),
|
|
),
|
|
budgetMs,
|
|
);
|
|
}),
|
|
]);
|
|
const elapsedMs = Date.now() - start;
|
|
|
|
return {
|
|
classCount,
|
|
fileCount,
|
|
elapsedMs,
|
|
peakHeapMB: Math.round(peakHeapMB),
|
|
nodeCount: result.graph.nodeCount,
|
|
edgeCount: result.graph.relationshipCount,
|
|
};
|
|
} finally {
|
|
clearInterval(heapSampler);
|
|
clearTimeout(budgetTimer);
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function printResults(label: string, results: BenchResult[]) {
|
|
console.log(`\n${label}`);
|
|
console.log('┌──────────┬──────────┬───────────┬──────────┬───────┬───────┐');
|
|
console.log('│ Classes │ Files │ Time (ms) │ Heap MB │ Nodes │ Edges │');
|
|
console.log('├──────────┼──────────┼───────────┼──────────┼───────┼───────┤');
|
|
for (const r of results) {
|
|
console.log(
|
|
`│ ${String(r.classCount).padStart(8)} │ ${String(r.fileCount).padStart(8)} │ ${String(r.elapsedMs).padStart(9)} │ ${String(r.peakHeapMB).padStart(8)} │ ${String(r.nodeCount).padStart(5)} │ ${String(r.edgeCount).padStart(5)} │`,
|
|
);
|
|
}
|
|
console.log('└──────────┴──────────┴───────────┴──────────┴───────┴───────┘');
|
|
|
|
if (results.length >= 2) {
|
|
console.log('\nScaling ratios (time_ratio / class_ratio):');
|
|
for (let i = 1; i < results.length; i++) {
|
|
const classRatio = results[i].classCount / results[i - 1].classCount;
|
|
const timeRatio = results[i].elapsedMs / results[i - 1].elapsedMs;
|
|
const scaling = timeRatio / classRatio;
|
|
console.log(
|
|
` ${results[i - 1].classCount} → ${results[i].classCount}: ${scaling.toFixed(2)}x (${scaling < 1.5 ? 'linear' : scaling < 3 ? 'superlinear' : 'WARNING: quadratic'})`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
describe.skipIf(!BENCH_ENABLED)('Objective-C pipeline benchmark', () => {
|
|
it('scales with file count — typed sibling receivers', async () => {
|
|
const scales = [40, 80, 160];
|
|
const results: BenchResult[] = [];
|
|
|
|
for (const classCount of scales) {
|
|
const result = await runBenchmark(classCount, 'spread', 180_000);
|
|
results.push(result);
|
|
console.log(
|
|
` ${classCount} classes: ${result.elapsedMs}ms, ${result.peakHeapMB}MB heap, ${result.nodeCount} nodes, ${result.edgeCount} edges`,
|
|
);
|
|
}
|
|
|
|
printResults('Objective-C Pipeline — Spread', results);
|
|
|
|
expect(results[results.length - 1].edgeCount).toBeGreaterThan(0);
|
|
|
|
for (let i = 1; i < results.length; i++) {
|
|
const classRatio = results[i].classCount / results[i - 1].classCount;
|
|
const timeRatio = results[i].elapsedMs / results[i - 1].elapsedMs;
|
|
expect(timeRatio / classRatio).toBeLessThan(3);
|
|
// Wall-clock at these scales is worker-startup dominated; node/edge
|
|
// cardinality is the load-bearing linearity check (cpp-pipeline analog).
|
|
expect(results[i].nodeCount / results[i - 1].nodeCount).toBeCloseTo(classRatio, 1);
|
|
expect(results[i].edgeCount / results[i - 1].edgeCount).toBeCloseTo(classRatio, 1);
|
|
}
|
|
}, 600_000);
|
|
|
|
it('scales with file count — shared protocol receivers', async () => {
|
|
const scales = [40, 80, 160];
|
|
const results: BenchResult[] = [];
|
|
|
|
for (const classCount of scales) {
|
|
const result = await runBenchmark(classCount, 'protocol', 180_000);
|
|
results.push(result);
|
|
console.log(
|
|
` ${classCount} classes: ${result.elapsedMs}ms, ${result.peakHeapMB}MB heap, ${result.nodeCount} nodes, ${result.edgeCount} edges`,
|
|
);
|
|
}
|
|
|
|
printResults('Objective-C Pipeline — Shared Protocol', results);
|
|
|
|
expect(results[results.length - 1].edgeCount).toBeGreaterThan(0);
|
|
|
|
for (let i = 1; i < results.length; i++) {
|
|
const classRatio = results[i].classCount / results[i - 1].classCount;
|
|
const timeRatio = results[i].elapsedMs / results[i - 1].elapsedMs;
|
|
expect(timeRatio / classRatio).toBeLessThan(3);
|
|
expect(results[i].nodeCount / results[i - 1].nodeCount).toBeCloseTo(classRatio, 1);
|
|
expect(results[i].edgeCount / results[i - 1].edgeCount).toBeCloseTo(classRatio, 1);
|
|
}
|
|
}, 600_000);
|
|
});
|