Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod entries;
mod install;
mod lazy_subobjects;
mod marks_measures;
mod memory;
mod resource_buffer;
mod window_state;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ struct PerformancePrototypeMethodsDeclaration {
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "Performance", enumerable)]
struct PerformancePrototypeAccessorsDeclaration {
#[webapi(accessor_property, getter = super::memory::performance_memory_getter)]
memory: (),

#[webapi(
accessor_property,
getter = performance_attribute_getter_callback,
Expand Down
165 changes: 165 additions & 0 deletions moli-renderer-v8/src/context_bootstrap/performance_runtime/memory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use super::PERFORMANCE_TIME_ORIGIN_SLOT;
use crate::util::{
callback_data_index_value, callback_data_item, get_private_value, set_private_value,
throw_type_error,
};
use moli_webapi_declare::WebApiObject;
use std::time::{Duration, Instant};

const MEMORY_PROTOTYPE_SLOT: &str = "__moliPerformanceMemoryPrototype";
const TOTAL_HEAP_SLOT: &str = "__moliMemoryInfoTotalHeap";
const USED_HEAP_SLOT: &str = "__moliMemoryInfoUsedHeap";
const HEAP_LIMIT_SLOT: &str = "__moliMemoryInfoHeapLimit";
const MEMORY_SLOTS: &[&str] = &[TOTAL_HEAP_SLOT, USED_HEAP_SLOT, HEAP_LIMIT_SLOT];
const SAMPLE_INTERVAL: Duration = Duration::from_secs(20 * 60);

#[derive(Default, WebApiObject)]
#[webapi(interface = "Object")]
struct MemoryInfoPrototypeDeclaration {
#[webapi(accessor_property, name = "totalJSHeapSize", enumerable, getter = memory_info_getter, data = callback_data_index_value(scope, 0))]
total_js_heap_size: (),

#[webapi(accessor_property, name = "usedJSHeapSize", enumerable, getter = memory_info_getter, data = callback_data_index_value(scope, 1))]
used_js_heap_size: (),

#[webapi(accessor_property, name = "jsHeapSizeLimit", enumerable, getter = memory_info_getter, data = callback_data_index_value(scope, 2))]
js_heap_size_limit: (),

#[webapi(to_string_tag, readonly, init = string("MemoryInfo"))]
tag: (),
}

#[derive(WebApiObject)]
#[webapi(interface = "Object")]
struct MemoryInfoObjectDeclaration<'scope> {
#[webapi(prototype)]
prototype: Option<v8::Local<'scope, v8::Object>>,

#[webapi(slot = TOTAL_HEAP_SLOT)]
total: f64,
#[webapi(slot = USED_HEAP_SLOT)]
used: f64,
#[webapi(slot = HEAP_LIMIT_SLOT)]
limit: f64,
}

#[derive(Clone, Copy)]
struct HeapSample {
taken_at: Instant,
sizes: [f64; 3],
}

pub(super) fn performance_memory_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
if get_private_value(scope, args.this(), PERFORMANCE_TIME_ORIGIN_SLOT).is_none() {
throw_type_error(scope, "Illegal invocation");
return;
}
let sizes = heap_sample(scope);
let Some(context) = args.this().get_creation_context(scope) else {
return;
};
let scope = &mut v8::ContextScope::new(scope, context);
let prototype = match get_private_value(scope, args.this(), MEMORY_PROTOTYPE_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
Some(prototype) => prototype,
None => {
// MemoryInfo is a legacy interface without a global constructor.
let Ok(prototype) = MemoryInfoPrototypeDeclaration::default().bind(scope) else {
return;
};
set_private_value(scope, args.this(), MEMORY_PROTOTYPE_SLOT, prototype.into());
prototype
}
};
if let Ok(snapshot) =
MemoryInfoObjectDeclaration::new(Some(prototype), sizes[0], sizes[1], sizes[2]).bind(scope)
{
rv.set(snapshot.into());
}
}

fn memory_info_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(slot) = callback_data_item(scope, &args, MEMORY_SLOTS, "MemoryInfo attribute") else {
return;
};
let Some(value) = get_private_value(scope, args.this(), slot) else {
throw_type_error(scope, "Illegal invocation");
return;
};
rv.set(value);
}

fn heap_sample(scope: &mut v8::PinScope<'_, '_>) -> [f64; 3] {
let now = Instant::now();
if let Some(sample) = scope.get_slot::<HeapSample>()
&& now.duration_since(sample.taken_at) < SAMPLE_INTERVAL
{
return sample.sizes;
}
let statistics = scope.get_heap_statistics();
let external = statistics.external_memory();
let sizes = [
statistics.total_physical_size().saturating_add(external),
statistics.used_heap_size().saturating_add(external),
statistics.heap_size_limit(),
]
.map(|size| quantize_heap_size(size) as f64);
scope.set_slot(HeapSample {
taken_at: now,
sizes,
});
sizes
}

// Chromium's legacy API exposes coarse, rate-limited V8 statistics. Use 100
// exponentially spaced buckets from 10 MB towards 4 GB, rounded to three
// significant digits, instead of exposing fine-grained allocation/GC timing.
fn quantize_heap_size(size: usize) -> usize {
let growth = (400.0_f32.ln() / 100.0).exp();
let mut boundary = 10_000_000.0_f32;
let mut decimal_threshold = 100_000_000_u64;
let mut precision = 100_000_u64;
let mut rounded = 0;
for _ in 0..100 {
rounded = (boundary as u64 / precision * precision) as usize;
if size <= rounded {
return rounded;
}
boundary *= growth;
if boundary >= decimal_threshold as f32 {
decimal_threshold *= 10;
precision *= 10;
}
}
rounded
}

#[cfg(test)]
mod tests {
use super::quantize_heap_size;

#[test]
fn memory_heap_buckets_are_coarse_monotonic_and_bounded() {
assert_eq!(quantize_heap_size(0), 10_000_000);
assert_eq!(quantize_heap_size(10_000_000), 10_000_000);
assert_eq!(quantize_heap_size(10_000_001), 10_600_000);
let mut previous = 0;
for size in (0..4_000_000_000_usize).step_by(1_000_000) {
let bucket = quantize_heap_size(size);
assert!(bucket >= previous);
assert_eq!(bucket % 100_000, 0);
previous = bucket;
}
assert_eq!(quantize_heap_size(usize::MAX), previous);
assert!((3_000_000_000..4_000_000_000).contains(&previous));
}
}
1 change: 1 addition & 0 deletions moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod media;
mod misc;
mod navigation;
mod performance;
mod performance_memory;
mod pointer_lock;
mod promise_rejection;
mod security_policy;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use super::*;

#[test]
fn performance_memory_exposes_readonly_branded_heap_snapshots_without_a_constructor() {
let mut vm = new_storage_test_vm("https://performance-memory.test/");
let result = vm
.eval(
r#"
(() => {
const descriptor = Object.getOwnPropertyDescriptor(Performance.prototype, 'memory');
const first = performance.memory;
const second = performance.memory;
const prototype = Object.getPrototypeOf(first);
const names = ['totalJSHeapSize', 'usedJSHeapSize', 'jsHeapSizeLimit'];
const rejects = callback => {
try { callback(); return false; } catch (error) { return error instanceof TypeError; }
};
const checks = [
descriptor.get.name === 'get memory', descriptor.get.length === 0,
descriptor.set === undefined, descriptor.enumerable, descriptor.configurable,
!Object.hasOwn(performance, 'memory'), !('MemoryInfo' in globalThis),
first !== second, Object.getPrototypeOf(second) === prototype,
Object.getPrototypeOf(prototype) === Object.prototype,
!Object.hasOwn(prototype, 'constructor'),
Object.prototype.toString.call(first) === '[object MemoryInfo]',
Object.getOwnPropertyNames(first).length === 0,
Object.getOwnPropertyNames(prototype).sort().join() === names.slice().sort().join(),
first.totalJSHeapSize >= first.usedJSHeapSize,
first.jsHeapSizeLimit >= first.usedJSHeapSize,
rejects(() => descriptor.get.call(Object.create(performance))),
rejects(() => descriptor.get.call({}))
];
for (const name of names) {
const property = Object.getOwnPropertyDescriptor(prototype, name);
const value = first[name];
checks.push(Number.isSafeInteger(value), value >= 10000000, value % 100000 === 0,
property.get.name === 'get ' + name, property.get.length === 0,
property.set === undefined, property.enumerable, property.configurable);
first[name] = 0;
checks.push(first[name] === value, second[name] === value,
rejects(() => { 'use strict'; first[name] = 0; }));
for (const fake of [{}, prototype, Object.create(first), performance]) {
checks.push(rejects(() => property.get.call(fake)));
}
}
return checks.every(Boolean);
})()
"#,
)
.expect("legacy memory snapshots should have branded readonly attributes");
assert_eq!(result, "true");
}

#[test]
fn performance_memory_uses_the_receiver_realm_and_accepts_genuine_foreign_snapshots() {
let mut vm = new_parsed_test_vm(
"https://performance-memory-realms.test/",
"<!doctype html><iframe></iframe>",
);
let result = vm.eval(r#"
(() => {
const child = document.querySelector('iframe').contentWindow;
const getter = Object.getOwnPropertyDescriptor(Performance.prototype, 'memory').get;
const childGetter = Object.getOwnPropertyDescriptor(child.Performance.prototype, 'memory').get;
const localPrototype = Object.getPrototypeOf(performance.memory);
const childPrototype = Object.getPrototypeOf(child.performance.memory);
const foreign = getter.call(child.performance);
return [
childPrototype !== localPrototype,
Object.getPrototypeOf(childPrototype) === child.Object.prototype,
Object.getPrototypeOf(foreign) === childPrototype,
Object.getPrototypeOf(childGetter.call(performance)) === localPrototype,
Object.getOwnPropertyDescriptor(localPrototype, 'usedJSHeapSize').get.call(foreign)
=== foreign.usedJSHeapSize,
!('MemoryInfo' in child)
].every(Boolean);
})()
"#).expect("memory snapshots should use their Performance object's realm");
assert_eq!(result, "true");
}
5 changes: 3 additions & 2 deletions moli-renderer-v8/src/worker/thread/tests/postmessage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1844,7 +1844,8 @@ async fn worker_performance_now_uses_readonly_monotonic_time_origin() {
readonly: descriptor && descriptor.writable === false,
unchanged: after === before,
numeric: typeof first === "number" && typeof second === "number",
monotonic: second >= first
monotonic: second >= first,
noLegacyMemory: !("memory" in performance) && !("MemoryInfo" in self)
});
close();
"#
Expand All @@ -1858,7 +1859,7 @@ async fn worker_performance_now_uses_readonly_monotonic_time_origin() {
.expect("channel closed");
assert_eq!(
expect_post_json(msg),
r#"{"readonly":true,"unchanged":true,"numeric":true,"monotonic":true}"#
r#"{"readonly":true,"unchanged":true,"numeric":true,"monotonic":true,"noLegacyMemory":true}"#
);
}

Expand Down
Loading