mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(track_memory_allocs): add allocation tracking for linter rules (#28470)
As an alternative to https://github.com/oxc-project/oxc/pull/27477, this requires minimal changes in the linter in order to add allocation tracking for individual rules. We run each rule individually and then compare the diff in heap size before and after as well as the number of system and arena allocations. Additionally, this runs as a local-only opt-in feature so that we don't check this in CI automatically, since linter allocations are likely quite noisy.
This commit is contained in:
1 parent
e3f90b1f80
commit
004647a4fa
8 files changed
+145
-1
No files matched your search
@@ -1,6 +1,7 @@
|
||||
# Rust
|
||||
target/
|
||||
**/*.rs.bk
|
||||
/tasks/track_memory_allocations/allocs_linter.yaml
|
||||
|
||||
# Wasm
|
||||
**/*.wasm
|
||||
|
||||
Generated
+2
@@ -2763,11 +2763,13 @@ dependencies = [
|
||||
"mimalloc-safe",
|
||||
"oxc_allocator",
|
||||
"oxc_formatter",
|
||||
"oxc_linter",
|
||||
"oxc_minifier",
|
||||
"oxc_parser",
|
||||
"oxc_semantic",
|
||||
"oxc_tasks_common",
|
||||
"oxc_transformer",
|
||||
"rustc-hash",
|
||||
"saphyr",
|
||||
]
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ description.workspace = true
|
||||
default = []
|
||||
ruledocs = ["oxc_macros/ruledocs"] # Enables the `ruledocs` feature for conditional compilation
|
||||
force_test_reporter = []
|
||||
track_allocations = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -81,4 +82,3 @@ url = { workspace = true }
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
markdown = { workspace = true }
|
||||
|
||||
@@ -516,6 +516,34 @@ impl<'a> ContextHost<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Run one native rule's callbacks for allocation profiling, without linter setup or teardown.
|
||||
///
|
||||
/// Uses the same eligibility and AST type filters as the regular rule runner.
|
||||
#[cfg(feature = "track_allocations")]
|
||||
pub fn run_rule_for_allocations(self: Rc<Self>, rule: &RuleEnum, severity: AllowWarnDeny) {
|
||||
let run_info = rule.run_info();
|
||||
let ast_types = rule.types_info();
|
||||
if rule.is_tsgolint_rule()
|
||||
|| (run_info == crate::RuleRunFunctionsImplemented::Run
|
||||
&& ast_types.is_some_and(|types| !self.semantic().nodes().contains_any(types)))
|
||||
|| !rule.should_run(&self)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let ctx = self.spawn(rule, severity);
|
||||
if run_info.is_run_once_implemented() {
|
||||
rule.run_once::<false>(&ctx, None);
|
||||
}
|
||||
if run_info.is_run_implemented() {
|
||||
for node in ctx.nodes() {
|
||||
if ast_types.is_none_or(|types| types.has(node.kind().ty())) {
|
||||
rule.run::<false>(node, &ctx, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`LintContext`] for testing purposes only.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn spawn_for_test(self: Rc<Self>) -> LintContext<'a> {
|
||||
|
||||
@@ -19,6 +19,7 @@ doctest = false
|
||||
[dependencies]
|
||||
oxc_allocator = { workspace = true, features = ["track_allocations"] }
|
||||
oxc_formatter = { workspace = true }
|
||||
oxc_linter = { workspace = true, optional = true, features = ["track_allocations"] }
|
||||
oxc_minifier = { workspace = true }
|
||||
oxc_parser = { workspace = true }
|
||||
oxc_semantic = { workspace = true }
|
||||
@@ -27,8 +28,10 @@ oxc_transformer = { workspace = true }
|
||||
|
||||
humansize = { workspace = true }
|
||||
mimalloc-safe = { workspace = true }
|
||||
rustc-hash = { workspace = true, optional = true }
|
||||
saphyr = { workspace = true }
|
||||
|
||||
[features]
|
||||
linter = ["dep:oxc_linter", "dep:rustc-hash"]
|
||||
# Sentinel flag that should only get enabled if we're running under `--all-features`
|
||||
is_all_features = []
|
||||
@@ -4,6 +4,35 @@ This task keeps track of the number of system allocations as well as arena alloc
|
||||
|
||||
Update the snapshots with `cargo allocs`.
|
||||
|
||||
## Optional per-rule linter tracking
|
||||
|
||||
Run `cargo run -p oxc_track_memory_allocations --profile coverage --features linter`
|
||||
to write only `tasks/track_memory_allocations/allocs_linter.yaml`, grouped by file
|
||||
and plugin/rule name. This is opt-in: normal `cargo allocs` runs and CI do not run
|
||||
it, including CI builds using `--all-features`.
|
||||
|
||||
Linter-enabled builds do not measure or rewrite the normal stage snapshots:
|
||||
the linter enables additional semantic-analysis features that change their
|
||||
allocation counts. Use `cargo allocs` without the feature for those snapshots.
|
||||
|
||||
The generated report is gitignored. Within each file, rules are sorted by
|
||||
`sys alloc bytes` descending, then `sys allocs` descending, then rule name.
|
||||
Values reflect the current run without snapshot tolerances. File size is omitted
|
||||
from the per-rule metrics.
|
||||
|
||||
Only native rule callbacks (`should_run`, `run_once`, and `run`) are measured,
|
||||
with the regular eligibility and AST type filters. Parsing, semantic analysis,
|
||||
configuration, context construction, and context teardown are excluded. Type-aware
|
||||
rules implemented by tsgolint are omitted. Skipped native rules have zero activity
|
||||
unless their `should_run` callback allocates.
|
||||
|
||||
Rules run individually in rule-ID order, rather than interleaving their node
|
||||
callbacks as the production runner does. They share a fresh per-file context.
|
||||
Diagnostic storage growth is charged to the rule that triggers it, and lazy shared
|
||||
work (such as React Compiler analysis) is charged to its first consumer. Diagnostics
|
||||
remain alive until all rules finish. For these measurements, `arena size` is the
|
||||
arena growth during that rule rather than the cumulative arena size.
|
||||
|
||||
## Metrics
|
||||
|
||||
Recorded per file and per stage (parser, semantic, transformer, minifier, formatter):
|
||||
|
||||
@@ -15,6 +15,9 @@ use oxc_transformer::{TransformOptions, Transformer};
|
||||
use std::alloc::{GlobalAlloc, Layout};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
|
||||
|
||||
#[cfg(feature = "linter")]
|
||||
mod linter;
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: TrackedAllocator = TrackedAllocator;
|
||||
|
||||
@@ -300,6 +303,11 @@ fn test() {
|
||||
pub fn run() -> Result<(), io::Error> {
|
||||
let files = TestFiles::complicated();
|
||||
|
||||
#[cfg(feature = "linter")]
|
||||
if !cfg!(feature = "is_all_features") {
|
||||
return linter::run(&files);
|
||||
}
|
||||
|
||||
let mut parser_entries = Vec::new();
|
||||
let mut semantic_entries = Vec::new();
|
||||
let mut transformer_entries = Vec::new();
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::{fmt::Write as _, fs, io, path::Path, rc::Rc, sync::Arc};
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_linter::{
|
||||
ConfigStore, ConfigStoreBuilder, ContextSubHost, ContextSubHostOptions, ExternalPluginStore,
|
||||
LintOptions, ModuleRecord, rules::ContextHost,
|
||||
};
|
||||
use oxc_parser::Parser;
|
||||
use oxc_semantic::SemanticBuilder;
|
||||
use oxc_tasks_common::{TestFiles, project_root};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use super::{record_stats_in, render_file_stats};
|
||||
|
||||
pub fn run(files: &TestFiles) -> io::Result<()> {
|
||||
let mut external_plugin_store = ExternalPluginStore::default();
|
||||
let config = ConfigStoreBuilder::all().build(&mut external_plugin_store).unwrap();
|
||||
let config_store = ConfigStore::new(config, FxHashMap::default(), external_plugin_store);
|
||||
let snapshot_path = project_root().join("tasks/track_memory_allocations/allocs_linter.yaml");
|
||||
let mut out = String::new();
|
||||
|
||||
for file in files.files() {
|
||||
let allocator = Allocator::default();
|
||||
let parsed = Parser::new(&allocator, &file.source_text, file.source_type).parse();
|
||||
assert!(parsed.diagnostics.is_empty());
|
||||
let path = Path::new(&file.file_name);
|
||||
let semantic = SemanticBuilder::new_linter().build(&parsed.program).semantic;
|
||||
let module_record = Arc::new(ModuleRecord::new(path, &parsed.module_record, &semantic));
|
||||
let state = config_store.resolve(path);
|
||||
let host = Rc::new(ContextHost::new(
|
||||
path,
|
||||
vec![ContextSubHost::new(semantic, module_record, 0, ContextSubHostOptions::default())],
|
||||
&allocator,
|
||||
LintOptions::default(),
|
||||
state.config,
|
||||
));
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for (rule, severity) in state.rules.iter().filter(|(rule, _)| !rule.is_tsgolint_rule()) {
|
||||
let rule_name = format!("{}/{}", rule.plugin_name(), rule.name());
|
||||
let arena_before = allocator.used_bytes();
|
||||
let ((), mut stats) = record_stats_in(&allocator, || {
|
||||
Rc::clone(&host).run_rule_for_allocations(rule, *severity);
|
||||
});
|
||||
stats.arena_used_bytes = allocator.used_bytes().saturating_sub(arena_before);
|
||||
entries.push((rule_name, stats));
|
||||
}
|
||||
|
||||
entries.sort_unstable_by(|(left_name, left), (right_name, right)| {
|
||||
right
|
||||
.counters
|
||||
.heap
|
||||
.alloc_bytes
|
||||
.cmp(&left.counters.heap.alloc_bytes)
|
||||
.then_with(|| right.counters.heap.allocs.cmp(&left.counters.heap.allocs))
|
||||
.then_with(|| left_name.cmp(right_name))
|
||||
});
|
||||
|
||||
writeln!(out, "{}:", file.file_name).unwrap();
|
||||
for (rule_name, stats) in entries {
|
||||
let mut metrics = stats.metrics(file.source_text.len());
|
||||
metrics.retain(|metric| metric.label != "file size");
|
||||
let mut rule_out = String::new();
|
||||
render_file_stats(&mut rule_out, &rule_name, &metrics, None);
|
||||
for line in rule_out.lines() {
|
||||
writeln!(out, " {line}").unwrap();
|
||||
}
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
fs::write(snapshot_path, out)
|
||||
}
|
||||
Reference in new issue
Block a user