Skip to content

Commit 8265d8f

Browse files
committed
Avoid frame materialization for primitive prototype reads
1 parent a8726fb commit 8265d8f

4 files changed

Lines changed: 133 additions & 8 deletions

File tree

crates/qjs-runtime/src/bytecode/vm_props.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ use super::vm_set::property_set_uses_setter;
1818
use crate::CallEnv;
1919

2020
impl Vm<'_> {
21+
fn primitive_prototype_env(&self) -> CallEnv {
22+
if self.env.dynamic_function_realm_global().is_some() {
23+
self.current_env()
24+
} else {
25+
self.realm_env()
26+
}
27+
}
28+
2129
/// Whether the realm's current Array.prototype owns any indexed property.
2230
/// Returns `None` when no Array.prototype is reachable. The Array.prototype
2331
/// object is cached so the hot path skips the `Array`-binding lookup; the
@@ -116,9 +124,12 @@ impl Vm<'_> {
116124
/// data property (no getter). Returns `None` to signal that the generic
117125
/// clone-and-writeback path is required: any accessor descriptor, a Proxy
118126
/// target, or a primitive base that needs intrinsic lookups not covered
119-
/// here. Prototype intrinsics are read out of `&self.globals` directly,
120-
/// avoiding the full `current_env()` map copy on the dominant method-call
121-
/// and member-read patterns.
127+
/// here. Prototype intrinsics normally use a realm-only environment,
128+
/// avoiding the full `current_env()` frame materialization on the dominant
129+
/// method-call and member-read patterns. The marked-realm compatibility
130+
/// path retains the complete frame view. Ordinary frame bindings named
131+
/// `String`, `Number`, and so on cannot affect a primitive value's
132+
/// intrinsic prototype.
122133
pub(super) fn try_direct_get(&self, object: &Value, key: &PropertyKey) -> Option<Value> {
123134
match key {
124135
PropertyKey::String(name) => self.try_direct_get_string(object, name),
@@ -130,7 +141,7 @@ impl Vm<'_> {
130141
match object {
131142
Value::Object(object) => {
132143
if symbol::is_symbol_primitive(object) {
133-
let env = self.current_env();
144+
let env = self.primitive_prototype_env();
134145
return data_property_value(inherited_primitive_prototype_descriptor(
135146
&env, "Symbol", key,
136147
));
@@ -217,25 +228,25 @@ impl Vm<'_> {
217228
if let Some(value) = string::string_property(value, key) {
218229
return Some(value);
219230
}
220-
let env = self.current_env();
231+
let env = self.primitive_prototype_env();
221232
data_property_value(inherited_primitive_prototype_descriptor(
222233
&env, "String", key,
223234
))
224235
}
225236
Value::Number(_) => {
226-
let env = self.current_env();
237+
let env = self.primitive_prototype_env();
227238
data_property_value(inherited_primitive_prototype_descriptor(
228239
&env, "Number", key,
229240
))
230241
}
231242
Value::Boolean(_) => {
232-
let env = self.current_env();
243+
let env = self.primitive_prototype_env();
233244
data_property_value(inherited_primitive_prototype_descriptor(
234245
&env, "Boolean", key,
235246
))
236247
}
237248
Value::BigInt(_) => {
238-
let env = self.current_env();
249+
let env = self.primitive_prototype_env();
239250
data_property_value(inherited_primitive_prototype_descriptor(
240251
&env, "BigInt", key,
241252
))

crates/qjs-runtime/src/function/env.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,28 @@ impl CallEnv {
972972
self.realm.borrow().get(name).cloned()
973973
}
974974

975+
/// Returns the active global object override used by indirect eval and
976+
/// dynamically constructed functions. A frame-local marker, including a
977+
/// non-object value that disables an outer override, wins before the
978+
/// realm's cached marker.
979+
pub(crate) fn dynamic_function_realm_global(&self) -> Option<ObjectRef> {
980+
let frame_value = self
981+
.frame_bindings
982+
.get(DYNAMIC_FUNCTION_REALM_GLOBAL)
983+
.or_else(|| {
984+
self.deopt_bindings
985+
.as_ref()
986+
.and_then(|bindings| bindings.get(DYNAMIC_FUNCTION_REALM_GLOBAL))
987+
});
988+
if let Some(value) = frame_value {
989+
return match value {
990+
Value::Object(global) => Some(global),
991+
_ => None,
992+
};
993+
}
994+
self.realm.dynamic_function_realm_global()
995+
}
996+
975997
/// Returns the realm's internal global-this slot without hashing its
976998
/// private string key. The slot is fixed when a realm is created; writes
977999
/// to the public `globalThis` property do not replace this identity.

crates/qjs-runtime/src/tests/bytecode.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,35 @@ fn named_get_fast_path_preserves_observable_prototype_fallbacks() {
518518
);
519519
}
520520

521+
#[test]
522+
fn primitive_named_get_uses_realm_prototypes_without_frame_shadowing() {
523+
assert_eq!(
524+
eval_bytecode_source(
525+
"let symbol = Symbol('marker'); \
526+
function read(String, Number, Boolean, BigInt, Symbol) { \
527+
return 'ab'.charAt(1) + ':' + (12).toString() + ':' + \
528+
true.toString() + ':' + (3n).toString() + ':' + symbol.description; \
529+
} \
530+
read(null, null, null, null, null);"
531+
),
532+
Ok(Value::String("b:12:true:3:marker".to_owned().into()))
533+
);
534+
assert_eq!(
535+
eval_bytecode_source(
536+
"let hits = 0; let original = String.prototype.charAt; \
537+
Object.defineProperty(String.prototype, 'charAt', { \
538+
configurable: true, get: function() { hits++; return original; } \
539+
}); \
540+
let result = 'xy'.charAt(1); \
541+
Object.defineProperty(String.prototype, 'charAt', { \
542+
configurable: true, writable: true, value: original \
543+
}); \
544+
result + ':' + hits;"
545+
),
546+
Ok(Value::String("y:1".to_owned().into()))
547+
);
548+
}
549+
521550
#[test]
522551
fn named_get_cache_tracks_receiver_identity_and_property_mutations() {
523552
assert_eq!(

tasks/T018-broad-performance.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2341,6 +2341,69 @@ This general global-binding optimization materially improves two unrelated
23412341
external sources while preserving the internal regression guard; B4 and B5
23422342
remain active.
23432343

2344+
The exact-SHA hosted workflows for
2345+
`a8726fb030e34ffd72d38240a8c6d5b017cbfe41` all completed successfully. CI
2346+
passed the comparison, Test262 subset, and full check jobs. Exact Test262
2347+
coverage remained 42,671 pass, one fail, zero timeout, and one actionable gap.
2348+
The hosted broad preview retained 25/25 cases, 3/3 valid blocks, and passing
2349+
linearity, but its 1.0179x candidate/base result was classified inconclusive:
2350+
several unrelated cases moved together on the variable hosted runner. The
2351+
candidate/QuickJS-NG diagnostic was 0.3574x. More importantly for the campaign
2352+
boundary, the hosted external preview still failed B5 by a wide margin at
2353+
12.608x QuickJS-NG for 5/5 JetStream cases, 7.736x for 7/14 Kraken cases, and
2354+
11.684x for 23/26 SunSpider cases. Hosted broad raw/report SHA-256 are
2355+
`e40fbd919b692b2ba31f0b8c08588a65a3258646e19535f9e58550c8c2039514`
2356+
and `f4b208db24a1b567506e9a7571973b4a6f1308966276128205a538fe4ce4f113`;
2357+
hosted external raw/report SHA-256 are
2358+
`68081a1cf2f8a0b841241d5740db57b3059fa2b54697f21d9f9d87584e631e8b`
2359+
and `94414e047fd7fb53be81b35dd98949ef3ee348957e8396f1e936f308fbccc284`.
2360+
2361+
The forty-third v2 unit follows the external string profile into primitive
2362+
named-property reads. Looking up a data method on a string, number, boolean,
2363+
bigint, or symbol previously materialized every live frame binding into a
2364+
`CallEnv`, even though primitive `[[Get]]` resolves the realm's intrinsic
2365+
prototype and frame bindings named `String`, `Number`, and so on are
2366+
irrelevant. The direct path now uses an empty-frame realm view. Accessor
2367+
descriptors still take the observable slow path. A cached dynamic-realm marker
2368+
keeps indirect eval and dynamically constructed functions on the complete
2369+
frame view; this guard was added after the full runtime suite caught the
2370+
cross-realm case during development. Tests cover all five primitive families,
2371+
frame-name shadowing, a prototype getter side effect, and the pre-existing
2372+
marked-realm behavior. The implementation contains no workload name, source
2373+
path, iteration count, checksum, or expected result.
2374+
2375+
Against base binary SHA-256
2376+
`8b566b3cb05d6570273b20248654403ba92af6288ecf487bd127c437b631a70b`,
2377+
candidate binary SHA-256
2378+
`2a7d910a70158e6c92f22674447bbbe56d515b6e4918d11ecce47b22cb609364`
2379+
improved two independent external string programs in eleven-block interleaved
2380+
runs: `string-validate-input` measured 0.778007x base and `string-base64`
2381+
0.637258x. `crypto-md5` and `crypto-sha1` were neutral at 1.000493x and
2382+
0.999578x, so they are not counted as wins. Complete corrected-candidate and
2383+
same-window base external previews retained identical coverage at 5/5
2384+
JetStream, 10/14 Kraken, and 23/26 SunSpider. Common-case candidate/base suite
2385+
geometric means were 0.981036x, 0.971765x, and 0.949689x; candidate/QuickJS-NG
2386+
remained far from B5 at 9.456082x, 5.507269x, and 6.260206x. Candidate external
2387+
raw/report
2388+
SHA-256 are
2389+
`739ed4b89f9c96218e71fe4b6c14b68fe107bb00d7b4ec517dc6411e3afa9ed5`
2390+
and `7fcbc241d7416d2aaae25b4ca559a77238590bc792ff15d43e78dc08a6735b09`;
2391+
base raw/report SHA-256 are
2392+
`f39138977bab8708d557d150d02ddf9a030dd1da92a935a3cb64da9151396483`
2393+
and `0c622af88447819a17350cef152a2ee89288db23fa4d6ca29a6f9bb53b11d654`.
2394+
2395+
The corrected candidate's complete broad physical plan produced all 1,625
2396+
records, 225/225 eligible formal measurements, zero non-OK samples, and 600
2397+
passing linearity samples. Candidate/base was 1.001860x overall; family ratios
2398+
ranged from 0.973575x for string through 1.006763x for call. The intended
2399+
`string_slice` case measured 0.973575x base. Candidate/QuickJS-NG was 0.194900x
2400+
overall, but allocation still failed B4 at 1.135609x. The strict analyzer
2401+
correctly rejected the receipt-less dirty development binaries, so these are
2402+
regression diagnostics rather than a fixed-hardware claim. Broad raw SHA-256
2403+
is `c60e47562d42e47cc83c8c6e60bd07aa71be1406e7b603ebbc36c7236c582342`.
2404+
This unit generalizes across independent external string programs while
2405+
keeping the broad regression guard neutral; B4 and B5 remain active.
2406+
23442407
## Historical Broad V1 Baseline
23452408

23462409
The first complete baseline was recorded on 2026-07-15 at commit

0 commit comments

Comments
 (0)