Skip to content

Instantly share code, notes, and snippets.

@connorshea
Last active August 6, 2026 01:49
Show Gist options
  • Select an option

  • Save connorshea/31d520f1a434f486aa7644d30eee79eb to your computer and use it in GitHub Desktop.

Select an option

Save connorshea/31d520f1a434f486aa7644d30eee79eb to your computer and use it in GitHub Desktop.
Global allocator tracking
diff --git a/apps/oxlint/src/lib.rs b/apps/oxlint/src/lib.rs
index ee5969fe6d..cf5ab06d90 100644
--- a/apps/oxlint/src/lib.rs
+++ b/apps/oxlint/src/lib.rs
@@ -45,18 +45,29 @@ mod js_plugins;
// Use Mimalloc as the global allocator if `--features allocator` is enabled.
// Mimalloc has better performance, but this is feature-gated because it's slow to compile.
// `--features allocator` is only used in release builds.
-#[cfg(all(
- feature = "allocator",
- not(any(
- target_arch = "arm",
- target_arch = "riscv64",
- miri,
- target_os = "freebsd",
- target_family = "wasm"
- ))
-))]
+// ---- ALLOC INSTRUMENTATION ----
+use std::alloc::{GlobalAlloc, Layout, System};
+use std::sync::atomic::{AtomicUsize, Ordering as AllocOrdering};
+
+pub struct CountingAlloc;
+
+pub static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
+pub static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
+
+unsafe impl GlobalAlloc for CountingAlloc {
+ unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
+ ALLOC_COUNT.fetch_add(1, AllocOrdering::Relaxed);
+ ALLOC_BYTES.fetch_add(layout.size(), AllocOrdering::Relaxed);
+ unsafe { System.alloc(layout) }
+ }
+ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
+ unsafe { System.dealloc(ptr, layout) }
+ }
+}
+
#[global_allocator]
-static GLOBAL: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc;
+static GLOBAL: CountingAlloc = CountingAlloc;
+// ---- ALLOC INSTRUMENTATION ----
const DEFAULT_OXLINTRC_NAME: &str = ".oxlintrc.json";
const DEFAULT_JSONC_OXLINTRC_NAME: &str = ".oxlintrc.jsonc";
diff --git a/apps/oxlint/src/main.rs b/apps/oxlint/src/main.rs
index 7613d02367..97e8b18878 100644
--- a/apps/oxlint/src/main.rs
+++ b/apps/oxlint/src/main.rs
@@ -41,5 +41,12 @@ fn main() -> CliRunResult {
let mut stdout = BufWriter::new(std::io::stdout());
// Run without external linter (no JS plugins)
- CliRunner::new(command, None).run(&mut stdout)
+ // ---- ALLOC INSTRUMENTATION ----
+ let r = CliRunner::new(command, None).run(&mut stdout);
+ eprintln!(
+ "ALLOC_COUNT={} ALLOC_BYTES={}",
+ oxlint::ALLOC_COUNT.load(std::sync::atomic::Ordering::Relaxed),
+ oxlint::ALLOC_BYTES.load(std::sync::atomic::Ordering::Relaxed),
+ );
+ r
}

Counting global allocator for oxlint

Swaps oxlint's mimalloc global allocator for one that counts allocations and bytes, and prints the totals to stderr on exit. Used to measure allocation impact of linter changes.

Apply counting-allocator.diff (against apps/oxlint/src/lib.rs and apps/oxlint/src/main.rs).

Build

cargo build --release -p oxlint --no-default-features

--no-default-features is required: the allocator feature pulls in mimalloc, and two #[global_allocator]s will not compile. This means the counting binary is not the same build profile as a normal release binary — use it for allocation counts only, never for timing.

Run

Counts go to stderr, diagnostics to stdout:

cd <corpus>
oxlint -c config.json --threads=1 --silent --format=default <target> 2>&1 >/dev/null \
  | grep -o 'ALLOC_COUNT=[0-9]* ALLOC_BYTES=[0-9]*'

--threads=1 for reproducibility. Counts are near-deterministic run to run; the byte total carries a small constant offset that differs between binaries, so only compare differences computed within one binary (see below).

Isolating the work you care about

The totals include parsing, semantic analysis and everything else. To attribute allocations to one rule or subsystem, run twice with the same binary and subtract:

graph_attributable = (rule_enabled_config) - (parse_only_config)

A parse-only config is the same file with every category off and no rules:

{
  "plugins": [],
  "categories": { "correctness": "off", "suspicious": "off", "pedantic": "off",
                  "perf": "off", "style": "off", "restriction": "off", "nursery": "off" },
  "rules": {}
}

Subtracting within a single binary cancels the constant startup offset. Do not subtract a parse-only baseline taken from a different binary.

Caveats

  • Everything is counted, including reallocs and zeroed allocations. GlobalAlloc's default realloc and alloc_zeroed both call self.alloc, and this impl overrides neither, so every allocation funnels through the counter.
  • Absolute totals over-report a production run. The same fact means a realloc that a real allocator would satisfy by growing in place instead becomes alloc + copy + dealloc here. Both sides of a comparison carry the same bias, so differences are sound; the absolute numbers are an upper bound, not what a mimalloc build actually does.
  • ALLOC_BYTES is requested bytes (layout.size()), not what the allocator reserved.
  • Allocations are counted before the underlying alloc is attempted, so a failed allocation still increments. Irrelevant in practice — OOM ends the run anyway.
  • --lsp returns early in main before the report, so language-server runs print nothing.
  • Allocations after the report (shutdown, final drops) are not counted.
  • Peak RSS is a separate question and this does not answer it. /usr/bin/time -l is blocked in some sandboxes; os.wait4() in Python gives ru_maxrss per child with no rebuild.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment