Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coarse-grained tracked structs #657

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

ibraheemdev
Copy link
Contributor

This PR makes tracked structs coarse-grained by default. To declare a field independently tracked, you now use the #[tracked] attribute. This is the opposite of the previous behavior, where #[id] effectively declared a field untracked. The #[id] attribute now has no effect. Importantly, reads of untracked fields do not require a read dependency to be declared.

There is some duplicated code in the macros as we need to generate separate accessors for tracked vs. untracked fields and I would like these to be static (as opposed to branching at runtime).

I believe the guide documentation also needs to be updated.

Resolves #598.

Copy link

netlify bot commented Jan 17, 2025

Deploy Preview for salsa-rs canceled.

Name Link
🔨 Latest commit a63bd53
🔍 Latest deploy log https://app.netlify.com/sites/salsa-rs/deploys/678afefe4220bb000804326f

Copy link

codspeed-hq bot commented Jan 17, 2025

CodSpeed Performance Report

Merging #657 will not alter performance

Comparing ibraheemdev:coarse-deps (a63bd53) with master (e4d65a6)

Summary

✅ 9 untouched benchmarks

@ibraheemdev ibraheemdev force-pushed the coarse-deps branch 3 times, most recently from 2937a33 to c331b84 Compare January 17, 2025 02:56
pub name: FunctionId<'db>,

#[tracked]
name_span: Span<'db>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried not adding this #[tracked] attribute because it seems unnecessary, but I actually got a test failure that I don't really understand:

---- type_check::fix_bad_variable_in_function stdout ----
Event: Event { thread_id: ThreadId(11), kind: WillCheckCancellation }
Event: Event { thread_id: ThreadId(11), kind: WillExecute { database_key: parse_statements(Id(0)) } }
Event: Event { thread_id: ThreadId(11), kind: WillCheckCancellation }
Event: Event { thread_id: ThreadId(11), kind: WillExecute { database_key: type_check_program(Id(1400)) } }
Event: Event { thread_id: ThreadId(11), kind: WillCheckCancellation }
Event: Event { thread_id: ThreadId(11), kind: WillExecute { database_key: type_check_function(Id(1800)) } }
Event: Event { thread_id: ThreadId(11), kind: WillCheckCancellation }
Event: Event { thread_id: ThreadId(11), kind: WillExecute { database_key: type_check_function(Id(1801)) } }
Event: Event { thread_id: ThreadId(11), kind: WillCheckCancellation }
Event: Event { thread_id: ThreadId(11), kind: WillExecute { database_key: find_function(Id(1c00)) } }
Event: Event { thread_id: ThreadId(11), kind: WillCheckCancellation }
Event: Event { thread_id: ThreadId(11), kind: WillCheckCancellation }
Event: Event { thread_id: ThreadId(11), kind: WillExecute { database_key: find_function(Id(1c01)) } }
Event: Event { thread_id: ThreadId(11), kind: DidSetCancellationFlag }
Event: Event { thread_id: ThreadId(11), kind: WillCheckCancellation }
Event: Event { thread_id: ThreadId(11), kind: WillExecute { database_key: parse_statements(Id(0)) } }
Event: Event { thread_id: ThreadId(11), kind: WillCheckCancellation }
Event: Event { thread_id: ThreadId(11), kind: WillCheckCancellation }
thread 'type_check::fix_bad_variable_in_function' panicked at /home/ibraheem/dev/sync/salsa/src/table.rs:281:9:
assertion `left == right` failed: page has hidden type `"salsa::table::Page<salsa::tracked_struct::Value<calc::ir::Function>>"` but `"salsa::table::Page<salsa::tracked_struct::Value<calc::ir::Program>>"` was expected
  left: TypeId(0xbeb3ca6f8846f92e6ae4fc7569b4bf5f)
 right: TypeId(0xbbdce2561688eb1bfc9fa5f5006a8342)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some debug statements to function::maybe_chagned_after to log the dependencies and compared the logs between a run that succeeds and a run that fails.

The most interesting difference is

Successful

[src/tracked_struct/tracked_field.rs:58:9] self = salsa::tracked_struct::tracked_field::FieldIngredientImpl<calc::ir::Program> {
    ingredient_index: IngredientIndex(
        13,
    ),
    field_index: 0,
}
Event: Event { thread_id: ThreadId(2), kind: WillCheckCancellation }
[src/function/maybe_changed_after.rs:154:9] &database_key_index = type_check_function(Id(1800))
[src/function/maybe_changed_after.rs:156:15] &old_memo.revisions.origin = Derived(
    QueryEdges {
        input_outputs: [
            Input(
                Function.args(Id(1000)),
            ),
            Input(
                Program(Id(1000)),
            ),
            Input(
                Span.start(Id(406)),
            ),
            Input(
                Span.end(Id(406)),
            ),
        ],
    },
)

With Panic

[src/tracked_struct/tracked_field.rs:58:9] self = salsa::tracked_struct::tracked_field::FieldIngredientImpl<calc::ir::Program> {
    ingredient_index: IngredientIndex(
        12,
    ),
    field_index: 0,
}
Event: Event { thread_id: ThreadId(2), kind: WillCheckCancellation }
[src/function/maybe_changed_after.rs:154:9] &database_key_index = type_check_function(Id(1800))
[src/function/maybe_changed_after.rs:156:15] &old_memo.revisions.origin = Derived(
    QueryEdges {
        input_outputs: [
            Input(
                Program(Id(1000)),
            ),
            Input(
                Program.statements(Id(1000)),
            ),
            Input(
                Span.start(Id(406)),
            ),
            Input(
                Span.end(Id(406)),
            ),
        ],
    },
)

The main difference is that the ingredient on which we run maybe_changed_after is different, AND so are the inputs. Looking at type_check_function then the inputs of the panic case make no sense. The query never calls Program.statements but it does call Function.args. I don't understand how it is happening but I suspect that we're incorrectly reusing a memo and turn a Function memo into a Program which would align with the panic

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think I know what the issue is...

You changed the tracked struct's Configuration::create_ingredients to only create ingredients for tracked fields. I think this change is correct. However, you call Ingredient::Index::successor in tracked_field with the field_index which is the absolute offset including all fields -- including untracked fields. What this means is that we now add incorrect dependencies as soon as we have a tracked struct where a tracked field follows an untracked field.

I hope that helps. I let you take a stab at correctly mapping the field indices. It would be great to have a unit test demonstrating the behavior and possibly an assertion somewhere that would help us catch this sooner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that is a tricky. I tried a couple things here:

  • Creating the ingredients with the absolute indices directly. Unfortunately this causes a panic, as some of the ingredient code relies on indices being create sequentially.
  • Passing the correct relative index to successor in tracked_field. Unfortunately this also did not seem to work correctly and didn't result in the correct fields being tracked, but I'm not exactly sure why. The logging code also relies on the indices being absolute in order to print field names, so this was a little awkward.

Eventually I think the easiest solution is just creating ingredients for all fields. I added a failing test that is now passing with this fix (and also fails with the second idea I tried).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's unfortunate but seems reasonable. Would it be possible to avoid creating any field ingredients if the structure only has tracked fields? Doing this optimization seems worthwhile, considering that all-fields-untracked is the new default

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that keeping a mapping table from field_index to relative_ingredient_index (a [Option<usize>] where the value is Some for all tracked fields) is annoying to write the macro code for? If not, maybe that's worth a try. I otherwise suggest we keep it as is for now (but optimize for the no-tracked fields case)

Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me. I think the idea of tracked and untracked fields is easier to understand than id (I'm fairly certain we used id incorrectly in Red Knot).

I think I have an understanding why the panic occurs and I added an inline comment with an explanation. I let you take a stab at fixing it.

src/tracked_struct.rs Show resolved Hide resolved
components/salsa-macro-rules/src/setup_tracked_struct.rs Outdated Show resolved Hide resolved
pub name: FunctionId<'db>,

#[tracked]
name_span: Span<'db>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some debug statements to function::maybe_chagned_after to log the dependencies and compared the logs between a run that succeeds and a run that fails.

The most interesting difference is

Successful

[src/tracked_struct/tracked_field.rs:58:9] self = salsa::tracked_struct::tracked_field::FieldIngredientImpl<calc::ir::Program> {
    ingredient_index: IngredientIndex(
        13,
    ),
    field_index: 0,
}
Event: Event { thread_id: ThreadId(2), kind: WillCheckCancellation }
[src/function/maybe_changed_after.rs:154:9] &database_key_index = type_check_function(Id(1800))
[src/function/maybe_changed_after.rs:156:15] &old_memo.revisions.origin = Derived(
    QueryEdges {
        input_outputs: [
            Input(
                Function.args(Id(1000)),
            ),
            Input(
                Program(Id(1000)),
            ),
            Input(
                Span.start(Id(406)),
            ),
            Input(
                Span.end(Id(406)),
            ),
        ],
    },
)

With Panic

[src/tracked_struct/tracked_field.rs:58:9] self = salsa::tracked_struct::tracked_field::FieldIngredientImpl<calc::ir::Program> {
    ingredient_index: IngredientIndex(
        12,
    ),
    field_index: 0,
}
Event: Event { thread_id: ThreadId(2), kind: WillCheckCancellation }
[src/function/maybe_changed_after.rs:154:9] &database_key_index = type_check_function(Id(1800))
[src/function/maybe_changed_after.rs:156:15] &old_memo.revisions.origin = Derived(
    QueryEdges {
        input_outputs: [
            Input(
                Program(Id(1000)),
            ),
            Input(
                Program.statements(Id(1000)),
            ),
            Input(
                Span.start(Id(406)),
            ),
            Input(
                Span.end(Id(406)),
            ),
        ],
    },
)

The main difference is that the ingredient on which we run maybe_changed_after is different, AND so are the inputs. Looking at type_check_function then the inputs of the panic case make no sense. The query never calls Program.statements but it does call Function.args. I don't understand how it is happening but I suspect that we're incorrectly reusing a memo and turn a Function memo into a Program which would align with the panic

pub name: FunctionId<'db>,

#[tracked]
name_span: Span<'db>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think I know what the issue is...

You changed the tracked struct's Configuration::create_ingredients to only create ingredients for tracked fields. I think this change is correct. However, you call Ingredient::Index::successor in tracked_field with the field_index which is the absolute offset including all fields -- including untracked fields. What this means is that we now add incorrect dependencies as soon as we have a tracked struct where a tracked field follows an untracked field.

I hope that helps. I let you take a stab at correctly mapping the field indices. It would be great to have a unit test demonstrating the behavior and possibly an assertion somewhere that would help us catch this sooner.

let id = C::deref_struct(s);
let data = Self::data(zalsa.table(), id);

data.read_lock(zalsa.current_revision());
Copy link
Contributor Author

@ibraheemdev ibraheemdev Jan 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the read_lock necessary here for an untracked field? It seems not because leak_fields does not call it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a 100% but the comment on Value::updated_at and the inline comment inside update does suggest to me that the lock is required before accessing any field (handing out references to field) and it not being present in leak_field seems like a bug.

Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks. This is great. I think we should be able to optimize the implementation to avoid creating any field-ingredients if all fields are tracked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Coarse-grained tracked structs
2 participants