Skip to content

Add a PyTorch cold-start "time to first index" benchmark#11

Open
kinto0 wants to merge 13 commits into
mainfrom
pytorch-cold-start-bench
Open

Add a PyTorch cold-start "time to first index" benchmark#11
kinto0 wants to merge 13 commits into
mainfrom
pytorch-cold-start-bench

Conversation

@kinto0

@kinto0 kinto0 commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Adds cold_start_go_to_definition next to the existing error_propagation bench in pyrefly/benches/pytorch.rs.

From a fresh server it initializes, opens torch/distributed/pipelining/_backward.py (deep in the dependency graph), and resolves the first cross-file go-to-definition on from torch.nn import Parameter. That response can only be produced once the file and its import closure have been analyzed, so the measured wall time is the time-to-first-index. A fresh server per sample makes each run a genuine cold start; shared LSP setup is factored into lsp_args().

Local numbers: cold-start ~3.8s, error-propagation ~150ms.

Note: cargo codspeed run now executes both pytorch benches under Valgrind, so CI cost increases; worth watching the first run.

kinto0 and others added 13 commits June 24, 2026 07:41
Summary:

currently, someone needs to comment on a github issue before a maintainer can manually assign it to them.

this rarely happens before a PR is open. Instead, we should let contributors assign themselves.

Reviewed By: connernilsen

Differential Revision: D109479872
Sets up the scaffolding for running Pyrefly microbenchmarks under divan with
CodSpeed instrumentation, so benchmark regressions can be tracked automatically
on pull requests.

codspeed-divan-compat is a drop-in replacement for the divan benchmarking crate:
locally it runs as a normal walltime harness, while in CI (under `cargo
codspeed`) it emits instrumented measurements. Because the bench macros it
re-exports expand to `divan::` paths, the crate is aliased to `divan`
(package = "codspeed-divan-compat") in Cargo.toml.

Adds a [[bench]] (harness = false) and the divan dev-dependency on the pyrefly
crate, plus the harness scaffolding in pyrefly/benches/micro.rs: a SHARED_STATE
that pre-initializes stdlib so only snippet checking is measured, a check_code
helper, divan::main(), and a smoke benchmark. The real benchmarks are ported in
the next commit.
Adds a GitHub Actions workflow that, on pushes to main and on PRs, installs
cargo-codspeed, builds the pyrefly benchmark target, and runs it through the
CodSpeed action in instruction-count simulation mode. Authentication uses GitHub
OpenID Connect (id-token: write), so no CODSPEED_TOKEN secret is required.

Also adds a [profile.bench] that inherits release but disables LTO and uses
multiple codegen units, keeping the CodSpeed build within standard CI runner
memory limits, and adds a CodSpeed status badge to the README.
The symlink pointed at ../pyrefly/rust-toolchain, whose target was deleted
in e1416af as redundant, leaving the symlink dangling. Swatinem/rust-cache
globs for rust-toolchain files and crashes with ENOENT when it follows the
broken link, breaking the CodSpeed (and eventually all rust-cache) CI jobs.

rustup already resolves the toolchain by walking up to the repo-root
rust-toolchain.toml, so the symlink was redundant; removing it fixes the
cache step with no loss of toolchain pinning for the wasm build.
The lsp_interaction tests drive the language server through an in-process
`LspInteraction`/`TestClient` harness (`object_model`). We want the Pyrefly
benchmarks to exercise the same realistic LSP path, but a `benches/` target and
the test target are separate compilation units, so neither can import the
other's modules — the harness has to live in a library both depend on.

Move `object_model` and `init` into a new dev-only crate, `pyrefly_lsp_test`,
rather than into the shipping `pyrefly` library (which would require feature/cfg
gymnastics and pull `pretty_assertions` into normal builds). The test target now
re-exports `pyrefly_lsp_test::object_model` so its 50+ modules keep referring to
`crate::object_model` unchanged; `ClientRequestHandle::id` becomes `pub` since it
is now read across a crate boundary.

Also drop the `#[ignore]`d `pytorch_benchmark` manual test; the next commits port
it to a CodSpeed benchmark built on this shared harness.
The PyTorch error-propagation benchmark needs a large, real-world Python
codebase to type-check, which isn't present on CI runners. Vendor it as a git
submodule pinned to a specific PyTorch revision (under `pyrefly/benches/pytorch`)
so the benchmark workload is reproducible across local runs and CI without
bloating the repository with copied files.
The old `pytorch_benchmark` was an `#[ignore]`d manual test: it measured how long
a type error takes to propagate across PyTorch (from `torch/nn/__init__.py` to
`torch/distributed/pipelining/_backward.py`), but nothing ran it automatically,
so regressions in incremental-recheck latency went untracked.

Re-express it as a divan benchmark under the same `codspeed-divan-compat` harness
as `micro.rs`, registered as a `[[bench]]` and run by the CodSpeed CI workflow
(which now checks out the PyTorch submodule). The cold start — server launch and
the first full project check — happens in `with_inputs`, outside the measured
region, so only the propagation latency is timed; a fresh server per sample keeps
the non-idempotent edit correct, and an RAII guard restores the edited file so the
submodule working tree stays clean.

The trigger rebinds `Parameter` to a non-type value instead of deleting its
re-export: deleting it lets a case-insensitive filesystem resolve `Parameter` to
the `parameter.py` submodule and mask the error, so the rebind makes the
benchmark deterministic across platforms. When the submodule isn't checked out
the benchmark skips, so `cargo bench` still works in a bare checkout.
CodSpeed's simulation mode runs the whole benchmark binary under Valgrind, which
slows execution ~10-50x and serializes threads. The cold start — a full check of
everything in PyTorch's `pyrefly.toml` project-includes — then stayed silent past
the harness's 50s message timeout, so setup timed out before any measurement ran.

Shrink and desensitize the unmeasured cold start: use `IndexingMode::None` so only
the two opened files and their import closure are checked (dependency-based
rechecking of open files, which is what we measure, is unaffected), wait for the
files' diagnostics to merely arrive rather than for an exact error count (which
varies by pyrefly version/platform), and let the harness client use much larger
timeouts via a new `TestClient::set_timeouts` for these slow instrumented runs.
The existing pytorch benchmark measures incremental recheck latency, but nothing
tracks the cold-start cost a developer pays when first opening a large project —
the initial index/check before the IDE can answer anything.

Add `cold_start_go_to_definition` alongside it: from a fresh server it
initializes, opens a file deep in the dependency graph, and resolves the first
cross-file go-to-definition (`from torch.nn import Parameter`). The response can
only be produced once that file and its import closure have been analyzed, so the
measured wall time is the time-to-first-index. A fresh server per sample makes
each run a genuine cold start. Shared LSP setup is factored into `lsp_args()`.
@github-actions github-actions Bot added the size/m label Jul 1, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 1, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 2 untouched benchmarks
🆕 1 new benchmark

Performance Changes

Benchmark BASE HEAD Efficiency
🆕 cold_start_go_to_definition N/A 66.8 s N/A

Comparing pytorch-cold-start-bench (340f71c) with main (5c755c4)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant