diff --git a/Cargo.lock b/Cargo.lock index 82908741fd623..464b8cde5d968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2053,6 +2053,9 @@ dependencies = [ "criterion", "mimalloc", "once_cell", + "red_knot", + "red_knot_module_resolver", + "ruff_db", "ruff_linter", "ruff_python_ast", "ruff_python_formatter", diff --git a/Cargo.toml b/Cargo.toml index a563af269b580..bfc8d351dca77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ ruff_source_file = { path = "crates/ruff_source_file" } ruff_text_size = { path = "crates/ruff_text_size" } ruff_workspace = { path = "crates/ruff_workspace" } +red_knot = { path = "crates/red_knot" } red_knot_module_resolver = { path = "crates/red_knot_module_resolver" } red_knot_python_semantic = { path = "crates/red_knot_python_semantic" } diff --git a/crates/red_knot/src/program/check.rs b/crates/red_knot/src/program/check.rs index 22633ad9a3ebd..8fe0d58f5fe4b 100644 --- a/crates/red_knot/src/program/check.rs +++ b/crates/red_knot/src/program/check.rs @@ -11,7 +11,7 @@ impl Program { self.with_db(|db| { let mut result = Vec::new(); for open_file in db.workspace.open_files() { - result.extend_from_slice(&db.check_file(open_file)); + result.extend_from_slice(&db.check_file_impl(open_file)); } result @@ -19,7 +19,11 @@ impl Program { } #[tracing::instrument(level = "debug", skip(self))] - fn check_file(&self, file: VfsFile) -> Diagnostics { + pub fn check_file(&self, file: VfsFile) -> Result { + self.with_db(|db| db.check_file_impl(file)) + } + + fn check_file_impl(&self, file: VfsFile) -> Diagnostics { let mut diagnostics = Vec::new(); diagnostics.extend_from_slice(lint_syntax(self, file)); diagnostics.extend_from_slice(lint_semantic(self, file)); diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index c95caf0d13bfd..a2fe36f318873 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -31,6 +31,10 @@ harness = false name = "formatter" harness = false +[[bench]] +name = "red_knot" +harness = false + [dependencies] once_cell = { workspace = true } serde = { workspace = true } @@ -41,11 +45,14 @@ criterion = { workspace = true, default-features = false } codspeed-criterion-compat = { workspace = true, default-features = false, optional = true } [dev-dependencies] +ruff_db = { workspace = true } ruff_linter = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_formatter = { workspace = true } ruff_python_parser = { workspace = true } ruff_python_trivia = { workspace = true } +red_knot = { workspace = true } +red_knot_module_resolver = { workspace = true } [lints] workspace = true diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs new file mode 100644 index 0000000000000..d482580885b42 --- /dev/null +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -0,0 +1,178 @@ +#![allow(clippy::disallowed_names)] + +use red_knot::program::Program; +use red_knot::Workspace; +use red_knot_module_resolver::{set_module_resolution_settings, ModuleResolutionSettings}; +use ruff_benchmark::criterion::{ + criterion_group, criterion_main, BatchSize, Criterion, Throughput, +}; +use ruff_db::file_system::{FileSystemPath, MemoryFileSystem}; +use ruff_db::parsed::parsed_module; +use ruff_db::vfs::{system_path_to_file, VfsFile}; +use ruff_db::Upcast; + +static FOO_CODE: &str = r#" +import typing + +from bar import Bar + +class Foo(Bar): + def foo() -> str: + return "foo" + + @typing.override + def bar() -> str: + return "foo_bar" +"#; + +static BAR_CODE: &str = r#" +class Bar: + def bar() -> str: + return "bar" + + def random(arg: int) -> int: + if arg == 1: + return 48472783 + if arg < 10: + return 20 + return 36673 +"#; + +static TYPING_CODE: &str = r#" +def override(): ... +"#; + +struct Case { + program: Program, + fs: MemoryFileSystem, + foo: VfsFile, + bar: VfsFile, + typing: VfsFile, +} + +fn setup_case() -> Case { + let fs = MemoryFileSystem::new(); + let foo_path = FileSystemPath::new("/src/foo.py"); + let bar_path = FileSystemPath::new("/src/bar.py"); + let typing_path = FileSystemPath::new("/src/typing.pyi"); + fs.write_files([ + (foo_path, FOO_CODE), + (bar_path, BAR_CODE), + (typing_path, TYPING_CODE), + ]) + .unwrap(); + + let workspace_root = FileSystemPath::new("/src"); + let workspace = Workspace::new(workspace_root.to_path_buf()); + + let mut program = Program::new(workspace, fs.clone()); + let foo = system_path_to_file(&program, foo_path).unwrap(); + + set_module_resolution_settings( + &mut program, + ModuleResolutionSettings { + extra_paths: vec![], + workspace_root: workspace_root.to_path_buf(), + site_packages: None, + custom_typeshed: None, + }, + ); + + program.workspace_mut().open_file(foo); + + let bar = system_path_to_file(&program, bar_path).unwrap(); + let typing = system_path_to_file(&program, typing_path).unwrap(); + + Case { + program, + fs, + foo, + bar, + typing, + } +} + +fn benchmark_without_parse(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("red_knot/check_file"); + group.throughput(Throughput::Bytes(FOO_CODE.len() as u64)); + + group.bench_function("red_knot_check_file[without_parse]", |b| { + b.iter_batched( + || { + let case = setup_case(); + // Pre-parse the module to only measure the semantic time. + parsed_module(case.program.upcast(), case.foo); + parsed_module(case.program.upcast(), case.bar); + parsed_module(case.program.upcast(), case.typing); + case + }, + |case| { + let Case { program, foo, .. } = case; + let result = program.check_file(foo).unwrap(); + + assert_eq!(result.as_slice(), [] as [String; 0]); + }, + BatchSize::SmallInput, + ); + }); + + group.finish(); +} + +fn benchmark_incremental(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("red_knot/check_file"); + group.throughput(Throughput::Bytes(FOO_CODE.len() as u64)); + + group.bench_function("red_knot_check_file[incremental]", |b| { + b.iter_batched( + || { + let mut case = setup_case(); + case.program.check_file(case.foo).unwrap(); + + case.fs + .write_file( + FileSystemPath::new("/src/foo.py"), + format!("{BAR_CODE}\n# A comment\n"), + ) + .unwrap(); + + case.bar.touch(&mut case.program); + case + }, + |case| { + let Case { program, foo, .. } = case; + let result = program.check_file(foo).unwrap(); + + assert_eq!(result.as_slice(), [] as [String; 0]); + }, + BatchSize::SmallInput, + ); + }); + + group.finish(); +} + +fn benchmark_cold(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("red_knot/check_file"); + group.throughput(Throughput::Bytes(FOO_CODE.len() as u64)); + + group.bench_function("red_knot_check_file[cold]", |b| { + b.iter_batched( + setup_case, + |case| { + let Case { program, foo, .. } = case; + let result = program.check_file(foo).unwrap(); + + assert_eq!(result.as_slice(), [] as [String; 0]); + }, + BatchSize::SmallInput, + ); + }); + + group.finish(); +} + +criterion_group!(cold, benchmark_without_parse); +criterion_group!(without_parse, benchmark_cold); +criterion_group!(incremental, benchmark_incremental); +criterion_main!(without_parse, cold, incremental); diff --git a/crates/ruff_db/src/file_system/memory.rs b/crates/ruff_db/src/file_system/memory.rs index 096a14db7e099..debe236e4f0e7 100644 --- a/crates/ruff_db/src/file_system/memory.rs +++ b/crates/ruff_db/src/file_system/memory.rs @@ -19,6 +19,7 @@ use crate::file_system::{FileSystem, FileSystemPath, FileType, Metadata, Result} /// Use a tempdir with the real file system to test these advanced file system features and complex file system behavior. /// /// Only intended for testing purposes. +#[derive(Clone)] pub struct MemoryFileSystem { inner: Arc, } diff --git a/crates/ruff_db/src/lib.rs b/crates/ruff_db/src/lib.rs index ceda9c8788c2f..ac2891cabe829 100644 --- a/crates/ruff_db/src/lib.rs +++ b/crates/ruff_db/src/lib.rs @@ -8,7 +8,7 @@ use crate::parsed::parsed_module; use crate::source::{line_index, source_text}; use crate::vfs::{Vfs, VfsFile}; -mod file_revision; +pub mod file_revision; pub mod file_system; pub mod parsed; pub mod source;