avm2: Implement ScopeChain caching

This commit is contained in:
EmperorBale 2023-01-31 17:47:37 -08:00 committed by Bale
parent 37ec94f95b
commit e7612f571d
3 changed files with 128 additions and 28 deletions

View File

@ -134,6 +134,25 @@ impl<'gc, V> PropertyMap<'gc, V> {
}
}
pub fn insert_with_namespace(
&mut self,
ns: Namespace<'gc>,
name: AvmString<'gc>,
mut value: V,
) -> Option<V> {
let bucket = self.0.entry(name).or_default();
if let Some((_, old_value)) = bucket.iter_mut().find(|(n, _)| *n == ns) {
swap(old_value, &mut value);
Some(value)
} else {
bucket.push((ns, value));
None
}
}
#[allow(dead_code)]
pub fn remove(&mut self, name: QName<'gc>) -> Option<V> {
let bucket = self.0.get_mut(&name.local_name());

View File

@ -5,10 +5,11 @@ use crate::avm2::domain::Domain;
use crate::avm2::object::{Object, TObject};
use crate::avm2::value::Value;
use crate::avm2::Error;
use crate::avm2::Multiname;
use crate::avm2::{Multiname, Namespace};
use core::fmt;
use gc_arena::{Collect, Gc, MutationContext};
use std::ops::Deref;
use gc_arena::{Collect, GcCell, MutationContext};
use super::property_map::PropertyMap;
/// Represents a Scope that can be on either a ScopeChain or local ScopeStack.
#[derive(Collect, Clone, Copy, Debug)]
@ -47,6 +48,33 @@ impl<'gc> Scope<'gc> {
}
}
/// Internal container that a ScopeChain uses
#[derive(Collect, Clone, Debug)]
#[collect(no_drop)]
struct ScopeContainer<'gc> {
/// The scopes of this ScopeChain
scopes: Vec<Scope<'gc>>,
/// The cache of this ScopeChain. A value of None indicates that caching is disabled
/// for this ScopeChain.
cache: Option<PropertyMap<'gc, Object<'gc>>>,
}
impl<'gc> ScopeContainer<'gc> {
fn new(scopes: Vec<Scope<'gc>>) -> Self {
let cache = (!scopes.iter().any(|scope| scope.with)).then(PropertyMap::default);
Self { scopes, cache }
}
fn get(&self, index: usize) -> Option<Scope<'gc>> {
self.scopes.get(index).cloned()
}
fn is_empty(&self) -> bool {
self.scopes.is_empty()
}
}
/// A ScopeChain "chains" scopes together.
///
/// A ScopeChain is used for "remembering" what a scope looked like. A ScopeChain also
@ -63,14 +91,14 @@ impl<'gc> Scope<'gc> {
#[derive(Collect, Clone, Copy)]
#[collect(no_drop)]
pub struct ScopeChain<'gc> {
scopes: Option<Gc<'gc, Vec<Scope<'gc>>>>,
container: Option<GcCell<'gc, ScopeContainer<'gc>>>,
domain: Domain<'gc>,
}
impl fmt::Debug for ScopeChain<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ScopeChain")
.field("scopes", &self.scopes)
.field("container", &self.container)
.finish()
}
}
@ -79,7 +107,7 @@ impl<'gc> ScopeChain<'gc> {
/// Creates a brand new ScopeChain with a domain. The domain should be the current domain in use.
pub fn new(domain: Domain<'gc>) -> Self {
Self {
scopes: None,
container: None,
domain,
}
}
@ -91,14 +119,14 @@ impl<'gc> ScopeChain<'gc> {
return *self;
}
// TODO: This current implementation is a bit expensive, but it is exactly what avmplus does, so it's good enough for now.
match self.scopes {
Some(scopes) => {
match self.container {
Some(container) => {
// The new ScopeChain is created by cloning the scopes of this ScopeChain,
// and pushing the new scopes on top of that.
let mut cloned = scopes.deref().clone();
let mut cloned = container.read().scopes.clone();
cloned.extend_from_slice(new_scopes);
Self {
scopes: Some(Gc::allocate(mc, cloned)),
container: Some(GcCell::allocate(mc, ScopeContainer::new(cloned))),
domain: self.domain,
}
}
@ -106,7 +134,10 @@ impl<'gc> ScopeChain<'gc> {
// We are chaining on top of an empty ScopeChain, so we don't actually
// need to chain anything.
Self {
scopes: Some(Gc::allocate(mc, new_scopes.to_vec())),
container: Some(GcCell::allocate(
mc,
ScopeContainer::new(new_scopes.to_vec()),
)),
domain: self.domain,
}
}
@ -114,11 +145,14 @@ impl<'gc> ScopeChain<'gc> {
}
pub fn get(&self, index: usize) -> Option<Scope<'gc>> {
self.scopes.and_then(|scopes| scopes.get(index).cloned())
self.container
.and_then(|container| container.read().get(index))
}
pub fn is_empty(&self) -> bool {
self.scopes.map(|scopes| scopes.is_empty()).unwrap_or(true)
self.container
.map(|container| container.read().is_empty())
.unwrap_or(true)
}
/// Returns the domain associated with this ScopeChain.
@ -126,38 +160,77 @@ impl<'gc> ScopeChain<'gc> {
self.domain
}
#[allow(clippy::collapsible_if)]
pub fn find(
fn find_internal(
&self,
multiname: &Multiname<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<Option<Object<'gc>>, Error<'gc>> {
// First search our scopes
if let Some(scopes) = self.scopes {
for (depth, scope) in scopes.iter().enumerate().rev() {
let values = scope.values();
) -> Result<Option<(Option<Namespace<'gc>>, Object<'gc>)>, Error<'gc>> {
if let Some(container) = self.container {
for (depth, scope) in container.read().scopes.iter().enumerate().rev() {
// We search the dynamic properties if either conditions are met:
// 1. Scope is a `with` scope
// 2. We are at depth 0 (global scope)
//
// But no matter what, we always search traits first.
if values.has_trait(multiname) {
return Ok(Some(values));
} else if scope.with() || depth == 0 {
if values.has_own_property(multiname) {
return Ok(Some(values));
// NOTE: We are manually searching the vtable's traits so we can figure out which namespace the trait
// belongs to.
let values = scope.values();
if let Some(vtable) = values.vtable() {
if let Some((namespace, _)) = vtable.get_trait_with_ns(multiname) {
return Ok(Some((Some(namespace), values)));
}
}
// Wasn't in the objects traits, let's try dynamic properties if the conditions are right.
if (scope.with() || depth == 0) && values.has_own_property(multiname) {
// NOTE: We return the QName as `None` to indicate that we should never cache this result.
// We NEVER cache the result of dynamic properties.
return Ok(Some((None, values)));
}
}
}
// That didn't work... let's try searching the domain now.
if let Some((_qname, mut script)) = self.domain.get_defining_script(multiname)? {
return Ok(Some(script.globals(&mut activation.context)?));
if let Some((qname, mut script)) = self.domain.get_defining_script(multiname)? {
return Ok(Some((
Some(qname.namespace()),
script.globals(&mut activation.context)?,
)));
}
Ok(None)
}
pub fn find(
&self,
multiname: &Multiname<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<Option<Object<'gc>>, Error<'gc>> {
// First we check the cache of our container
if let Some(container) = self.container {
if let Some(cache) = &container.read().cache {
let cached = cache.get_for_multiname(multiname);
if cached.is_some() {
return Ok(cached.cloned());
}
}
}
let found = self.find_internal(multiname, activation)?;
if let (Some((Some(ns), obj)), Some(container)) = (found, self.container) {
// We found a value that hasn't been cached yet, so let's try to cache it now
let mut write = container.write(activation.context.gc_context);
if let Some(ref mut cache) = write.cache {
cache.insert_with_namespace(
ns,
multiname
.local_name()
.expect("Resolvable multinames should always have a local name"),
obj,
);
}
}
Ok(found.map(|o| o.1))
}
pub fn resolve(
&self,
name: &Multiname<'gc>,

View File

@ -105,6 +105,14 @@ impl<'gc> VTable<'gc> {
.cloned()
}
pub fn get_trait_with_ns(self, name: &Multiname<'gc>) -> Option<(Namespace<'gc>, Property)> {
self.0
.read()
.resolved_traits
.get_with_ns_for_multiname(name)
.map(|(ns, p)| (ns, *p))
}
/// Coerces `value` to the type of the slot with id `slot_id`
pub fn coerce_trait_value(
&self,