use ::{
bevy_app::{App, Plugin, PostUpdate},
bevy_diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic},
bevy_ecs::system::Res,
bevy_reflect::PartialReflect,
};
use bevy_ecs::{resource::Resource, system::ResMut};
use bevy_mod_scripting_derive::DebugWithTypeInfo;
use bevy_mod_scripting_display::{
DebugWithTypeInfo, DebugWithTypeInfoBuilder, DisplayWithTypeInfo,
};
use bevy_platform::collections::HashMap;
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::{
cell::UnsafeCell,
cmp::Ordering,
fmt::{Display, Formatter},
hash::Hasher,
sync::{Arc, atomic::AtomicU64},
};
pub const ALLOCATOR_TOTAL_DIAG_PATH: DiagnosticPath =
DiagnosticPath::const_new("scripting_allocator_total");
pub const ALLOCATOR_TOTAL_COLLECTED_DIAG_PATH: DiagnosticPath =
DiagnosticPath::const_new("scripting_allocator_total_collected");
#[derive(Clone, DebugWithTypeInfo)]
#[debug_with_type_info(bms_display_path = "bevy_mod_scripting_display")]
pub struct ReflectAllocationId(pub(crate) Arc<u64>);
impl DisplayWithTypeInfo for ReflectAllocationId {
fn display_with_type_info(
&self,
f: &mut std::fmt::Formatter<'_>,
_type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>,
) -> std::fmt::Result {
write!(f, "{}", self.id())
}
}
impl ReflectAllocationId {
pub fn id(&self) -> u64 {
*self.0
}
pub(crate) fn new(id: u64) -> Self {
Self(Arc::new(id))
}
pub fn strong_count(&self) -> usize {
Arc::strong_count(&self.0)
}
}
impl std::hash::Hash for ReflectAllocationId {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id().hash(state);
}
}
impl PartialEq for ReflectAllocationId {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id()
}
}
impl Eq for ReflectAllocationId {}
impl PartialOrd for ReflectAllocationId {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ReflectAllocationId {
fn cmp(&self, other: &Self) -> Ordering {
self.id().cmp(&other.id())
}
}
#[derive(Debug)]
pub struct OwningPtr<T: ?Sized> {
ptr: *mut T,
}
impl<T: ?Sized> OwningPtr<T> {
pub unsafe fn new(ptr: *mut T) -> Self {
Self { ptr }
}
}
impl<T: ?Sized> Drop for OwningPtr<T> {
fn drop(&mut self) {
unsafe {
let _ = Box::from_raw(self.ptr);
}
}
}
pub struct ReflectAllocation(Box<UnsafeCell<dyn PartialReflect>>);
impl DebugWithTypeInfo for ReflectAllocation {
fn to_string_with_type_info(
&self,
f: &mut std::fmt::Formatter<'_>,
type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>,
) -> std::fmt::Result {
f.debug_tuple_with_type_info("ReflectAllocation", type_info_provider)
.field(&((self.0.get() as *mut ()) as usize))
.finish()
}
}
unsafe impl Sync for ReflectAllocation {}
impl ReflectAllocation {
pub fn get_ptr(&self) -> *mut dyn PartialReflect {
self.0.as_ref().get()
}
pub fn new(value: Box<dyn PartialReflect>) -> Self {
let value: Box<UnsafeCell<dyn PartialReflect>> = unsafe { std::mem::transmute(value) };
Self(value)
}
pub unsafe fn take(self) -> Box<dyn PartialReflect> {
unsafe { std::mem::transmute(self.0) }
}
}
impl Display for ReflectAllocationId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Resource, Clone, DebugWithTypeInfo)]
#[debug_with_type_info(bms_display_path = "bevy_mod_scripting_display")]
pub struct AppReflectAllocator {
pub(crate) allocator: Arc<RwLock<ReflectAllocator>>,
}
impl Default for AppReflectAllocator {
fn default() -> Self {
Self {
allocator: Arc::new(RwLock::new(ReflectAllocator::default())),
}
}
}
impl AppReflectAllocator {
pub fn read(&self) -> RwLockReadGuard<'_, ReflectAllocator> {
self.allocator.read()
}
pub fn write(&self) -> RwLockWriteGuard<'_, ReflectAllocator> {
self.allocator.write()
}
}
#[derive(Default, DebugWithTypeInfo)]
#[debug_with_type_info(bms_display_path = "bevy_mod_scripting_display")]
pub struct ReflectAllocator {
allocations: HashMap<ReflectAllocationId, ReflectAllocation>,
}
#[profiling::all_functions]
impl ReflectAllocator {
pub fn allocate<T: PartialReflect>(&mut self, value: T) -> ReflectAllocationId {
self.allocate_boxed(Box::new(value))
}
pub fn allocate_boxed(&mut self, value: Box<dyn PartialReflect>) -> ReflectAllocationId {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id =
ReflectAllocationId::new(COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed));
let value = ReflectAllocation::new(value);
self.allocations.insert(id.clone(), value);
id
}
pub fn insert(
&mut self,
id: ReflectAllocationId,
value: ReflectAllocation,
) -> Option<ReflectAllocation> {
self.allocations.insert(id, value)
}
pub fn remove(&mut self, id: &ReflectAllocationId) -> Option<ReflectAllocation> {
self.allocations.remove(id)
}
pub fn get_mut(&mut self, id: &ReflectAllocationId) -> Option<&mut ReflectAllocation> {
self.allocations.get_mut(id)
}
pub fn get(&self, id: &ReflectAllocationId) -> Option<&ReflectAllocation> {
self.allocations.get(id)
}
pub fn deallocate(&mut self, id: &ReflectAllocationId) {
self.allocations.remove(id);
}
pub fn clean_garbage_allocations(&mut self) {
self.allocations.retain(|k, _| Arc::strong_count(&k.0) > 1);
}
pub fn iter_allocations(
&self,
) -> impl Iterator<Item = (&ReflectAllocationId, &ReflectAllocation)> {
self.allocations.iter()
}
}
#[profiling::function]
pub fn garbage_collector(allocator: ResMut<AppReflectAllocator>, mut diagnostics: Diagnostics) {
let mut allocator = allocator.write();
let before = allocator.allocations.len();
allocator.clean_garbage_allocations();
let after = allocator.allocations.len();
diagnostics.add_measurement(&ALLOCATOR_TOTAL_DIAG_PATH, || after as f64);
diagnostics.add_measurement(&ALLOCATOR_TOTAL_COLLECTED_DIAG_PATH, || {
(before - after) as f64
});
}
pub fn measure_allocations(allocator: Res<AppReflectAllocator>, mut diagnostics: Diagnostics) {
let allocator = allocator.read();
let allocations_count = allocator.allocations.len();
diagnostics.add_measurement(&ALLOCATOR_TOTAL_DIAG_PATH, || allocations_count as f64);
}
pub struct AllocatorDiagnosticPlugin;
impl Plugin for AllocatorDiagnosticPlugin {
fn build(&self, app: &mut App) {
app.register_diagnostic(Diagnostic::new(ALLOCATOR_TOTAL_DIAG_PATH).with_suffix(" allocs"))
.register_diagnostic(
Diagnostic::new(ALLOCATOR_TOTAL_COLLECTED_DIAG_PATH).with_suffix(" deallocs"),
)
.add_systems(PostUpdate, measure_allocations);
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_reflect_allocator_garbage_clean() {
let mut allocator = ReflectAllocator::default();
let id = allocator.allocate(0);
assert_eq!(allocator.allocations.len(), 1);
drop(id);
allocator.clean_garbage_allocations();
assert_eq!(allocator.allocations.len(), 0);
}
#[test]
fn test_reflect_allocator_allocate_clean_and_access_does_not_overwrite_id() {
let mut allocator = ReflectAllocator::default();
let id = allocator.allocate(0);
let id2 = allocator.allocate("string");
assert_eq!(allocator.allocations.len(), 2);
drop(id);
allocator.clean_garbage_allocations();
assert_eq!(allocator.allocations.len(), 1);
allocator.allocate(3);
assert_eq!(allocator.allocations.len(), 2);
let ref_ = unsafe { &*allocator.get(&id2).unwrap().get_ptr() };
assert!(ref_.reflect_partial_eq(&"string").unwrap());
}
#[test]
fn test_reflect_allocator_garbage_clean_no_garbage() {
let mut allocator = ReflectAllocator::default();
let _id = allocator.allocate(0);
assert_eq!(allocator.allocations.len(), 1);
allocator.clean_garbage_allocations();
assert_eq!(allocator.allocations.len(), 1);
}
}