Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ description = "rustc's frontend with no LLVM and no std: parsing through MIR, as
repository = "https://github.com/pathscale/frontend"
readme = "README.md"
publish = true
# Measurement tools for working on this crate, not part of it.
exclude = ["tools/"]

[lib]
path = "src/lib.rs"
Expand Down
51 changes: 51 additions & 0 deletions tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Tools for working on frontend

Measurement and reproduction tools, used to find where the time goes. None of them is part of
the crate, and `cargo publish` leaves this directory out.

**The rules still apply.** POSIX `sh` and bun only: no Python, perl, awk or node. Nothing here
runs in the build.

## `bounded.sh`

`bounded.sh SECONDS CMD...` runs a command and kills it after SECONDS, since macOS has no
`timeout`. Every tool below is run under it. It runs the command in the background, where stdin
is `/dev/null`, so to feed stdin wrap the command in `sh -c "... < file"`.

## `profile/`: macOS `sample` profiles, folded with bun

Take a profile of a running `parallel_timing` with `sample PID SECONDS -mayDie -f out.txt`, then:

| Script | What it prints |
| --- | --- |
| `fold.ts FILE [filter]` | Inclusive and self samples per function |
| `kids.ts FILE NAME` | The direct callees of every frame whose name contains NAME |
| `parents.ts FILE NAME [levels]` | Caller chains of those frames |
| `subself.ts FILE... ROOT` | Self samples of the frames under ROOT, summed over files |
| `phase.ts FILE...` | The main thread's own time, charged to the innermost compiler pass on its stack. `PHASES="borrow check;type check bodies"` adds each pass's top functions |
| `share.ts FILE...` | The main thread's own time by kind: allocator, query machinery, hash tables, memory copies, interning, obligation processing |
| `agg.ts FILE` | Minimum and median of `LABEL ... check N ms` lines, per label, for A/B runs |
| `map.ts A B` | Per-pass seconds from two `-Z time-passes` JSON runs, side by side |

`name.ts` decodes v0-mangled symbols for the others.

## `parse-ref/`: frontend's parser against rust-analyzer's and syn's

A crate of its own (not in the workspace). It times the three parsers over the same sources:
`cargo run --release --manifest-path tools/parse-ref/Cargo.toml -- DIR [ROUNDS]` (3 rounds by default; `ONLY_FRONTEND=1` times frontend alone).

## `deps/`: reading a crate with its dependencies loaded, from source

- **`chain.sh`:** reads a dependency chain with `frontend-facts`, in dependency order, each
crate once, `--items`, and writes each crate's metadata for the crates after it. It is the
exact recipe (`--cfg`, `--env`, `--extern`, `--standard-library`, `--proc-macro`) for std's
closure and for anyhow, serde_json and tokio with their closures.
- `chain.sh std` reads std's closure; `chain.sh reg` reads the three registry crates (run
`std` first). `chain.sh anyhow`, `serde_json` or `tokio` reads one.
- Set `LIBRARY` (a rust-src `library/` of frontend's upstream era, with its `vendor/`),
`REGISTRY` (a cargo registry source directory holding the named crate versions), `OUT` (the
output directory) and optionally `FACTS` (the `frontend-facts` binary) and `ARCH`.
- **`check.sh ok e0599 e0061`:** checks the fixtures beside it against the tokio chain.
- `ok.rs` checks clean.
- `e0599.rs` calls a method no type has (E0599).
- `e0061.rs` passes a wrong argument count (E0061).
11 changes: 11 additions & 0 deletions tools/bounded.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh
# bounded.sh SECONDS CMD...: run CMD, kill it after SECONDS. Output to stdout.
limit=$1; shift
"$@" &
p=$!
i=0
while kill -0 $p 2>/dev/null; do
if [ $i -ge $limit ]; then kill -9 $p; echo "WATCHDOG-KILLED after ${limit}s: $*"; exit 124; fi
sleep 1; i=$((i+1))
done
wait $p
94 changes: 94 additions & 0 deletions tools/deps/chain.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/bin/sh
# chain.sh [std|reg|anyhow|serde_json|tokio]: read a dependency chain from source with
# frontend-facts, each crate once, in dependency order, writing each crate's metadata for the
# crates after it. `std` reads std's closure; `reg` reads the three registry crates' closures
# (run `std` first). Every crate is read `--items`.
#
# LIBRARY a rust-src `library/` of frontend's upstream era, with its `vendor/`
# REGISTRY a cargo registry source directory holding the registry crates named below
# OUT where metadata (`meta/lib<name>.rmeta`), facts and logs go
# FACTS the `frontend-facts` binary (default `target/release/frontend-facts`)
# ARCH the target architecture std's build script would report (default `aarch64`)
set -u
here=$(cd "$(dirname "$0")" && pwd)
B=${FACTS:-target/release/frontend-facts}
L=${LIBRARY:?set LIBRARY to a rust-src library/ directory}
V=$L/vendor
R=${REGISTRY:?set REGISTRY to a cargo registry src directory}
S=${OUT:?set OUT to an output directory}
M=$S/meta
mkdir -p $M $S/out
CB="--extern noprelude:compiler_builtins=$M/libcompiler_builtins.rmeta"
# rd NAME ROOT EDITION ARGS...
rd() {
name=$1; root=$2; ed=$3; shift 3
$here/../bounded.sh 600 /usr/bin/time -p $B --root $root --edition $ed --items ${LIBFLAG:-} "$@" --emit-metadata $M/lib$name.rmeta $name > $S/out/$name.json 2> $S/out/$name.err
rc=$?
t=$(grep '^real' $S/out/$name.err | tail -1)
n=$(grep -c '^error' $S/out/$name.err)
echo "$name rc=$rc $t errors=$n"
if [ $rc -ne 0 ]; then grep '^error' -A2 $S/out/$name.err | head -12; exit 1; fi
}
case "${1:-std}" in
std)
LIBFLAG=--standard-library
rd core $L/core/src/lib.rs 2024
rd compiler_builtins $L/compiler-builtins/compiler-builtins/src/lib.rs 2024 --cfg 'feature="compiler-builtins"' --cfg 'feature="unmangled-names"' --extern core=$M/libcore.rmeta
rd alloc $L/alloc/src/lib.rs 2024 --extern core=$M/libcore.rmeta $CB
rd cfg_if $V/cfg-if-1.0.4/src/lib.rs 2018 --cfg 'feature="rustc-dep-of-std"' --cfg 'feature="core"' --extern core=$M/libcore.rmeta $CB
rd libc $V/libc-0.2.189/src/lib.rs 2021 --cfg 'feature="rustc-dep-of-std"' --cfg 'feature="align"' --cfg 'feature="rustc-std-workspace-core"' --extern rustc_std_workspace_core=$M/libcore.rmeta $CB
rd rustc_demangle $V/rustc-demangle-0.1.28/src/lib.rs 2015 --cfg 'feature="rustc-dep-of-std"' --cfg 'feature="core"' --extern core=$M/libcore.rmeta $CB
rd hashbrown $V/hashbrown-0.17.1/src/lib.rs 2024 --cfg 'feature="rustc-dep-of-std"' --cfg 'feature="nightly"' --cfg 'feature="core"' --cfg 'feature="alloc"' --cfg 'feature="rustc-internal-api"' --extern core=$M/libcore.rmeta --extern alloc=$M/liballoc.rmeta $CB
rd unwind $L/unwind/src/lib.rs 2024 --extern core=$M/libcore.rmeta --extern libc=$M/liblibc.rmeta --extern noprelude:rustc_std_workspace_core=$M/libcore.rmeta $CB
rd panic_abort $L/panic_abort/src/lib.rs 2024 --extern core=$M/libcore.rmeta --extern alloc=$M/liballoc.rmeta $CB
rd std_detect $L/std_detect/src/lib.rs 2024 --extern core=$M/libcore.rmeta --extern alloc=$M/liballoc.rmeta --extern libc=$M/liblibc.rmeta --extern noprelude:rustc_std_workspace_core=$M/libcore.rmeta $CB
rd panic_unwind $L/panic_unwind/src/lib.rs 2024 --extern core=$M/libcore.rmeta --extern alloc=$M/liballoc.rmeta --extern libc=$M/liblibc.rmeta --extern unwind=$M/libunwind.rmeta --extern cfg_if=$M/libcfg_if.rmeta --extern noprelude:rustc_std_workspace_core=$M/libcore.rmeta $CB
rd std $L/std/src/lib.rs 2024 --cfg backtrace_in_libstd --env STD_ENV_ARCH=${ARCH:-aarch64} \
--extern core=$M/libcore.rmeta --extern alloc=$M/liballoc.rmeta --extern libc=$M/liblibc.rmeta \
--extern cfg_if=$M/libcfg_if.rmeta --extern hashbrown=$M/libhashbrown.rmeta --extern unwind=$M/libunwind.rmeta \
--extern panic_abort=$M/libpanic_abort.rmeta --extern panic_unwind=$M/libpanic_unwind.rmeta --cfg 'feature="panic-unwind"' --extern std_detect=$M/libstd_detect.rmeta \
--extern rustc_demangle=$M/librustc_demangle.rmeta \
--extern noprelude:rustc_std_workspace_core=$M/libcore.rmeta $CB
LIBFLAG=
;;
esac
# std and everything std loads, for a crate that uses std.
STD="--extern std=$M/libstd.rmeta --extern noprelude:core=$M/libcore.rmeta --extern noprelude:alloc=$M/liballoc.rmeta $CB"
for d in libc cfg_if hashbrown unwind panic_abort panic_unwind std_detect rustc_demangle; do
STD="$STD --extern noprelude:$d=$M/lib$d.rmeta"
done
STD="$STD --extern noprelude:rustc_std_workspace_core=$M/libcore.rmeta"
case "${1:-std}" in
reg|anyhow)
rd anyhow $R/anyhow-1.0.104/src/lib.rs 2021 --cfg 'feature="std"' --cfg 'feature="default"' $STD
;;
esac
case "${1:-std}" in
reg|serde_json)
# serde_core `include!`s a file its build script writes; frontend runs no build script, so it is written here.
SERDE_OUT=$S/out-serde_core; mkdir -p $SERDE_OUT
printf '#[doc(hidden)]\npub mod __private229 {\n #[doc(hidden)]\n pub use crate::private::*;\n}\n' > $SERDE_OUT/private.rs
rd serde_core $R/serde_core-1.0.229/src/lib.rs 2021 --cfg 'feature="std"' --cfg 'feature="result"' --cfg 'feature="default"' --env OUT_DIR=$SERDE_OUT --env CARGO_PKG_VERSION_PATCH=229 $STD
rd itoa $R/itoa-1.0.18/src/lib.rs 2021 $STD
rd memchr $R/memchr-2.8.3/src/lib.rs 2021 --cfg 'feature="std"' --cfg 'feature="alloc"' $STD
rd zmij $R/zmij-1.0.23/src/lib.rs 2021 $STD
rd serde_json $R/serde_json-1.0.151/src/lib.rs 2021 --cfg 'feature="std"' --cfg 'feature="default"' --cfg 'fast_arithmetic="64"' \
--extern serde_core=$M/libserde_core.rmeta --extern itoa=$M/libitoa.rmeta --extern memchr=$M/libmemchr.rmeta --extern zmij=$M/libzmij.rmeta $STD
;;
esac
case "${1:-std}" in
reg|tokio)
LIBFLAG=--standard-library; rd rustc_literal_escaper $V/rustc-literal-escaper-0.0.8/src/lib.rs 2021 --cfg 'feature="rustc-dep-of-std"' --extern noprelude:rustc_std_workspace_core=$M/libcore.rmeta --extern noprelude:rustc_std_workspace_std=$M/libstd.rmeta $STD; LIBFLAG=
LIBFLAG=--standard-library; rd proc_macro $L/proc_macro/src/lib.rs 2024 --extern rustc_literal_escaper=$M/librustc_literal_escaper.rmeta $STD; LIBFLAG=
PM="--extern proc_macro=$M/libproc_macro.rmeta --extern noprelude:rustc_literal_escaper=$M/librustc_literal_escaper.rmeta"
rd unicode_ident $R/unicode-ident-1.0.26/src/lib.rs 2021 $STD
rd proc_macro2 $R/proc-macro2-1.0.107/src/lib.rs 2021 --cfg 'feature="proc-macro"' --cfg 'feature="default"' --cfg wrap_proc_macro --extern unicode_ident=$M/libunicode_ident.rmeta $PM $STD
rd quote $R/quote-1.0.47/src/lib.rs 2021 --cfg 'feature="proc-macro"' --cfg 'feature="default"' --extern proc_macro2=$M/libproc_macro2.rmeta --extern noprelude:unicode_ident=$M/libunicode_ident.rmeta $PM $STD
SYNF=""; for f in derive parsing printing clone-impls proc-macro full default; do SYNF="$SYNF --cfg feature=\"$f\""; done
rd syn $R/syn-3.0.6/src/lib.rs 2021 $SYNF --extern proc_macro2=$M/libproc_macro2.rmeta --extern quote=$M/libquote.rmeta --extern unicode_ident=$M/libunicode_ident.rmeta $PM $STD
rd tokio_macros $R/tokio-macros-2.7.2/src/lib.rs 2021 --proc-macro --extern proc_macro2=$M/libproc_macro2.rmeta --extern quote=$M/libquote.rmeta --extern syn=$M/libsyn.rmeta --extern noprelude:unicode_ident=$M/libunicode_ident.rmeta $PM $STD
rd pin_project_lite $R/pin-project-lite-0.2.17/src/lib.rs 2018 $STD
TOKF=""; for f in sync macros rt; do TOKF="$TOKF --cfg feature=\"$f\""; done
rd tokio $R/tokio-1.53.1/src/lib.rs 2021 $TOKF --extern pin_project_lite=$M/libpin_project_lite.rmeta --extern tokio_macros=$M/libtokio_macros.rmeta --extern noprelude:proc_macro=$M/libproc_macro.rmeta $STD
;;
esac
24 changes: 24 additions & 0 deletions tools/deps/check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh
# check.sh FIXTURE...: check each fixture (`ok`, `e0599`, `e0061`, beside this script) against
# tokio and its closure, read by `chain.sh std` and `chain.sh tokio` into `$OUT/meta`. The
# answers are frontend-facts' `Checked` JSON, with the wall clock of each check.
#
# OUT the directory chain.sh wrote
# FACTS the `frontend-facts` binary (default `target/release/frontend-facts`)
set -u
here=$(cd "$(dirname "$0")" && pwd)
B=${FACTS:-target/release/frontend-facts}
S=${OUT:?set OUT to the directory chain.sh wrote}
M=$S/meta
D="--extern tokio=$M/libtokio.rmeta --extern std=$M/libstd.rmeta"
for d in core alloc compiler_builtins libc cfg_if hashbrown unwind panic_abort panic_unwind std_detect rustc_demangle pin_project_lite tokio_macros proc_macro rustc_literal_escaper; do
D="$D --extern noprelude:$d=$M/lib$d.rmeta"
done
D="$D --extern noprelude:rustc_std_workspace_core=$M/libcore.rmeta"
mkdir -p $S/out
for f in "$@"; do
# Through `sh -c`: the watchdog runs the command in the background, where stdin is /dev/null.
"$here/../bounded.sh" 120 sh -c "exec /usr/bin/time -p $B --check --edition 2021 $D user < $here/$f.rs" > $S/out/check-$f.json 2> $S/out/check-$f.err
echo "== $f rc=$? $(grep '^real' $S/out/check-$f.err)"
head -c 900 $S/out/check-$f.json; echo
done
11 changes: 11 additions & 0 deletions tools/deps/e0061.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use tokio::sync::Mutex;

pub async fn bump(m: &Mutex<i32>) -> i32 {
let mut guard = m.lock().await;
*guard += 1;
*guard
}

pub fn make() -> Mutex<i32> {
Mutex::new(0, 1)
}
11 changes: 11 additions & 0 deletions tools/deps/e0599.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use tokio::sync::Mutex;

pub async fn bump(m: &Mutex<i32>) -> i32 {
let mut guard = m.lock_all().await;
*guard += 1;
*guard
}

pub fn make() -> Mutex<i32> {
Mutex::new(0)
}
11 changes: 11 additions & 0 deletions tools/deps/ok.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use tokio::sync::Mutex;

pub async fn bump(m: &Mutex<i32>) -> i32 {
let mut guard = m.lock().await;
*guard += 1;
*guard
}

pub fn make() -> Mutex<i32> {
Mutex::new(0)
}
17 changes: 17 additions & 0 deletions tools/parse-ref/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "parse-ref"
version = "0.1.0"
edition = "2024"
rust-version = "1.97"
publish = false

[dependencies]
frontend = { path = "../..", features = ["diagnostics"] }
ra_ap_syntax = "^0.0.349"
syn = { version = "2", features = ["full", "parsing"], default-features = false }
proc-macro2 = { version = "1", default-features = false }

[profile.release]
debug = false

[workspace]
85 changes: 85 additions & 0 deletions tools/parse-ref/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Parse-only throughput of three Rust parsers over the same files: frontend's `parses`,
//! rust-analyzer's parser building a rowan tree (`ra_ap_syntax`), and `syn`. Wall clock,
//! MB/s, and allocations counted by this binary's global allocator.

use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
use std::time::Instant;

struct Counting;
static ALLOCS: AtomicU64 = AtomicU64::new(0);
static BYTES: AtomicU64 = AtomicU64::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Relaxed);
BYTES.fetch_add(l.size() as u64, Relaxed);
unsafe { System.alloc(l) }
}
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
unsafe { System.dealloc(p, l) }
}
unsafe fn realloc(&self, p: *mut u8, l: Layout, n: usize) -> *mut u8 {
ALLOCS.fetch_add(1, Relaxed);
BYTES.fetch_add(n as u64, Relaxed);
unsafe { System.realloc(p, l, n) }
}
}
#[global_allocator]
static A: Counting = Counting;

fn catcher(f: &mut dyn FnMut()) -> Result<(), frontend::unwind_janky::Payload> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
}

fn files(dir: &str) -> Vec<String> {
let mut paths = Vec::new();
let mut stack = vec![std::path::PathBuf::from(dir)];
while let Some(d) = stack.pop() {
for e in std::fs::read_dir(&d).unwrap().flatten() {
let p = e.path();
if p.is_dir() { stack.push(p) } else if p.extension().is_some_and(|x| x == "rs") { paths.push(p) }
}
}
paths.sort();
paths.iter().map(|p| std::fs::read_to_string(p).unwrap()).collect()
}

fn time(name: &str, files: &[String], rounds: usize, mut f: impl FnMut(&str) -> bool) {
let bytes: usize = files.iter().map(String::len).sum();
// One warm-up round, not timed.
let failed = files.iter().filter(|s| !f(s)).count();
let (a0, b0) = (ALLOCS.load(Relaxed), BYTES.load(Relaxed));
let start = Instant::now();
for _ in 0..rounds {
for s in files {
std::hint::black_box(f(s));
}
}
let secs = start.elapsed().as_secs_f64() / rounds as f64;
let allocs = (ALLOCS.load(Relaxed) - a0) / rounds as u64;
let abytes = (BYTES.load(Relaxed) - b0) / rounds as u64;
println!(
"{name:<22} {:>8.1} ms {:>8.1} MB/s {:>11} allocs {:>7.1} allocs/KB {:>8.1} MB alloc ({failed} files refused)",
secs * 1e3,
bytes as f64 / 1e6 / secs,
allocs,
allocs as f64 / (bytes as f64 / 1e3),
abytes as f64 / 1e6,
);
}

fn main() {
frontend::unwind_janky::install_catcher(catcher);
std::panic::set_hook(Box::new(|_| {}));
let dir = std::env::args().nth(1).expect("a directory of .rs files");
let rounds: usize = std::env::args().nth(2).and_then(|n| n.parse().ok()).unwrap_or(3);
let files = files(&dir);
let bytes: usize = files.iter().map(String::len).sum();
println!("{} files, {:.1} MB, {rounds} rounds, single thread", files.len(), bytes as f64 / 1e6);
time("frontend parses", &files, rounds, |s| frontend::frontend_facts::syntax::parses(s).is_ok());
if std::env::var_os("ONLY_FRONTEND").is_some() { return; }
time("rust-analyzer (rowan)", &files, rounds, |s| {
ra_ap_syntax::SourceFile::parse(s, ra_ap_syntax::Edition::Edition2024).errors().is_empty()
});
time("syn", &files, rounds, |s| syn::parse_file(s).is_ok());
}
4 changes: 4 additions & 0 deletions tools/profile/agg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const t = await Bun.file(Bun.argv[2]).text();
const m = new Map<string, number[]>();
for (const l of t.split("\n")) { const x = l.match(/^(\S+) .*check ([\d.]+) ms/); if (x) { (m.get(x[1]) ?? m.set(x[1], []).get(x[1])!).push(+x[2]); } }
for (const [k, v] of m) { v.sort((a,b)=>a-b); console.log(k, "min", v[0], "median", v[Math.floor(v.length/2)], "all", v.join(" ")); }
40 changes: 40 additions & 0 deletions tools/profile/fold.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// fold.ts SAMPLE_FILE [filter]: inclusive and self sample counts per function from macOS `sample`.
const text = await Bun.file(Bun.argv[2]).text();
const filter = Bun.argv[3] ?? "";
const lines = text.split("\n");
const start = lines.findIndex((l) => l.startsWith("Call graph:"));
const end = lines.findIndex((l, i) => i > start && l.startsWith("Total number in stack"));
import { idents } from "./name.ts";
function name(sym: string): string {
const m = sym.match(/_R[A-Za-z0-9_]+/);
if (!m) return sym.replace(/\s+\(in .*$/, "").replace(/\s+\+ .*$/, "").trim().slice(0, 60);
const parts = idents(m[0])
.filter((p) => !/^(frontend|std|core|alloc)$/.test(p));
return parts.slice(-2).join("::");
}
const incl = new Map<string, number>(), self = new Map<string, number>();
const stack: { depth: number; n: number; name: string; kids: number }[] = [];
const flush = (upto: number) => {
while (stack.length && stack[stack.length - 1].depth >= upto) {
const f = stack.pop()!;
self.set(f.name, (self.get(f.name) ?? 0) + Math.max(0, f.n - f.kids));
}
};
for (const l of lines.slice(start + 1, end)) {
const m = l.match(/^([ +!:|]*)(\d+) (.*)$/);
if (!m) continue;
const depth = m[1].length, n = +m[2], nm = name(m[3]);
flush(depth);
if (stack.length) stack[stack.length - 1].kids += n;
// count inclusive once per distinct name on the current path
if (!stack.some((f) => f.name === nm)) incl.set(nm, (incl.get(nm) ?? 0) + n);
stack.push({ depth, n, name: nm, kids: 0 });
}
flush(0);
const show = (title: string, m: Map<string, number>) => {
console.log(`== ${title}`);
[...m].filter(([k]) => k.includes(filter)).sort((a, b) => b[1] - a[1]).slice(0, 25)
.forEach(([k, v]) => console.log(String(v).padStart(7), k));
};
show("self", self);
show("inclusive", incl);
Loading