Skip to content

Commit c13aaa5

Browse files
committed
Store typed array metadata in internal slots
1 parent d17cde1 commit c13aaa5

5 files changed

Lines changed: 143 additions & 108 deletions

File tree

crates/qjs-runtime/src/typed_array.rs

Lines changed: 47 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,32 @@ pub(crate) use ordering::*;
2929

3030
const MAX_TYPED_ARRAY_LENGTH: usize = 1_000_000;
3131

32-
/// Internal slot naming the concrete TypedArray kind (e.g. `"Uint8Array"`).
33-
pub(crate) const TYPED_ARRAY_KIND_PROPERTY: &str = "\0TypedArrayKind";
34-
/// Internal slot referencing the backing `ArrayBuffer` object.
35-
pub(crate) const TYPED_ARRAY_BUFFER_PROPERTY: &str = "\0TypedArrayBuffer";
36-
/// Internal slot holding the byte offset of the view into its buffer.
37-
pub(crate) const TYPED_ARRAY_BYTE_OFFSET_PROPERTY: &str = "\0TypedArrayByteOffset";
38-
/// Internal slot holding the element count of the view.
39-
pub(crate) const TYPED_ARRAY_LENGTH_PROPERTY: &str = "\0TypedArrayArrayLength";
40-
/// Internal slot marking views whose length tracks a resizable ArrayBuffer.
41-
pub(crate) const TYPED_ARRAY_LENGTH_TRACKING_PROPERTY: &str = "\0TypedArrayLengthTracking";
32+
#[derive(Clone)]
33+
pub(crate) struct TypedArraySlots {
34+
kind: NativeFunction,
35+
buffer: ObjectRef,
36+
byte_offset: usize,
37+
fixed_length: usize,
38+
length_tracking: bool,
39+
}
40+
41+
impl TypedArraySlots {
42+
pub(crate) fn new(
43+
kind: NativeFunction,
44+
buffer: ObjectRef,
45+
byte_offset: usize,
46+
fixed_length: usize,
47+
length_tracking: bool,
48+
) -> Self {
49+
Self {
50+
kind,
51+
buffer,
52+
byte_offset,
53+
fixed_length,
54+
length_tracking,
55+
}
56+
}
57+
}
4258

4359
/// Whether `object` carries the TypedArray brand.
4460
pub(crate) fn is_typed_array_object(object: &ObjectRef) -> bool {
@@ -378,10 +394,7 @@ pub(crate) fn native_typed_array_prototype_buffer(
378394
this_value: Value,
379395
) -> Result<Value, RuntimeError> {
380396
let object = typed_array_receiver(&this_value)?;
381-
match object.own_property(TYPED_ARRAY_BUFFER_PROPERTY) {
382-
Some(Property { value, .. }) => Ok(value),
383-
None => Ok(Value::Undefined),
384-
}
397+
Ok(typed_array_buffer(&object).map_or(Value::Undefined, Value::Object))
385398
}
386399

387400
/// `get %TypedArray.prototype%[Symbol.toStringTag]`: the kind name, or
@@ -390,15 +403,11 @@ pub(crate) fn native_typed_array_prototype_to_string_tag(
390403
this_value: Value,
391404
) -> Result<Value, RuntimeError> {
392405
match this_value {
393-
Value::Object(object) if is_typed_array_object(&object) => {
394-
match object.own_property(TYPED_ARRAY_KIND_PROPERTY) {
395-
Some(Property {
396-
value: Value::String(name),
397-
..
398-
}) => Ok(Value::String(name)),
399-
_ => Ok(Value::Undefined),
400-
}
401-
}
406+
Value::Object(object) if is_typed_array_object(&object) => Ok(Value::String(
407+
typed_array_name(typed_array_kind(&object))
408+
.to_owned()
409+
.into(),
410+
)),
402411
_ => Ok(Value::Undefined),
403412
}
404413
}
@@ -605,13 +614,9 @@ fn is_object_like(value: &Value) -> bool {
605614
// --- Internal-slot helpers ---------------------------------------------------
606615

607616
pub(crate) fn typed_array_kind(object: &ObjectRef) -> NativeFunction {
608-
match object.own_property(TYPED_ARRAY_KIND_PROPERTY) {
609-
Some(Property {
610-
value: Value::String(name),
611-
..
612-
}) => native_for_name(&name),
613-
_ => NativeFunction::Uint8Array,
614-
}
617+
object
618+
.typed_array_slots()
619+
.map_or(NativeFunction::Uint8Array, |slots| slots.kind)
615620
}
616621

617622
pub(crate) fn typed_array_length(object: &ObjectRef) -> usize {
@@ -628,13 +633,10 @@ pub(super) fn typed_array_length_for_buffer_byte_length(
628633
object: &ObjectRef,
629634
buffer_byte_length: usize,
630635
) -> usize {
631-
let fixed_length = match object.own_property(TYPED_ARRAY_LENGTH_PROPERTY) {
632-
Some(Property {
633-
value: Value::Number(length),
634-
..
635-
}) => length as usize,
636-
_ => return 0,
636+
let Some(slots) = object.typed_array_slots() else {
637+
return 0;
637638
};
639+
let fixed_length = slots.fixed_length;
638640
let offset = typed_array_byte_offset(object);
639641
let element = bytes_per_element(typed_array_kind(object));
640642
if typed_array_is_length_tracking(object) {
@@ -657,23 +659,13 @@ pub(super) fn typed_array_length_for_buffer_byte_length(
657659
}
658660

659661
fn typed_array_fixed_length(object: &ObjectRef) -> Option<usize> {
660-
match object.own_property(TYPED_ARRAY_LENGTH_PROPERTY) {
661-
Some(Property {
662-
value: Value::Number(length),
663-
..
664-
}) => Some(length as usize),
665-
_ => None,
666-
}
662+
object.typed_array_slots().map(|slots| slots.fixed_length)
667663
}
668664

669665
pub(crate) fn typed_array_is_length_tracking(object: &ObjectRef) -> bool {
670-
matches!(
671-
object.own_property(TYPED_ARRAY_LENGTH_TRACKING_PROPERTY),
672-
Some(Property {
673-
value: Value::Boolean(true),
674-
..
675-
})
676-
)
666+
object
667+
.typed_array_slots()
668+
.is_some_and(|slots| slots.length_tracking)
677669
}
678670

679671
pub(crate) fn typed_array_is_out_of_bounds(object: &ObjectRef) -> bool {
@@ -687,13 +679,7 @@ pub(crate) fn typed_array_is_out_of_bounds(object: &ObjectRef) -> bool {
687679
if typed_array_is_length_tracking(object) {
688680
typed_array_byte_offset(object) > buffer_byte_length
689681
} else {
690-
let fixed_length = match object.own_property(TYPED_ARRAY_LENGTH_PROPERTY) {
691-
Some(Property {
692-
value: Value::Number(length),
693-
..
694-
}) => length as usize,
695-
_ => 0,
696-
};
682+
let fixed_length = typed_array_fixed_length(object).unwrap_or(0);
697683
let element = bytes_per_element(typed_array_kind(object));
698684
let byte_length = fixed_length.checked_mul(element);
699685
byte_length.is_none_or(|byte_length| {
@@ -705,23 +691,13 @@ pub(crate) fn typed_array_is_out_of_bounds(object: &ObjectRef) -> bool {
705691
}
706692

707693
pub(crate) fn typed_array_byte_offset(object: &ObjectRef) -> usize {
708-
match object.own_property(TYPED_ARRAY_BYTE_OFFSET_PROPERTY) {
709-
Some(Property {
710-
value: Value::Number(offset),
711-
..
712-
}) => offset as usize,
713-
_ => 0,
714-
}
694+
object
695+
.typed_array_slots()
696+
.map_or(0, |slots| slots.byte_offset)
715697
}
716698

717699
pub(crate) fn typed_array_buffer(object: &ObjectRef) -> Option<ObjectRef> {
718-
match object.own_property(TYPED_ARRAY_BUFFER_PROPERTY) {
719-
Some(Property {
720-
value: Value::Object(buffer),
721-
..
722-
}) => Some(buffer),
723-
_ => None,
724-
}
700+
object.typed_array_slots().map(|slots| slots.buffer.clone())
725701
}
726702

727703
pub(crate) fn typed_array_buffer_detached(object: &ObjectRef) -> bool {
@@ -878,14 +854,6 @@ pub(crate) fn typed_array_name(native: NativeFunction) -> &'static str {
878854
}
879855
}
880856

881-
fn native_for_name(name: &str) -> NativeFunction {
882-
TYPED_ARRAY_KINDS
883-
.iter()
884-
.find(|(kind, _)| *kind == name)
885-
.map(|(_, native)| *native)
886-
.unwrap_or(NativeFunction::Uint8Array)
887-
}
888-
889857
#[cfg(test)]
890858
mod base64_tests;
891859
#[cfg(test)]

crates/qjs-runtime/src/typed_array/construct.rs

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ use crate::{
77

88
use super::element::write_element;
99
use super::{
10-
MAX_TYPED_ARRAY_LENGTH, TYPED_ARRAY_BUFFER_PROPERTY, TYPED_ARRAY_BYTE_OFFSET_PROPERTY,
11-
TYPED_ARRAY_KIND_PROPERTY, TYPED_ARRAY_LENGTH_PROPERTY, TYPED_ARRAY_LENGTH_TRACKING_PROPERTY,
12-
bytes_per_element, coerce_element, is_big_int_kind, is_typed_array_object,
13-
to_typed_array_length, typed_array_kind, typed_array_name,
10+
MAX_TYPED_ARRAY_LENGTH, TypedArraySlots, bytes_per_element, coerce_element, is_big_int_kind,
11+
is_typed_array_object, to_typed_array_length, typed_array_kind, typed_array_name,
1412
};
1513
use crate::CallEnv;
1614

@@ -340,28 +338,14 @@ fn install_view_slots(
340338
length_tracking: bool,
341339
) {
342340
let name = typed_array_name(native);
343-
object.mark_typed_array_exotic();
341+
object.install_typed_array_slots(TypedArraySlots::new(
342+
native,
343+
buffer,
344+
byte_offset,
345+
length,
346+
length_tracking,
347+
));
344348
object.set_to_string_tag(name);
345-
object.define_property(
346-
TYPED_ARRAY_KIND_PROPERTY.to_owned(),
347-
Property::non_enumerable(Value::String(name.to_owned().into())),
348-
);
349-
object.define_property(
350-
TYPED_ARRAY_BUFFER_PROPERTY.to_owned(),
351-
Property::non_enumerable(Value::Object(buffer.clone())),
352-
);
353-
object.define_property(
354-
TYPED_ARRAY_BYTE_OFFSET_PROPERTY.to_owned(),
355-
Property::non_enumerable(Value::Number(byte_offset as f64)),
356-
);
357-
object.define_property(
358-
TYPED_ARRAY_LENGTH_PROPERTY.to_owned(),
359-
Property::non_enumerable(Value::Number(length as f64)),
360-
);
361-
object.define_property(
362-
TYPED_ARRAY_LENGTH_TRACKING_PROPERTY.to_owned(),
363-
Property::non_enumerable(Value::Boolean(length_tracking)),
364-
);
365349
}
366350

367351
// --- static methods (%TypedArray%.from / %TypedArray%.of) --------------------

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@ fn concrete_constructor_requires_new() {
3939
fn typed_array_brand_is_not_a_string_keyed_property() {
4040
assert_eq!(
4141
eval(
42-
"let array = new Uint8Array(1); \
42+
"let array = new Int16Array([258]); \
4343
delete array['\\0TypedArrayKind']; \
44+
array['\\0TypedArrayBuffer'] = null; \
4445
let fake = { '\\0TypedArrayKind': 'Uint8Array' }; \
45-
ArrayBuffer.isView(array) + ':' + ArrayBuffer.isView(fake);"
46+
ArrayBuffer.isView(array) + ':' + ArrayBuffer.isView(fake) + ':' + \
47+
array[0] + ':' + array.BYTES_PER_ELEMENT;"
4648
),
47-
Ok(Value::String("true:false".to_owned().into()))
49+
Ok(Value::String("true:false:258:2".to_owned().into()))
4850
);
4951
}
5052

crates/qjs-runtime/src/value/object.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@ struct ObjectColdData {
239239
/// remains a hidden property; bytes live here so typed-array element access
240240
/// does not have to encode and decode a string on every read or write.
241241
internal_bytes: RefCell<Option<Vec<u8>>>,
242+
/// TypedArray internal slots. Keeping these out of string-keyed property
243+
/// storage avoids repeated hashing on every indexed access and prevents
244+
/// guessed property names from mutating spec-internal view metadata.
245+
typed_array_slots: OnceCell<crate::typed_array::TypedArraySlots>,
242246
/// Iterator.zip helper internal state. Ordinary objects hold `None`; zip
243247
/// helpers store their records here so advancement does not round-trip
244248
/// through observable-looking property storage.
@@ -844,10 +848,25 @@ impl ObjectRef {
844848
self.0.array_prototype_exotic.get()
845849
}
846850

847-
pub(crate) fn mark_typed_array_exotic(&self) {
851+
fn mark_typed_array_exotic(&self) {
848852
self.0.typed_array_exotic.set(true);
849853
}
850854

855+
pub(crate) fn install_typed_array_slots(&self, slots: crate::typed_array::TypedArraySlots) {
856+
self.mark_typed_array_exotic();
857+
let installed = self.0.cold().typed_array_slots.set(slots);
858+
debug_assert!(
859+
installed.is_ok(),
860+
"TypedArray internal slots must only be installed once"
861+
);
862+
}
863+
864+
pub(crate) fn typed_array_slots(&self) -> Option<&crate::typed_array::TypedArraySlots> {
865+
self.0
866+
.cold_if_present()
867+
.and_then(|cold| cold.typed_array_slots.get())
868+
}
869+
851870
pub(crate) fn is_typed_array_exotic(&self) -> bool {
852871
self.0.typed_array_exotic.get()
853872
}

tasks/T018-broad-performance.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,6 +2467,68 @@ SHA-256: `e71b97feb3314937dccf318007df67a4adc38bad11ad24b5f43548015d9f62b7`.
24672467
This unit materially improves multiple independent external sources while
24682468
keeping the internal regression guard neutral; B4 and B5 remain active.
24692469

2470+
The exact-SHA hosted workflows for
2471+
`d17cde1891f329debfeb023781ae13e3ae72318c` all completed successfully. CI
2472+
passed the comparison, Test262 subset, and full check jobs. Exact Test262
2473+
coverage remained 42,671 pass, one fail, zero timeout, and one actionable gap.
2474+
The hosted broad preview retained 25/25 cases, 225/225 eligible measurements,
2475+
and all 75 linearity pairs. Its 1.009458x candidate/base result was
2476+
inconclusive on the variable hosted runner; candidate/QuickJS-NG was 0.357441x.
2477+
The hosted external preview still failed B5 at 12.714481x QuickJS-NG for 5/5
2478+
JetStream cases, 7.670607x for 10/14 Kraken cases, and 11.436771x for 23/26
2479+
SunSpider cases. Hosted broad raw/report SHA-256 are
2480+
`5c38f33d55998c24c791a86d0f16b522932423d005217780aa0ba96d42244e40`
2481+
and `994c8f2464f94475266a4f46dfadc8b17dc88e9d98721105328c116879e4ea6a`;
2482+
hosted external raw/report SHA-256 are
2483+
`b8c64f9f8aa3c0b947a7462887168370c345597bc216b8068834d2fe989148a5`
2484+
and `f48754941ee1ee3f89514ea295fbbe3443a40c7610c814f9b02499a99db986f3`.
2485+
2486+
The forty-fifth v2 unit follows a fresh JetStream Gaussian Blur profile into
2487+
TypedArray representation. The five spec-internal view slots for kind, backing
2488+
buffer, byte offset, fixed length, and length tracking previously lived as
2489+
hidden string-keyed object properties. Each indexed access therefore repeated
2490+
property-map hashing and descriptor cloning before reading or writing bytes;
2491+
scripts that guessed the NUL-prefixed keys could also delete or overwrite the
2492+
metadata. Typed arrays now store one immutable slot record in the object's
2493+
existing lazily allocated cold data. Ordinary objects gain no hot-layout field,
2494+
while all TypedArray helpers read the record directly and continue to query the
2495+
backing buffer for live detached and resizable state. A focused test proves
2496+
that deleting or forging the old string keys cannot change a real Int16Array's
2497+
brand, element value, or element width. No workload name, source path,
2498+
iteration count, checksum, or expected benchmark value appears in the
2499+
implementation.
2500+
2501+
Against base binary SHA-256
2502+
`aebef9ebc2a8805c530888ca412c9a21febf94e55350874ce1fefab1918f7680`,
2503+
candidate binary SHA-256
2504+
`d2bda569d41560a1e00607163a0fe5f258d2c46257b8ae362231d0e02fa093f7`
2505+
cut JetStream `gaussian-blur` to 0.515644x base. Complete one-block candidate
2506+
and same-window base external previews retained identical coverage at 5/5
2507+
JetStream, 11/14 Kraken, and 23/26 SunSpider. Common-case candidate/base suite
2508+
geometric means were 0.878084x, 1.004602x, and 1.000667x; the non-target suite
2509+
movements were neutral, and the complete broad diagnostic below did not
2510+
reproduce the short `json-parse-financial` outlier. Candidate/QuickJS-NG still
2511+
failed B5 at 8.001698x, 4.671355x, and 6.202666x. Candidate external raw/report
2512+
SHA-256 are
2513+
`9c1153991587be3a76c1f06f22fe2a6c0084e892a7b42a2669d39fb5370c5f4e`
2514+
and `7f51336e473b1a37b56917449c25c607e74e7fc893e7e6ebd0b27532709d9194`;
2515+
base raw/report SHA-256 are
2516+
`7ccd6895ac6437fdec87a2711d5ea8be2cf6339a242123cc48fe616208bada1f`
2517+
and `c0a4d391b07a81935cc7bffab85ee20bc293d53b0fbce1d51d87498dde4b81ed`.
2518+
2519+
The complete three-role broad development diagnostic produced 225/225
2520+
eligible exact-checksum measurements, zero non-OK samples, and 600 linearity
2521+
samples with all 75 engine/case pairs passing. Candidate/base was 0.999558x
2522+
overall; family ratios ranged from 0.983997x for string through 1.003190x for
2523+
allocation, and the worst case was 1.008119x. Candidate/QuickJS-NG was
2524+
0.194814x overall, but allocation still failed B4 at 1.145679x. The strict
2525+
analyzer correctly rejected the receipt-less dirty development binaries, so
2526+
this is regression evidence rather than a fixed-hardware claim. Broad raw
2527+
SHA-256: `d42ede2ad98dd0866348e2c2b9a3007f35aeb276bd80cd6615dbb44816d477dd`.
2528+
This representation change materially improves an external typed-array
2529+
workload while preserving the non-target regression guard; B4 and B5 remain
2530+
active.
2531+
24702532
## Historical Broad V1 Baseline
24712533

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

0 commit comments

Comments
 (0)