Track array-predicate model dependencies in O(1) via track_any_change - #12806
Open
tilladam wants to merge 3 commits into
Open
Track array-predicate model dependencies in O(1) via track_any_change#12806tilladam wants to merge 3 commits into
tilladam wants to merge 3 commits into
Conversation
tilladam
force-pushed
the
model-track-any-change
branch
from
August 9, 2026 14:38
65efa25 to
f73f23c
Compare
tilladam
force-pushed
the
model-track-any-change
branch
from
August 12, 2026 15:36
f73f23c to
8961deb
Compare
Model::track_row_data_changes inserted the row into tracked_rows unconditionally, while the Rust ModelNotify equivalent does so only when crate::properties::is_currently_tracking(). Generated C++ reaches these from imperative contexts too — a callback or function body indexing into a model — and there the recorded row registers no dependency, it only makes later changes to that row mark the model's shared row-data property dirty, waking bindings that never asked for it. Export is_currently_tracking over the C ABI as slint_property_is_currently_tracking and wrap it as slint::private_api::is_currently_tracking, then guard the whole body with it, matching the shape of the Rust implementation. changelog: Fixed C++ models marking bindings dirty for row changes they never depended on, when a model row was read outside a binding evaluation.
Follow-up to the review of slint-ui#12790: model_any/model_all/model_find_index used to call track_row_data_changes for every visited row, which inserts each row into the sorted tracked_rows vector — O(n log n) lookups plus O(n²) worst-case insertion to track a whole model, per binding evaluation. Add ModelTracker::track_any_change(row_count), registering the currently evaluated binding as a dependency of any change to the model: the row count or any row's data. The default implementation falls back to track_row_count_changes plus per-row track_row_data_changes — exactly the previous behavior, so forwarding or third-party trackers stay correct; the row_count parameter exists to make that fallback possible. ModelNotify overrides it with a single all_rows_tracked flag plus the two existing dirty-property reads: row_changed short-circuits the tracked_rows binary search on the flag, and the flag is cleared wherever tracked_rows is cleared (row_added/row_removed/reset), after which the dirtied binding re-registers on its next evaluation. The C++ Model class gets the identical treatment (all_rows_tracked member and a track_any_change function), and the C++ model_any/model_all/ model_find_index helpers use it instead of per-row row_data_tracked. Conversely, track_row_data_changes stops recording rows individually while the flag is set, since row_changed already reports every row then, and track_any_change clears the tracked_rows it supersedes. Otherwise a model shared between a repeater and a predicate would keep paying the per-row insertion this commit set out to remove, into a vector nothing reads. One deliberate trade-off: per-row tracking was precise under short-circuiting — any/find-index stop at the first match, so later rows were untracked and changes to them didn't re-evaluate. track_any_change over-approximates: a change to any row re-evaluates the predicate, trading occasional spurious (cheap) re-evaluations for tracking cost independent of the model size. Note the over-approximation is not confined to the predicate: the dirty flag is the model's shared row-data property, so while a predicate binding is registered, a change to a row past its match also dirties the other row-data bindings on that model, which previously had that row in nobody's tracked_rows. Also drops the T: Default bound from model_any/model_all, matching model_find_index. Absent rows are now skipped by all three rather than fed to the predicate as default values — note model_all uses is_none_or, not is_some_and, so an unreadable row doesn't fail the whole model. changelog: Reduced the dependency-tracking cost of the array predicates (`array.any`/`array.all`/`array.find-index`) from per-row to constant.
tilladam
force-pushed
the
model-track-any-change
branch
from
August 12, 2026 17:34
8961deb to
48881f6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to the review discussion in #12790 (#12790 (comment)):
model_any/model_all/model_find_indexused to calltrack_row_data_changesfor every visited row, which inserts each row into the sortedtracked_rowsvector — O(n log n) lookups plus O(n²) worst-case insertion to track a whole model, per binding evaluation.Two commits, reviewable independently.
1.
C++: only record tracked model rows inside a binding evaluationModel::track_row_data_changesrecorded the row unconditionally, while the RustModelNotifyequivalent does so only whenis_currently_tracking(). Generated C++ reaches these from imperative contexts too — a callback or function body indexing into a model — and there the recorded row registers no dependency, it only makes later changes to that row dirty bindings that never asked for it.Exports
is_currently_trackingover the C ABI asslint_property_is_currently_tracking(new symbol) and guards the body with it, matching the Rust shape. Goes first so the second commit never lands the problem in a worse form.2.
Track array-predicate model dependencies in O(1) via track_any_changeAdds
ModelTracker::track_any_change(row_count), registering the currently evaluated binding as a dependency of any change to the model (row count or any row's data):track_row_count_changes+ per-rowtrack_row_data_changes— exactly the previous behavior, so forwarding/third-party trackers stay correct. Therow_countparameter exists to make that correct fallback possible; happy to bikeshed the signature.ModelNotifyoverrides it with a singleall_rows_trackedflag plus the two existing dirty-property reads.row_changedshort-circuits thetracked_rowsbinary search on the flag; the flag is cleared wherevertracked_rowsis cleared (row_added/row_removed/reset), after which the dirtied binding re-registers on its next evaluation. Conversely,track_row_data_changesstops recording rows individually while the flag is set, andtrack_any_changeclears thetracked_rowsit supersedes.Modelclass gets the identical treatment, and the C++model_any/model_all/model_find_indexhelpers use it instead of per-rowrow_data_tracked.One deliberate trade-off: per-row tracking was precise under short-circuiting —
any/find-indexstop at the first match, so later rows were untracked and changes to them didn't re-evaluate.track_any_changeover-approximates. Note this is not confined to the predicate: the dirty flag is the model's shared row-data property, so a change to a row past the match also dirties the other row-data bindings on that model.Also drops the
T: Defaultbound frommodel_any/model_all. Absent rows are skipped by all three rather than fed to the predicate as default values —model_allusesis_none_or, notis_some_and, so an unreadable row doesn't fail the whole model.Tests
Rust:
test_any_change_tracking,test_any_change_subsumes_row_tracking,test_predicates_skip_absent_rows.C++ (catch2):
"Track model row data changes outside a binding","Model any-change tracking","Model any-change tracking outside a binding","Model any-change tracking subsumes row tracking","Array predicates skip absent rows".The two
outside a bindingcases are the ones that fail without the guards; the rest cover re-evaluation on row changes past the match point, add/remove, and reset.