use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
use ahash::AHashMap;
use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use crate::server::brp_messages::{BrpRequest, QueryFilter};
use bevy_remote::builtin_methods::{
BRP_QUERY_METHOD, BRP_GET_COMPONENTS_METHOD, BRP_LIST_COMPONENTS_METHOD,
};
use crate::server::error::{Error, Result};
#[derive(Debug, Clone, Serialize)]
pub struct QueryPerformanceMetrics {
pub query_id: String,
pub query: String,
pub execution_time_ms: u64,
pub entities_processed: usize,
pub components_accessed: usize,
pub cache_hit: bool,
pub parallel_processing: bool,
pub memory_usage_bytes: usize,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub archetype_pattern: ArchetypeAccessPattern,
}
#[derive(Debug, Clone, Serialize)]
pub struct ArchetypeAccessPattern {
pub required_components: HashSet<String>,
pub excluded_components: HashSet<String>,
pub matching_archetypes: usize,
pub parallel_friendly: bool,
}
#[derive(Debug, Clone)]
pub struct CachedQueryState {
pub query_pattern: QueryPattern,
pub perf_characteristics: QueryPerformanceCharacteristics,
pub last_used: Instant,
pub usage_count: u64,
pub avg_execution_time_ms: f64,
}
#[derive(Debug, Clone)]
pub struct QueryPattern {
pub query_hash: u64,
pub original_request: BrpRequest,
pub access_strategy: ComponentAccessStrategy,
pub performance_impact: PerformanceImpact,
}
#[derive(Debug, Clone)]
pub enum ComponentAccessStrategy {
DirectArchetype {
components: Vec<String>,
parallel_threshold: usize,
},
FilteredIteration {
with_components: Vec<String>,
without_components: Vec<String>,
filter_early: bool,
},
MultiPass {
passes: Vec<QueryPass>,
parallel_passes: Vec<usize>,
},
}
#[derive(Debug, Clone)]
pub struct QueryPass {
pub components: Vec<String>,
pub filter: Option<QueryFilter>,
pub parallel_safe: bool,
pub estimated_entity_count: usize,
}
#[derive(Debug, Clone)]
pub struct QueryPerformanceCharacteristics {
pub parallel_friendly: bool,
pub estimated_entity_count: usize,
pub memory_overhead_per_entity: usize,
pub cpu_complexity: u8,
pub cacheable: bool,
}
#[derive(Debug, Clone, Serialize)]
pub enum PerformanceImpact {
Low,
Medium,
High,
Critical,
}
pub struct QueryStateCache {
cache: AHashMap<u64, CachedQueryState>,
metrics_history: Vec<QueryPerformanceMetrics>,
max_cache_size: usize,
max_metrics_history: usize,
cache_stats: CacheStatistics,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct CacheStatistics {
pub total_queries: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub evictions: u64,
pub avg_build_time_ms: f64,
pub avg_execution_time_ms: f64,
}
impl QueryStateCache {
pub fn new(max_cache_size: usize, max_metrics_history: usize) -> Self {
Self {
cache: AHashMap::with_capacity(max_cache_size),
metrics_history: Vec::with_capacity(max_metrics_history),
max_cache_size,
max_metrics_history,
cache_stats: CacheStatistics::default(),
}
}
pub async fn get_or_build_query_state(
&mut self,
request: &BrpRequest,
) -> Result<Arc<CachedQueryState>> {
let query_hash = self.hash_request(request);
if let Some(cached_state) = self.cache.get_mut(&query_hash) {
cached_state.last_used = Instant::now();
cached_state.usage_count += 1;
self.cache_stats.cache_hits += 1;
self.cache_stats.total_queries += 1;
debug!("Query state cache hit for hash: {}", query_hash);
return Ok(Arc::new(cached_state.clone()));
}
let build_start = Instant::now();
let query_state = self.build_query_state(request).await?;
let build_time = build_start.elapsed();
self.cache_stats.cache_misses += 1;
self.cache_stats.total_queries += 1;
self.cache_stats.avg_build_time_ms =
(self.cache_stats.avg_build_time_ms * (self.cache_stats.cache_misses - 1) as f64 +
build_time.as_millis() as f64) / self.cache_stats.cache_misses as f64;
self.cache.insert(query_hash, query_state.clone());
self.evict_if_necessary();
debug!(
"Built and cached new query state in {}ms, hash: {}",
build_time.as_millis(),
query_hash
);
Ok(Arc::new(query_state))
}
async fn build_query_state(&self, request: &BrpRequest) -> Result<CachedQueryState> {
let query_pattern = self.analyze_query_pattern(request).await?;
let perf_characteristics = self.analyze_performance_characteristics(&query_pattern);
Ok(CachedQueryState {
query_pattern,
perf_characteristics,
last_used: Instant::now(),
usage_count: 1,
avg_execution_time_ms: 0.0,
})
}
async fn analyze_query_pattern(&self, request: &BrpRequest) -> Result<QueryPattern> {
let query_hash = self.hash_request(request);
let (access_strategy, performance_impact) = match request.method.as_str() {
BRP_QUERY_METHOD => {
let filter = request.params.as_ref()
.and_then(|p| p.get("filter"))
.and_then(|f| serde_json::from_value::<QueryFilter>(f.clone()).ok());
let limit = request.params.as_ref()
.and_then(|p| p.get("limit"))
.and_then(|l| l.as_u64())
.map(|l| l as usize);
self.analyze_query_request(filter.as_ref(), limit).await?
}
BRP_GET_COMPONENTS_METHOD => {
let components: Vec<String> = request.params.as_ref()
.and_then(|p| p.get("components"))
.and_then(|c| serde_json::from_value(c.clone()).ok())
.unwrap_or_default();
let strategy = ComponentAccessStrategy::DirectArchetype {
components,
parallel_threshold: usize::MAX,
};
(strategy, PerformanceImpact::Low)
}
BRP_LIST_COMPONENTS_METHOD => {
let strategy = ComponentAccessStrategy::DirectArchetype {
components: vec![],
parallel_threshold: 1000,
};
(strategy, PerformanceImpact::Medium)
}
_ => {
let strategy = ComponentAccessStrategy::DirectArchetype {
components: vec![],
parallel_threshold: 500,
};
(strategy, PerformanceImpact::Medium)
}
};
Ok(QueryPattern {
query_hash,
original_request: request.clone(),
access_strategy,
performance_impact,
})
}
async fn analyze_query_request(
&self,
filter: Option<&QueryFilter>,
limit: Option<usize>,
) -> Result<(ComponentAccessStrategy, PerformanceImpact)> {
let Some(filter) = filter else {
let strategy = ComponentAccessStrategy::FilteredIteration {
with_components: vec![],
without_components: vec![],
filter_early: false,
};
return Ok((strategy, PerformanceImpact::High));
};
let with_components = filter.with.clone().unwrap_or_default();
let without_components = filter.without.clone().unwrap_or_default();
let parallel_threshold = match (with_components.len(), without_components.len()) {
(0, 0) => 100, (1..=2, 0) => 500, (_, _) => 1000, };
let performance_impact = if with_components.is_empty() && without_components.is_empty() {
PerformanceImpact::High
} else if with_components.len() <= 2 && without_components.len() <= 1 {
PerformanceImpact::Medium
} else {
PerformanceImpact::Low
};
let strategy = if with_components.len() <= 3 && without_components.is_empty() {
ComponentAccessStrategy::DirectArchetype {
components: with_components,
parallel_threshold,
}
} else {
ComponentAccessStrategy::FilteredIteration {
with_components,
without_components,
filter_early: true,
}
};
Ok((strategy, performance_impact))
}
async fn analyze_list_entities_request(
&self,
filter: Option<&QueryFilter>,
) -> Result<(ComponentAccessStrategy, PerformanceImpact)> {
if let Some(filter) = filter {
self.analyze_query_request(Some(filter), None).await
} else {
let strategy = ComponentAccessStrategy::FilteredIteration {
with_components: vec![],
without_components: vec![],
filter_early: false,
};
Ok((strategy, PerformanceImpact::Critical))
}
}
fn analyze_performance_characteristics(&self, pattern: &QueryPattern) -> QueryPerformanceCharacteristics {
let (parallel_friendly, estimated_entity_count, cpu_complexity) = match &pattern.access_strategy {
ComponentAccessStrategy::DirectArchetype { components, parallel_threshold } => {
let entity_count = self.estimate_entity_count_for_components(components);
(entity_count >= *parallel_threshold, entity_count, 2)
}
ComponentAccessStrategy::FilteredIteration { with_components, without_components, .. } => {
let entity_count = self.estimate_filtered_entity_count(with_components, without_components);
let complexity = 3 + without_components.len() as u8;
(entity_count >= 500, entity_count, complexity.min(10))
}
ComponentAccessStrategy::MultiPass { passes, .. } => {
let total_entities: usize = passes.iter().map(|p| p.estimated_entity_count).sum();
let complexity = (5 + passes.len() as u8).min(10);
(total_entities >= 1000, total_entities, complexity)
}
};
let memory_overhead_per_entity = match &pattern.access_strategy {
ComponentAccessStrategy::DirectArchetype { components, .. } => {
components.len() * 64 + 32 }
ComponentAccessStrategy::FilteredIteration { .. } => {
128 }
ComponentAccessStrategy::MultiPass { .. } => {
256 }
};
let cacheable = matches!(pattern.performance_impact,
PerformanceImpact::Medium | PerformanceImpact::High | PerformanceImpact::Critical
);
QueryPerformanceCharacteristics {
parallel_friendly,
estimated_entity_count,
memory_overhead_per_entity,
cpu_complexity,
cacheable,
}
}
fn estimate_entity_count_for_components(&self, components: &[String]) -> usize {
match components.len() {
0 => 10000, 1 => 5000, 2 => 2000, 3 => 800, _ => 200, }
}
fn estimate_filtered_entity_count(&self, with: &[String], without: &[String]) -> usize {
let base_count = self.estimate_entity_count_for_components(with);
let exclusion_factor = 1.0 - (without.len() as f64 * 0.2).min(0.8);
(base_count as f64 * exclusion_factor) as usize
}
fn hash_request(&self, request: &BrpRequest) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
match request.method.as_str() {
BRP_QUERY_METHOD => {
"Query".hash(&mut hasher);
if let Some(params) = &request.params {
if let Some(filter_val) = params.get("filter") {
if let Ok(filter) = serde_json::from_value::<QueryFilter>(filter_val.clone()) {
filter.with.hash(&mut hasher);
filter.without.hash(&mut hasher);
} else {
None::<Vec<String>>.hash(&mut hasher);
}
} else {
None::<Vec<String>>.hash(&mut hasher);
}
params.get("limit").hash(&mut hasher);
} else {
None::<Vec<String>>.hash(&mut hasher);
None::<u64>.hash(&mut hasher);
}
}
BRP_GET_COMPONENTS_METHOD => {
"Get".hash(&mut hasher);
if let Some(params) = &request.params {
params.get("entity").hash(&mut hasher);
params.get("components").hash(&mut hasher);
}
}
BRP_LIST_COMPONENTS_METHOD => {
"ListComponents".hash(&mut hasher);
}
_ => {
request.method.hash(&mut hasher);
request.params.hash(&mut hasher);
}
}
hasher.finish()
}
pub fn record_metrics(&mut self, metrics: QueryPerformanceMetrics) {
if let Some(cached_state) = self.cache.get_mut(&self.hash_request(&BrpRequest {
method: BRP_QUERY_METHOD.to_string(),
id: None,
params: Some(serde_json::json!({})),
})) {
cached_state.avg_execution_time_ms =
(cached_state.avg_execution_time_ms * (cached_state.usage_count - 1) as f64 +
metrics.execution_time_ms as f64) / cached_state.usage_count as f64;
}
self.cache_stats.avg_execution_time_ms =
(self.cache_stats.avg_execution_time_ms * (self.metrics_history.len() as f64) +
metrics.execution_time_ms as f64) / (self.metrics_history.len() + 1) as f64;
self.metrics_history.push(metrics);
if self.metrics_history.len() > self.max_metrics_history {
self.metrics_history.drain(0..self.metrics_history.len() - self.max_metrics_history);
}
}
fn evict_if_necessary(&mut self) {
if self.cache.len() <= self.max_cache_size {
return;
}
let mut oldest_time = Instant::now();
let mut oldest_hash = 0;
for (&hash, state) in &self.cache {
if state.last_used < oldest_time {
oldest_time = state.last_used;
oldest_hash = hash;
}
}
if oldest_hash != 0 {
self.cache.remove(&oldest_hash);
self.cache_stats.evictions += 1;
debug!("Evicted query state with hash: {}", oldest_hash);
}
}
pub fn stats(&self) -> &CacheStatistics {
&self.cache_stats
}
pub fn recent_metrics(&self, count: usize) -> Vec<&QueryPerformanceMetrics> {
let start_idx = self.metrics_history.len().saturating_sub(count);
self.metrics_history[start_idx..].iter().collect()
}
pub fn clear(&mut self) {
self.cache.clear();
self.metrics_history.clear();
self.cache_stats = CacheStatistics::default();
info!("Query state cache cleared");
}
}
pub struct QueryOptimizer {
cache: Arc<RwLock<QueryStateCache>>,
parallel_threshold: usize,
metrics_enabled: bool,
}
impl QueryOptimizer {
pub fn new(cache_size: usize, metrics_history_size: usize, parallel_threshold: usize) -> Self {
Self {
cache: Arc::new(RwLock::new(QueryStateCache::new(cache_size, metrics_history_size))),
parallel_threshold,
metrics_enabled: true,
}
}
pub async fn optimize_request(&self, request: &BrpRequest) -> Result<OptimizedQuery> {
let mut cache = self.cache.write().await;
let cached_state = cache.get_or_build_query_state(request).await?;
let should_use_parallel = cached_state.perf_characteristics.parallel_friendly
&& cached_state.perf_characteristics.estimated_entity_count >= self.parallel_threshold;
Ok(OptimizedQuery {
original_request: request.clone(),
cached_state,
should_use_parallel,
optimization_applied: true,
})
}
pub async fn record_execution_metrics(&self, metrics: QueryPerformanceMetrics) {
if !self.metrics_enabled {
return;
}
let mut cache = self.cache.write().await;
cache.record_metrics(metrics);
}
pub async fn get_stats(&self) -> CacheStatistics {
let cache = self.cache.read().await;
cache.stats().clone()
}
pub async fn get_recent_metrics(&self, count: usize) -> Vec<QueryPerformanceMetrics> {
let cache = self.cache.read().await;
cache.recent_metrics(count).into_iter().cloned().collect()
}
pub async fn clear_cache(&self) {
let mut cache = self.cache.write().await;
cache.clear();
}
}
#[derive(Debug)]
pub struct OptimizedQuery {
pub original_request: BrpRequest,
pub cached_state: Arc<CachedQueryState>,
pub should_use_parallel: bool,
pub optimization_applied: bool,
}
impl OptimizedQuery {
pub fn performance_impact(&self) -> &PerformanceImpact {
&self.cached_state.query_pattern.performance_impact
}
pub fn estimated_execution_time_ms(&self) -> u64 {
self.cached_state.avg_execution_time_ms as u64
}
pub fn should_cache_results(&self) -> bool {
self.cached_state.perf_characteristics.cacheable
}
pub fn recommended_batch_size(&self) -> Option<usize> {
if !self.should_use_parallel {
return None;
}
let entity_count = self.cached_state.perf_characteristics.estimated_entity_count;
let cpu_count = num_cpus::get();
Some((entity_count / (cpu_count * 2)).max(100).min(1000))
}
}
unsafe impl Send for QueryStateCache {}
unsafe impl Sync for QueryStateCache {}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_query_state_cache_basic() {
let mut cache = QueryStateCache::new(10, 100);
let request = BrpRequest {
method: BRP_LIST_COMPONENTS_METHOD.to_string(),
id: None,
params: None,
};
let state = cache.get_or_build_query_state(&request).await.unwrap();
assert_eq!(state.usage_count, 1);
assert!(cache.stats().cache_misses == 1);
let state2 = cache.get_or_build_query_state(&request).await.unwrap();
assert!(cache.stats().cache_hits == 1);
}
#[tokio::test]
async fn test_query_optimizer() {
let optimizer = QueryOptimizer::new(10, 100, 500);
let request = BrpRequest {
method: BRP_QUERY_METHOD.to_string(),
id: None,
params: Some(serde_json::json!({
"filter": {
"with": ["Transform"],
"without": [],
},
"limit": null,
"strict": false,
})),
};
let optimized = optimizer.optimize_request(&request).await.unwrap();
assert!(optimized.optimization_applied);
}
}