Skip to content

Runaway execution time for attribute assignments with cyclic reachability constraints #711

@sharkdp

Description

@sharkdp

The following example causes the ty run time to grow exponentially with the number of isinstance checks. This is very similar to the problematic case in astral-sh/ruff#18669, with the only difference that the attributes are actually defined, circumventing the patch in astral-sh/ruff#18669.

class C:
    def f(self: "C"):
        self.a = ""
        self.b = ""
    
        if isinstance(self.a, str):
            return
    
        if isinstance(self.b, str):
            return
        if isinstance(self.b, str):
            return
        # [repeat >10 times]

For some reason, this scales even worse than the previous example. We end up doing 16 × 3^N - 38 member_lookup_with_policy calls, where N is the number of isinstance(self.b, str) checks. The previous case "only" scaled with 2^N.

patch with a micro benchmark
diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs
index 3093bd6b7a..a4d9ed2536 100644
--- a/crates/ruff_benchmark/benches/ty.rs
+++ b/crates/ruff_benchmark/benches/ty.rs
@@ -348,10 +348,10 @@ fn benchmark_many_tuple_assignments(criterion: &mut Criterion) {
     });
 }
 
-fn benchmark_many_attribute_assignments(criterion: &mut Criterion) {
+fn benchmark_complex_constrained_attributes_1(criterion: &mut Criterion) {
     setup_rayon();
 
-    criterion.bench_function("ty_micro[many_attribute_assignments]", |b| {
+    criterion.bench_function("ty_micro[complex_constrained_attributes_1]", |b| {
         b.iter_batched_ref(
             || {
                 // This is a regression benchmark for https://github.com/astral-sh/ty/issues/627.
@@ -400,6 +400,48 @@ fn benchmark_many_attribute_assignments(criterion: &mut Criterion) {
     });
 }
 
+fn benchmark_complex_constrained_attributes_2(criterion: &mut Criterion) {
+    setup_rayon();
+
+    criterion.bench_function("ty_micro[complex_constrained_attributes_2]", |b| {
+        b.iter_batched_ref(
+            || {
+                // This is is similar to the case above, but now the attributes are actually defined
+                setup_micro_case(
+                    r#"
+                    class C:
+                        def f(self: "C"):
+                            self.a = ""
+                            self.b = ""
+
+                            if isinstance(self.a, str):
+                                return
+
+                            if isinstance(self.b, str):
+                                return
+                            if isinstance(self.b, str):
+                                return
+                            if isinstance(self.b, str):
+                                return
+                            if isinstance(self.b, str):
+                                return
+                            if isinstance(self.b, str):
+                                return
+                            if isinstance(self.b, str):
+                                return
+                    "#,
+                )
+            },
+            |case| {
+                let Case { db, .. } = case;
+                let result = db.check();
+                assert_eq!(result.len(), 0);
+            },
+            BatchSize::SmallInput,
+        );
+    });
+}
+
 struct ProjectBenchmark<'a> {
     project: InstalledProject<'a>,
     fs: MemoryFileSystem,
@@ -535,7 +577,8 @@ criterion_group!(
     micro,
     benchmark_many_string_assignments,
     benchmark_many_tuple_assignments,
-    benchmark_many_attribute_assignments,
+    benchmark_complex_constrained_attributes_1,
+    benchmark_complex_constrained_attributes_2,
 );
 criterion_group!(project, anyio, attrs, hydra);
 criterion_main!(check_file, micro, project);

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghang

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions