diff --git a/_release-content/release-notes/inspection.md b/_release-content/release-notes/inspection.md index 7b1dbef1ca568..fcc35cd37e299 100644 --- a/_release-content/release-notes/inspection.md +++ b/_release-content/release-notes/inspection.md @@ -1,7 +1,7 @@ --- title: Entity inspection tools authors: ["@jbuehler23", "@alice-i-cecile"] -pull_requests: [25818, 25822, 25823, 25824, 25826] +pull_requests: [25818, 25822, 25823, 25824, 25826, 25845] --- `bevy_dev_tools::inspection` is gaining a backend for inspecting worlds, entities, components, and resources. @@ -13,3 +13,4 @@ This note will be completed once the rest of the series lands. - Added `WorldSummary` to `bevy_dev_tools` inspection (#25818) - Added component inspection to `bevy_dev_tools` (#25823) - Added `stepping.*` methods to the Bevy Remote Protocol (#25826) +- Added entity and resource inspection to `bevy_dev_tools` (#25845) diff --git a/crates/bevy_dev_tools/src/inspection/entity_inspection.rs b/crates/bevy_dev_tools/src/inspection/entity_inspection.rs new file mode 100644 index 0000000000000..7f551065644f9 --- /dev/null +++ b/crates/bevy_dev_tools/src/inspection/entity_inspection.rs @@ -0,0 +1,216 @@ +//! Types describing entities as a whole, gathered by inspecting a [`World`](bevy_ecs::world::World). +//! +//! The per-component counterpart lives in [`component_inspection`](crate::inspection::component_inspection). + +use bevy_ecs::{entity::Entity, query::SpawnDetails}; +use bevy_utils::memory_size::MemorySize; +use core::fmt::{Display, Formatter}; + +use crate::inspection::{ + component_inspection::{ComponentInspection, ComponentInspectionSettings}, + label_resolution::EntityLabel, +}; + +/// The result of inspecting an entity, summarized by its [`Display`] implementation. +#[derive(Clone, Debug)] +pub struct EntityInspection { + /// The entity being inspected. + pub entity: Entity, + /// The label of the entity, if one could be resolved. + pub label: Option, + /// The sum of the shallow sizes of the entity's components, excluding heap allocations. + /// + /// [`None`] if [`include_components`](EntityInspectionSettings::include_components) is false. + pub total_memory_size: Option, + /// The components on the entity, in inspection form. + pub components: Option>, + /// Information about how and when this entity was spawned. + pub spawn_details: Option, +} + +impl Display for EntityInspection { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let label = match &self.label { + Some(label) => label.as_str(), + None => "Entity", + }; + write!(f, "{label} ({})", self.entity)?; + + if let Some(total_memory_size) = &self.total_memory_size { + write!(f, "\nMemory Size: {total_memory_size}")?; + } + + if let Some(spawn_details) = self.spawn_details { + write!(f, "\nSpawned on tick {}", spawn_details.spawn_tick().get())?; + + if let Some(location) = spawn_details.spawned_by().into_option() { + write!(f, " by {location}")?; + } + } + + if let Some(components) = &self.components { + write!(f, "\nComponents:")?; + for component in components { + write!(f, "\n- {component}")?; + } + } + + Ok(()) + } +} + +/// An error that can occur when attempting to inspect an entity. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EntityInspectionError { + /// The entity does not exist in the world. + EntityNotFound(Entity), +} + +impl Display for EntityInspectionError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + EntityInspectionError::EntityNotFound(entity) => { + write!(f, "Entity {entity} not found in world") + } + } + } +} + +impl core::error::Error for EntityInspectionError {} + +/// Settings for inspecting an individual entity. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct EntityInspectionSettings { + /// Whether component information should be included in the inspection. Component-based label + /// resolution is unavailable when it is not. + pub include_components: bool, + /// Settings used when inspecting the components on the entity. + pub component_settings: ComponentInspectionSettings, +} + +impl Default for EntityInspectionSettings { + fn default() -> Self { + Self { + include_components: true, + component_settings: ComponentInspectionSettings::default(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::inspection::{ + component_inspection::ComponentMetadataMap, + extension_methods::WorldInspectionExtensionTrait, + }; + use bevy_ecs::{component::Component, name::Name, reflect::AppTypeRegistry, world::World}; + use bevy_reflect::Reflect; + + #[derive(Component, Reflect, Debug)] + struct Health(u32); + + fn test_world() -> World { + let mut world = World::new(); + world.init_resource::(); + world + .resource_mut::() + .write() + .register::(); + world + } + + #[test] + fn inspect_reports_label_components_and_size() { + let mut world = test_world(); + let entity = world.spawn((Name::new("Player"), Health(7))).id(); + + let inspection = world + .inspect(entity, EntityInspectionSettings::default()) + .unwrap(); + + assert_eq!(inspection.entity, entity); + assert_eq!(inspection.label.unwrap().as_str(), "Player"); + + let components = inspection.components.unwrap(); + assert_eq!(components.len(), 2); + + let expected_size: u64 = components + .iter() + .map(|component| component.memory_size.0) + .sum(); + assert_eq!( + inspection.total_memory_size, + Some(MemorySize(expected_size)) + ); + } + + #[test] + fn inspect_cached_matches_inspect() { + let mut world = test_world(); + let entity = world.spawn((Name::new("Player"), Health(7))).id(); + let metadata_map = ComponentMetadataMap::generate(&world); + + let settings = EntityInspectionSettings::default(); + let uncached = world.inspect(entity, settings).unwrap(); + let cached = world + .inspect_cached(entity, &settings, &metadata_map) + .unwrap(); + + assert_eq!(cached.label, uncached.label); + assert_eq!(cached.total_memory_size, uncached.total_memory_size); + assert_eq!( + cached.components.unwrap().len(), + uncached.components.unwrap().len() + ); + } + + #[test] + fn despawned_entity_returns_error() { + let mut world = test_world(); + let entity = world.spawn(Health(7)).id(); + world.despawn(entity); + + let result = world.inspect(entity, EntityInspectionSettings::default()); + + assert_eq!( + result.unwrap_err(), + EntityInspectionError::EntityNotFound(entity) + ); + } + + #[test] + fn excluding_components_omits_them() { + let mut world = test_world(); + let entity = world.spawn((Name::new("Player"), Health(7))).id(); + + let inspection = world + .inspect( + entity, + EntityInspectionSettings { + include_components: false, + ..Default::default() + }, + ) + .unwrap(); + + assert!(inspection.components.is_none()); + assert!(inspection.total_memory_size.is_none()); + assert_eq!(inspection.label.unwrap().as_str(), "Player"); + } + + #[test] + fn display_contains_label_and_components() { + let mut world = test_world(); + let entity = world.spawn((Name::new("Player"), Health(7))).id(); + + let displayed = world + .inspect(entity, EntityInspectionSettings::default()) + .unwrap() + .to_string(); + + assert!(displayed.contains("Player"), "{displayed}"); + assert!(displayed.contains("Health"), "{displayed}"); + } +} diff --git a/crates/bevy_dev_tools/src/inspection/extension_methods.rs b/crates/bevy_dev_tools/src/inspection/extension_methods.rs index 7446caa8e7705..720543e92dacb 100644 --- a/crates/bevy_dev_tools/src/inspection/extension_methods.rs +++ b/crates/bevy_dev_tools/src/inspection/extension_methods.rs @@ -1,19 +1,49 @@ //! Inspection methods that extend Bevy's own types. -use bevy_ecs::{component::Component, component::ComponentId, entity::Entity, world::World}; +use bevy_ecs::{ + component::{Component, ComponentId}, + entity::Entity, + query::SpawnDetails, + reflect::AppTypeRegistry, + resource::{IsResource, Resource}, + system::{Commands, EntityCommands}, + world::{EntityWorldMut, World}, +}; +use bevy_log::{info, warn}; use bevy_utils::memory_size::MemorySize; -use core::any::type_name; +use core::any::{type_name, TypeId}; use crate::inspection::{ component_inspection::{ ComponentDetailLevel, ComponentInspection, ComponentInspectionError, - ComponentInspectionSettings, ComponentTypeInspection, ComponentTypeMetadata, + ComponentInspectionSettings, ComponentMetadataMap, ComponentTypeInspection, + ComponentTypeMetadata, }, + entity_inspection::{EntityInspection, EntityInspectionError, EntityInspectionSettings}, + label_resolution::{resolve_label, ComponentLabelData, EntityLabel}, reflection_tools::{clone_incomplete, component_value_to_string}, + resource_inspection::{ + ResourceInspection, ResourceInspectionError, ResourceInspectionSettings, + }, }; /// Inspection methods for [`World`], provided as an extension trait. pub trait WorldInspectionExtensionTrait { + /// Inspects the given entity, computing component type metadata on the fly. + fn inspect( + &self, + entity: Entity, + settings: EntityInspectionSettings, + ) -> Result; + + /// Inspects the given entity, reusing the component type metadata in `metadata_map`. + fn inspect_cached( + &self, + entity: Entity, + settings: &EntityInspectionSettings, + metadata_map: &ComponentMetadataMap, + ) -> Result; + /// Inspects the component with the given [`ComponentId`] on the given entity, /// using the caller-provided [`ComponentTypeMetadata`]. fn inspect_component_by_id( @@ -43,9 +73,91 @@ pub trait WorldInspectionExtensionTrait { &self, component_id: ComponentId, ) -> Result; + + /// Inspects the resource of type `R`. + fn inspect_resource( + &self, + settings: ResourceInspectionSettings, + ) -> Result; + + /// Inspects the resource with the given [`ComponentId`], + /// the dynamically-typed variant of [`inspect_resource`](Self::inspect_resource). + fn inspect_resource_by_id( + &self, + component_id: ComponentId, + settings: ResourceInspectionSettings, + ) -> Result; + + /// Inspects every resource currently present in the world. + fn inspect_all_resources( + &self, + settings: ResourceInspectionSettings, + ) -> Vec; } impl WorldInspectionExtensionTrait for World { + fn inspect( + &self, + entity: Entity, + settings: EntityInspectionSettings, + ) -> Result { + let metadata_map = ComponentMetadataMap::for_entity(self, entity); + + self.inspect_cached(entity, &settings, &metadata_map) + } + + fn inspect_cached( + &self, + entity: Entity, + settings: &EntityInspectionSettings, + metadata_map: &ComponentMetadataMap, + ) -> Result { + let entity_ref = self + .get_entity(entity) + .map_err(|_| EntityInspectionError::EntityNotFound(entity))?; + + let spawn_details = self + .try_query::() + .and_then(|mut query| query.get(self, entity).ok()); + + let (components, total_memory_size) = if settings.include_components { + let components: Vec = entity_ref + .archetype() + .components() + .iter() + .filter_map(|component_id| { + let metadata = metadata_map.get(component_id)?; + + self.inspect_component_by_id( + *component_id, + entity, + metadata, + settings.component_settings, + ) + .ok() + }) + .collect(); + + let total_bytes = components + .iter() + .fold(0u64, |acc, component| acc + component.memory_size.0); + + (Some(components), Some(MemorySize(total_bytes))) + } else { + (None, None) + }; + + let label = resolve_entity_label(self, entity, components.as_deref(), metadata_map); + + Ok(EntityInspection { + entity, + label, + total_memory_size, + components, + spawn_details, + }) + } + fn inspect_component_by_id( &self, component_id: ComponentId, @@ -139,4 +251,181 @@ impl WorldInspectionExtensionTrait for World { metadata, }) } + + fn inspect_resource( + &self, + settings: ResourceInspectionSettings, + ) -> Result { + let component_id = self.components().component_id::().ok_or( + ResourceInspectionError::ResourceNotRegistered(type_name::()), + )?; + + self.inspect_resource_by_id(component_id, settings) + } + + fn inspect_resource_by_id( + &self, + component_id: ComponentId, + settings: ResourceInspectionSettings, + ) -> Result { + let component_info = self.components().get_info(component_id).ok_or( + ResourceInspectionError::ResourceIdNotRegistered(component_id), + )?; + let memory_size = MemorySize::new(component_info.layout().size()); + let name = component_info.name(); + let type_id = component_info.type_id(); + + let type_registration = type_id.and_then(|type_id| { + self.get_resource::() + .and_then(|type_registry| type_registry.read().get(type_id).cloned()) + }); + + let value = match self.resource_entities().get(component_id) { + Some(resource_entity) => { + component_value_to_string(self, resource_entity, type_id, settings.full_type_names) + } + None => "".to_string(), + }; + + Ok(ResourceInspection { + component_id, + name, + value, + type_id, + memory_size, + type_registration, + }) + } + + fn inspect_all_resources( + &self, + settings: ResourceInspectionSettings, + ) -> Vec { + self.resource_entities() + .iter() + .filter_map(|(component_id, _entity)| { + self.inspect_resource_by_id(component_id, settings).ok() + }) + .collect() + } +} + +/// Determines the label of an inspected entity from its inspected components. Entities that back +/// a resource take the short name of that resource. +fn resolve_entity_label( + world: &World, + entity: Entity, + components: Option<&[ComponentInspection]>, + metadata_map: &ComponentMetadataMap, +) -> Option { + let Some(components) = components else { + return resolve_label(world, entity, &[]); + }; + + let is_resource = components.iter().any(|component| { + metadata_map + .get(&component.component_id) + .and_then(|metadata| metadata.type_id) + == Some(TypeId::of::()) + }); + + if is_resource { + return components + .iter() + .find(|component| { + world + .resource_entities() + .get(component.component_id) + .is_some() + }) + .map(|component| EntityLabel::resolved(&component.name.shortname().to_string())); + } + + let short_names: Vec = components + .iter() + .map(|component| component.name.shortname().to_string()) + .collect(); + + let label_data: Vec = components + .iter() + .zip(short_names.iter()) + .map(|(component, short_name)| ComponentLabelData { + component_id: component.component_id, + short_name: short_name.as_str(), + label_definition_priority: metadata_map + .get(&component.component_id) + .and_then(|metadata| metadata.label_definition_priority), + }) + .collect(); + + resolve_label(world, entity, &label_data) +} + +/// Inspection methods for [`EntityCommands`], provided as an extension trait. +pub trait EntityCommandsInspectionExtensionTrait { + /// Inspects this entity, logging the result at the info level. + fn inspect(&mut self, settings: EntityInspectionSettings); + + /// Inspects the component of type `C` on this entity, logging the result at the info level. + fn inspect_component(&mut self, settings: ComponentInspectionSettings); +} + +impl EntityCommandsInspectionExtensionTrait for EntityCommands<'_> { + fn inspect(&mut self, settings: EntityInspectionSettings) { + let entity = self.id(); + + self.queue(move |entity_world_mut: EntityWorldMut| { + let world = entity_world_mut.world(); + match world.inspect(entity, settings) { + Ok(inspection) => info!("{inspection}"), + Err(error) => warn!("Failed to inspect entity: {error}"), + } + }); + } + + fn inspect_component(&mut self, settings: ComponentInspectionSettings) { + let entity = self.id(); + + self.queue(move |entity_world_mut: EntityWorldMut| { + let world = entity_world_mut.world(); + match world.inspect_component::(entity, settings) { + Ok(inspection) => info!("{inspection}"), + Err(error) => warn!("Failed to inspect component: {error}"), + } + }); + } +} + +/// Inspection methods for [`Commands`], provided as an extension trait. +pub trait CommandsInspectionExtensionTrait { + /// Inspects the resource of type `R`, logging the result at the info level. + fn inspect_resource(&mut self, settings: ResourceInspectionSettings); + + /// Inspects every resource in the world, logging the results at the info level. + fn inspect_all_resources(&mut self, settings: ResourceInspectionSettings); +} + +impl CommandsInspectionExtensionTrait for Commands<'_, '_> { + fn inspect_resource(&mut self, settings: ResourceInspectionSettings) { + self.queue( + move |world: &mut World| match world.inspect_resource::(settings) { + Ok(inspection) => info!("{inspection}"), + Err(error) => warn!("Failed to inspect resource: {error}"), + }, + ); + } + + fn inspect_all_resources(&mut self, settings: ResourceInspectionSettings) { + self.queue(move |world: &mut World| { + let mut inspections = world.inspect_all_resources(settings); + inspections.sort_by_key(|inspection| inspection.name.shortname().to_string()); + + let mut log_string = format!("Inspecting all resources ({} found):", inspections.len()); + for inspection in &inspections { + log_string.push_str(&format!("\n- {inspection}")); + } + + info!("{log_string}"); + }); + } } diff --git a/crates/bevy_dev_tools/src/inspection/mod.rs b/crates/bevy_dev_tools/src/inspection/mod.rs index a379c8716d8df..ab2cde3e08bbb 100644 --- a/crates/bevy_dev_tools/src/inspection/mod.rs +++ b/crates/bevy_dev_tools/src/inspection/mod.rs @@ -5,7 +5,9 @@ //! and presenting that data to the user in a number of convenient, often interactive ways. pub mod component_inspection; +pub mod entity_inspection; pub mod extension_methods; pub mod label_resolution; pub mod reflection_tools; +pub mod resource_inspection; pub mod world_summary; diff --git a/crates/bevy_dev_tools/src/inspection/resource_inspection.rs b/crates/bevy_dev_tools/src/inspection/resource_inspection.rs new file mode 100644 index 0000000000000..cef4000d5c5cf --- /dev/null +++ b/crates/bevy_dev_tools/src/inspection/resource_inspection.rs @@ -0,0 +1,169 @@ +//! Types describing resources, gathered by inspecting a [`World`](bevy_ecs::world::World). + +use bevy_ecs::component::ComponentId; +use bevy_reflect::TypeRegistration; +use bevy_utils::{memory_size::MemorySize, prelude::DebugName}; +use core::{ + any::TypeId, + fmt::{Display, Formatter}, +}; + +/// The result of inspecting a resource, summarized by its [`Display`] implementation. +#[derive(Clone, Debug)] +pub struct ResourceInspection { + /// The [`ComponentId`] of the resource. + pub component_id: ComponentId, + /// The type name of the resource. + pub name: DebugName, + /// The value of the resource as a string, gathered via reflection. + pub value: String, + /// The [`TypeId`] of the resource, or `None` for dynamic types. + pub type_id: Option, + /// The shallow size of the resource in memory, excluding heap allocations. + pub memory_size: MemorySize, + /// The registered type information of the resource, if it is reflected and registered. + pub type_registration: Option, +} + +impl Display for ResourceInspection { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{} ({}): {}", + self.name.shortname(), + self.memory_size, + self.value + ) + } +} + +/// An error that can occur when attempting to inspect a resource. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ResourceInspectionError { + /// The resource type was not registered in the world. + ResourceNotRegistered(&'static str), + /// The resource ID provided was not registered in the world. + ResourceIdNotRegistered(ComponentId), +} + +impl Display for ResourceInspectionError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + ResourceInspectionError::ResourceNotRegistered(name) => { + write!(f, "Resource type {name} not registered in world") + } + ResourceInspectionError::ResourceIdNotRegistered(component_id) => { + write!(f, "{component_id:?} not registered in world") + } + } + } +} + +impl core::error::Error for ResourceInspectionError {} + +/// Settings for inspecting a resource. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct ResourceInspectionSettings { + /// Whether type paths in the value string are kept in full. + /// When false, every `::` in the formatted value is collapsed, including inside string values. + pub full_type_names: bool, +} + +impl Default for ResourceInspectionSettings { + fn default() -> Self { + Self { + full_type_names: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::inspection::extension_methods::WorldInspectionExtensionTrait; + use bevy_ecs::{reflect::AppTypeRegistry, resource::Resource, world::World}; + use bevy_reflect::Reflect; + + #[derive(Resource, Reflect, Debug)] + struct Score(u32); + + #[derive(Resource, Debug)] + struct Unregistered; + + fn test_world() -> World { + let mut world = World::new(); + world.init_resource::(); + world + .resource_mut::() + .write() + .register::(); + world.insert_resource(Score(7)); + world + } + + #[test] + fn inspect_resource_reports_name_and_value() { + let world = test_world(); + + let inspection = world + .inspect_resource::(ResourceInspectionSettings::default()) + .unwrap(); + + assert_eq!(inspection.name.shortname().to_string(), "Score"); + assert!(inspection.value.contains('7'), "{}", inspection.value); + assert_eq!(inspection.type_id, Some(TypeId::of::())); + assert_eq!(inspection.memory_size, MemorySize::new(4)); + } + + #[test] + fn inspect_all_resources_includes_inserted_resource() { + let world = test_world(); + + let inspections = world.inspect_all_resources(ResourceInspectionSettings::default()); + + assert!( + inspections + .iter() + .any(|inspection| inspection.type_id == Some(TypeId::of::())), + "the inserted resource should be inspected" + ); + } + + #[test] + fn unregistered_resource_returns_error() { + let world = test_world(); + + let result = world.inspect_resource::(ResourceInspectionSettings::default()); + + assert!(matches!( + result.unwrap_err(), + ResourceInspectionError::ResourceNotRegistered(_) + )); + } + + #[test] + fn display_contains_name_and_value() { + let world = test_world(); + + let displayed = world + .inspect_resource::(ResourceInspectionSettings::default()) + .unwrap() + .to_string(); + + assert!(displayed.contains("Score"), "{displayed}"); + assert!(displayed.contains('7'), "{displayed}"); + } + + #[test] + fn missing_type_registry_is_not_required() { + let mut world = World::new(); + world.insert_resource(Score(7)); + + let inspection = world + .inspect_resource::(ResourceInspectionSettings::default()) + .unwrap(); + + assert!(inspection.type_registration.is_none()); + } +}