mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
76cd6bea69
* perf: reduce cold scan startup and add V8 profiling Add repeatable CPU and heap profiling so cold-run bottlenecks can be measured, then remove profile-proven startup, traversal, and security-scan overhead. * perf: add deterministic stress benchmark Exercise cold scans with reproducible diagnostics and remove redundant visitor-map allocations so regressions are measurable and behavior stays hash-verifiable. * fix(tests): normalize benchmark paths cross-platform Resolve the absolute fixture path through Node so the harness expectation matches Windows path semantics. * perf: avoid effect analysis parent traversal Reuse Oxc visitor keys with a parent-safe fallback so effect scope analysis no longer strips and restores every AST parent reference. * perf: reduce repeated semantic traversal Reuse host visitors and shared analysis caches to avoid redundant cold-scan AST passes while preserving diagnostic parity. * fix: harden performance regression coverage * fix(perf): support profiling on Node 20 * fix(plugin): skip CFGs for bodyless functions * fix: harden profiling and semantic compatibility Prevent benchmark artifacts and host differences from corrupting comparisons, while preserving host AST and React Compiler compatibility across optimized scan paths. * fix(plugin): preserve rule correctness in fast scan paths Keep security prefilters comment-tolerant and resolve React HOC wrappers by binding provenance so scan optimizations cannot hide valid diagnostics. * fix: address review findings and consolidate duplicated code - wrapWithSemanticContext copies the rule's visitors instead of mutating a possibly shared object; walkAst regains its null-root guard - no-multi-comp HOC identity accepts React-compat runtimes via REACT_RUNTIME_MODULE_SOURCES (now incl. @wordpress/element) across ESM, require(), and TS import-equals, with regression coverage in both directions - shared traversal core (forEachChildNode) replaces the walkChildren and containsJsx copies; isImportedFromReact deduped into is-react-api-call - performance harness consolidated (27 -> 22 files): shared commander options, shared profile-frame accumulation, record shape guards, dead BenchmarkSample.profileDirectory removed - CLI-spawning harness tests skip without a built dist; dead and tautological test assertions removed Net -282 LOC against the branch head. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugin): capture the Program root for every rule Rules can reach context.scopes through shared helpers and factories (createDeprecatedReactImportRule resolves namespace aliases via resolveConstIdentifierAlias), so the codegen'd requiresSemanticContext flag — a regex over the rule's own file — silently left factory-based rules on stub scope analyses: no-react-dom-deprecated-apis and no-react19-deprecated-apis stopped reporting namespace usages through the host wrapper. Delete the flag and its regex detector entirely and install the root-capture Program visitor on every rule. The analyses stay lazy and memoized per Program, so rules that never read them still pay only one call per rule per file, and no future helper-routed consumer can be missed. Regression test runs the real host-wrapped rule and fails against the gated wrapper. Fixes cursor bugbot review finding on PR #1114. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scripts): reject cyclic heap-profile node graphs during collection collectNodes flattened the parsed tree with no visited guard, so a cyclic or shared-node object graph would loop before reaching the duplicate-ID check. Unreachable through analyzeHeapProfiles (JSON.parse output is always a strict tree), but guard and throw like the CPU analyzer so a synthetic graph fails deterministically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
41 lines
2.2 KiB
TypeScript
41 lines
2.2 KiB
TypeScript
import { BYTES_PER_MEBIBYTE, PERCENT_MULTIPLIER } from "./constants.ts";
|
||
import type { PerformanceResult } from "./types.ts";
|
||
|
||
const formatMilliseconds = (value: number): string => `${value.toFixed(1)} ms`;
|
||
const formatMebibytes = (value: number | null): string =>
|
||
value === null ? "n/a" : `${(value / BYTES_PER_MEBIBYTE).toFixed(1)} MiB`;
|
||
|
||
export const renderPerformanceMarkdown = (result: PerformanceResult): string => {
|
||
const lines = [
|
||
"# React Doctor performance results",
|
||
"",
|
||
`Generated: ${result.generatedAt}`,
|
||
`React Doctor: ${result.reactDoctorGitSha ?? "unknown"}${result.reactDoctorIsDirty ? " (dirty)" : ""}`,
|
||
`Host: ${result.host.cpuModel}, ${result.host.cpuCount} CPUs, Node ${result.host.nodeVersion}, ${result.host.platform}-${result.host.architecture}`,
|
||
"",
|
||
"| Target | Mode | Cache | Workers | Samples | Wall median | MAD | Range | Peak RSS | Files/s | MiB/s |",
|
||
"| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
||
];
|
||
for (const series of result.series) {
|
||
const residentSetMedian = series.maximumResidentSetBytes?.median ?? null;
|
||
lines.push(
|
||
`| ${series.target.label} | ${series.mode} | ${series.cacheCohort} | ${series.workerCount} | ${series.samples.length} | ${formatMilliseconds(series.wallMilliseconds.median)} | ${formatMilliseconds(series.wallMilliseconds.medianAbsoluteDeviation)} | ${formatMilliseconds(series.wallMilliseconds.minimum)}–${formatMilliseconds(series.wallMilliseconds.maximum)} | ${formatMebibytes(residentSetMedian)} | ${series.filesPerSecond.toFixed(1)} | ${series.mebibytesPerSecond.toFixed(1)} |`,
|
||
);
|
||
}
|
||
if (result.comparisons.length > 0) {
|
||
lines.push(
|
||
"",
|
||
"## Comparison",
|
||
"",
|
||
"| Series | Baseline | Current | Delta | Result |",
|
||
"| --- | ---: | ---: | ---: | --- |",
|
||
);
|
||
for (const comparison of result.comparisons) {
|
||
lines.push(
|
||
`| ${comparison.key} | ${formatMilliseconds(comparison.baselineMedianMilliseconds)} | ${formatMilliseconds(comparison.currentMedianMilliseconds)} | ${(comparison.deltaRatio * PERCENT_MULTIPLIER).toFixed(1)}% | ${comparison.classification} |`,
|
||
);
|
||
}
|
||
}
|
||
return `${lines.join("\n")}\n`;
|
||
};
|