diff --git a/packages/code-analyzer-eslint-engine/test/perf-eslint-run.test.ts b/packages/code-analyzer-eslint-engine/test/perf-eslint-run.test.ts new file mode 100644 index 00000000..75f63c88 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/perf-eslint-run.test.ts @@ -0,0 +1,92 @@ +import {Engine, RuleDescription, Workspace} from "@salesforce/code-analyzer-engine-api"; +import {ESLintEnginePlugin} from "../src"; +import {DEFAULT_CONFIG, ESLintEngineConfig} from "../src/config"; +import {createDescribeOptions, createRunOptions} from "./test-helpers"; + +/** + * One-off performance probe for running ESLint rules (runRules). + * + * What this test does: + * - Instantiates the ESLint engine with the current base configuration. + * - Discovers rules (describeRules), then selects a subset to run (e.g., Recommended). + * - Runs the engine against the provided workspace (PERF_WS must be set). + * - Samples Node's resident set size (RSS) and prints a JSON summary: + * { + * "files_scanned": , + * "rules_run": , + * "run_ms": , + * "peak_rss_mb": , + * "violations": + * } + * + * How to run (skipped by default; opt-in with env var): + * ESLINT_ENGINE_PERF_RUN=true PERF_WS="/absolute/path/to/project" PERF_DISCOVER=true \ + * npm run test-typescript -- packages/code-analyzer-eslint-engine/test/perf-eslint-run.test.ts + * + * Optional env vars: + * - PERF_RULES_LIMIT=200 → cap the number of rules to run (keeps runtime stable) + * - PERF_DISCOVER=true → let ESLint auto-discover configs from PERF_WS (sets config_root to PERF_WS) + * - You can toggle: + * // disable_react_base_config: true, + * // disable_typescript_base_config: true, + * in the config below to attribute parser/plugin costs. + */ +const RUN = process.env.ESLINT_ENGINE_PERF_RUN === 'true'; +(RUN ? describe : describe.skip)('ESLint engine perf (runRules one-off)', () => { + it('measures runRules wall time and peak RSS', async () => { + const cfg: ESLintEngineConfig = { + ...DEFAULT_CONFIG, + // Toggle to isolate costs if desired: + disable_react_base_config: false, + disable_typescript_base_config: true, + config_root: __dirname, + auto_discover_eslint_config: process.env.PERF_DISCOVER === 'true', + }; + + const wsPath = process.env.PERF_WS; + if (!wsPath) { + throw new Error('PERF_WS env var must be set to an absolute path for runRules perf test.'); + } + const ws: Workspace = new Workspace('perf', [wsPath]); + if (process.env.PERF_DISCOVER === 'true') { + cfg.config_root = wsPath; + } + + const engine: Engine = await new ESLintEnginePlugin().createEngine('eslint', cfg); + + // Discover and select rules to run (Recommended by default), with an optional cap. + const discovered: RuleDescription[] = await engine.describeRules(createDescribeOptions(ws)); + const rulesToRun: string[] = discovered + .filter(r => r.tags.includes('Recommended')) + .slice(0, Number(process.env.PERF_RULES_LIMIT) || 200) + .map(r => r.name); + + const peak = {rss: 0}; + const sampler = setInterval(() => { + const m = process.memoryUsage(); + if (m.rss > peak.rss) peak.rss = m.rss; + }, 50); + + const mem0 = process.memoryUsage().rss; + const t0 = performance.now(); + const results = await engine.runRules(rulesToRun, createRunOptions(ws)); + const t1 = performance.now(); + const mem1 = process.memoryUsage().rss; + clearInterval(sampler); + + // Calculate files that produced violations (as a proxy for scanned footprint). + const filesScanned = Array.from(new Set(results.violations.map(v => v.codeLocations[0].file))).length; + + // eslint-disable-next-line no-console + console.log(JSON.stringify({ + files_scanned: filesScanned, + rules_run: rulesToRun.length, + run_ms: Math.round(t1 - t0), + peak_rss_mb: Math.round(Math.max(peak.rss, mem0, mem1) / (1024 * 1024)), + violations: results.violations.length + }, null, 2)); + + expect(rulesToRun.length).toBeGreaterThan(0); + }); +}); + diff --git a/packages/code-analyzer-eslint-engine/test/perf-eslint.test.ts b/packages/code-analyzer-eslint-engine/test/perf-eslint.test.ts new file mode 100644 index 00000000..a46c6578 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/perf-eslint.test.ts @@ -0,0 +1,90 @@ +import {Engine, RuleDescription, Workspace} from "@salesforce/code-analyzer-engine-api"; +import {ESLintEnginePlugin} from "../src"; +import {DEFAULT_CONFIG, ESLintEngineConfig} from "../src/config"; +import {createDescribeOptions} from "./test-helpers"; +import * as path from "node:path"; + +/** + * One-off performance probe for the ESLint engine. + * + * What this test does: + * - Instantiates the ESLint engine with the current base configuration. + * - Calls engine.describeRules(), which exercises rule discovery (parsing configs, resolving plugins, building rule lists). + * - Samples Node's resident set size (RSS) periodically to estimate the peak memory use during describeRules(). + * - Emits a single JSON object to stdout with: + * { + * "rule_count": , + * "describe_ms": , + * "peak_rss_mb": + * } + * + * How to run (skipped by default; opt-in with env var): + * ESLINT_ENGINE_PERF=true npm run test-typescript -- packages/code-analyzer-eslint-engine/test/perf-eslint.test.ts + * + * ESLINT_ENGINE_PERF=true PERF_WS="/Users/arun.tyagi/projects/dreamhouse-sfdx" PERF_DISCOVER=true \ + * npm run test-typescript -- packages/code-analyzer-eslint-engine/test/perf-eslint.test.ts + * + * What to look for: + * - describe_ms: Use this to compare wall-time across changes. Lower is better. + * - peak_rss_mb: Track memory impact/regressions (e.g., when enabling additional parsers/plugins). + * - rule_count: Sanity check that you are comparing like-for-like runs (similar rule surface). + * + * How to isolate parser/plugin costs: + * - Set disable_react_base_config: true → measure baseline without React rules/plugins. + * - Set disable_typescript_base_config: true → measure without the TypeScript parser/rules. + * Run the test multiple times, toggling these flags, and compare the outputs. + * + * Notes: + * - This is a coarse probe (RSS sampling every 50ms). For deeper analysis, also try: + * node --cpu-prof --heap-prof ./node_modules/.bin/jest packages/code-analyzer-eslint-engine/test/perf-eslint.test.ts + * and inspect the generated profiles in Chrome DevTools. + */ +const RUN = process.env.ESLINT_ENGINE_PERF === 'true'; +(RUN ? describe : describe.skip)('ESLint engine perf (one-off)', () => { + it('measures describeRules wall time and peak RSS', async () => { + const config: ESLintEngineConfig = { + ...DEFAULT_CONFIG, + // Toggle these to isolate costs: + // disable_react_base_config: true, + // disable_typescript_base_config: true, + config_root: __dirname + }; + + // If a workspace path is provided (PERF_WS), analyze that project. + // Optionally allow ESLint to auto-discover configs from that project (PERF_DISCOVER=true). + const wsPath = process.env.PERF_WS; + const ws: Workspace | undefined = wsPath ? new Workspace('perf', [wsPath]) : undefined; + if (wsPath && process.env.PERF_DISCOVER === 'true') { + (config as ESLintEngineConfig).auto_discover_eslint_config = true; + (config as ESLintEngineConfig).config_root = wsPath; + } + + // Create engine with the chosen config flags + const engine: Engine = await new ESLintEnginePlugin().createEngine('eslint', config); + + // Track peak RSS during the measured operation with a light-weight sampler. + const peak = { rss: 0 }; + const sampler = setInterval(() => { + const m = process.memoryUsage(); + if (m.rss > peak.rss) peak.rss = m.rss; + }, 50); + + // Measure wall time of describeRules (rule discovery). + const mem0 = process.memoryUsage().rss; + const t0 = performance.now(); + const rules: RuleDescription[] = await engine.describeRules(createDescribeOptions(ws)); + const t1 = performance.now(); + const mem1 = process.memoryUsage().rss; + clearInterval(sampler); + + // eslint-disable-next-line no-console + console.log(JSON.stringify({ + rule_count: rules.length, + describe_ms: Math.round(t1 - t0), + peak_rss_mb: Math.round(Math.max(peak.rss, mem0, mem1) / (1024 * 1024)) + }, null, 2)); + + expect(rules.length).toBeGreaterThan(0); + }); +}); +