-
-
Notifications
You must be signed in to change notification settings - Fork 161
fix(runtime): stop the concat memo probing when it isn't paying (fixes #9391) #9396
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -973,18 +973,24 @@ fn concat_memo_returns_one_object_for_equal_results() { | |||||||||||
| let _lock = crate::gc::global_side_table_test_lock(); | ||||||||||||
| crate::string::concat::test_clear_concat_memo(); | ||||||||||||
|
|
||||||||||||
| crate::string::concat::test_reset_memo_governor(); | ||||||||||||
| let prefix = crate::string::js_string_from_bytes(b"field_".as_ptr(), 6); | ||||||||||||
| let first = crate::string::js_string_concat_value(prefix, 7.0); | ||||||||||||
| // #9391's doorkeeper admits a result on its SECOND sighting, so the third | ||||||||||||
| // evaluation is the first that can share. That ordering is the point: a | ||||||||||||
| // result seen once never costs a rooted entry. | ||||||||||||
| let _first = crate::string::js_string_concat_value(prefix, 7.0); | ||||||||||||
| let second = crate::string::js_string_concat_value(prefix, 7.0); | ||||||||||||
|
Comment on lines
+981
to
+982
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert that the first result is not memoized. The test discards the first result. The assertion on Keep the first pointer value and assert that it differs from Proposed fix- let _first = crate::string::js_string_concat_value(prefix, 7.0);
+ let first_ptr = crate::string::js_string_concat_value(prefix, 7.0) as usize;
let second = crate::string::js_string_concat_value(prefix, 7.0);
+ assert_ne!(first_ptr, second as usize, "the first sighting must not be memoized");📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| // A DIFFERENT prefix object with the same bytes must still reach the entry: | ||||||||||||
| // the memo is keyed on result content, not on operand identity. | ||||||||||||
| let other_prefix = crate::string::js_string_from_bytes(b"field_".as_ptr(), 6); | ||||||||||||
| assert_ne!(prefix as usize, other_prefix as usize); | ||||||||||||
| let second = crate::string::js_string_concat_value(other_prefix, 7.0); | ||||||||||||
| let third = crate::string::js_string_concat_value(other_prefix, 7.0); | ||||||||||||
|
|
||||||||||||
| assert_eq!( | ||||||||||||
| first as usize, second as usize, | ||||||||||||
| "equal concat results must share one memoized string" | ||||||||||||
| second as usize, third as usize, | ||||||||||||
| "once admitted, equal concat results must share one memoized string" | ||||||||||||
| ); | ||||||||||||
| let first = third; | ||||||||||||
| unsafe { | ||||||||||||
| assert_eq!((*first).byte_len, 7); | ||||||||||||
| // Shared, so the in-place `+=` append can never mutate it under a | ||||||||||||
|
|
@@ -1045,3 +1051,75 @@ fn concat_memo_declines_non_ascii_prefixes() { | |||||||||||
| assert_eq!((*a).utf16_len, 3); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /// #9391: the memo must stop PROBING when it stops paying. | ||||||||||||
| /// | ||||||||||||
| /// `bench_gc_pressure` builds half a million distinct `"item_" + i` strings. | ||||||||||||
| /// Before the governor the memo probed every one of them for six hits, and the | ||||||||||||
| /// probe alone — buffer assembly plus hashing — cost the row 21 ms against | ||||||||||||
| /// 12 ms with the memo compiled out. | ||||||||||||
| /// | ||||||||||||
| /// This asserts the governor's DECISION, not a wall-clock number: a timing | ||||||||||||
| /// test here would be noise-sensitive and would not say why it failed. | ||||||||||||
| #[test] | ||||||||||||
| fn concat_memo_governor_disables_itself_when_nothing_hits() { | ||||||||||||
| crate::string::concat::test_reset_memo_governor(); | ||||||||||||
| assert!( | ||||||||||||
| crate::string::concat::test_memo_enabled(), | ||||||||||||
| "governor starts enabled" | ||||||||||||
| ); | ||||||||||||
|
|
||||||||||||
| // One full window of candidates, none of which hit. | ||||||||||||
| let window = crate::string::concat::test_memo_window(); | ||||||||||||
| for _ in 0..window { | ||||||||||||
| crate::string::concat::test_memo_should_probe(); | ||||||||||||
| } | ||||||||||||
| assert!( | ||||||||||||
| !crate::string::concat::test_memo_enabled(), | ||||||||||||
| "a window with no hits must turn the probe off" | ||||||||||||
| ); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /// The other half: a workload that DOES hit keeps the memo on, so the governor | ||||||||||||
| /// cannot silently disable the case the memo exists for | ||||||||||||
| /// (`bench_object_property`, which hits 211,960 times out of 212,000). | ||||||||||||
| #[test] | ||||||||||||
| fn concat_memo_governor_stays_on_when_hits_are_frequent() { | ||||||||||||
| crate::string::concat::test_reset_memo_governor(); | ||||||||||||
| let window = crate::string::concat::test_memo_window(); | ||||||||||||
| for _ in 0..window { | ||||||||||||
| crate::string::concat::test_memo_should_probe(); | ||||||||||||
| crate::string::concat::test_memo_note_hit(); | ||||||||||||
| } | ||||||||||||
| assert!( | ||||||||||||
| crate::string::concat::test_memo_enabled(), | ||||||||||||
| "a window that hits every time must keep the probe on" | ||||||||||||
| ); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /// And it recovers: a backoff always expires into a probation window, so a | ||||||||||||
| /// program whose first phase misses and whose second phase hits is picked up | ||||||||||||
| /// rather than left permanently disabled. | ||||||||||||
| #[test] | ||||||||||||
| fn concat_memo_governor_recovers_after_backoff() { | ||||||||||||
| crate::string::concat::test_reset_memo_governor(); | ||||||||||||
| let window = crate::string::concat::test_memo_window(); | ||||||||||||
| for _ in 0..window { | ||||||||||||
| crate::string::concat::test_memo_should_probe(); | ||||||||||||
| } | ||||||||||||
| assert!(!crate::string::concat::test_memo_enabled()); | ||||||||||||
|
|
||||||||||||
| // Serving the backoff out must eventually re-enable. The exact number of | ||||||||||||
| // windows is a tuning detail; that it terminates is the contract. | ||||||||||||
| let mut windows = 0; | ||||||||||||
| while !crate::string::concat::test_memo_enabled() && windows < 8 { | ||||||||||||
| for _ in 0..window { | ||||||||||||
| crate::string::concat::test_memo_should_probe(); | ||||||||||||
| } | ||||||||||||
| windows += 1; | ||||||||||||
| } | ||||||||||||
| assert!( | ||||||||||||
| crate::string::concat::test_memo_enabled(), | ||||||||||||
| "backoff must expire into a probation window, got stuck for {windows} windows" | ||||||||||||
| ); | ||||||||||||
| } | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 24174
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 11479
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50370
Make the
MEMO_GOVtransition atomic.MEMO_GOVis process-global.concat_memo_should_probestores a value derived from an earlier load, whileconcat_memo_note_hitatomically increments the same word. If runtime workers interleave these paths, the store can overwrite a candidate or hit update. The governor can then undercount hits and disable probing for a hot memo. Use a compare-exchange retry loop, and let only the successful window-closing operation reset counters and update backoff state.🤖 Prompt for AI Agents