use std::collections::HashMap;
use std::time::Instant;
use web_sys::{DomRect, Element};
#[derive(Debug, Clone)]
pub struct LayoutChange {
pub element_id: String,
pub previous_layout: DomRect,
pub current_layout: DomRect,
pub timestamp: f64,
pub change_type: LayoutChangeType,
pub performance_impact: PerformanceImpact,
}
#[derive(Debug, Clone)]
pub enum LayoutChangeType {
Position,
Size,
PositionAndSize,
None,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PerformanceImpact {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone)]
pub struct LayoutStats {
pub total_changes: usize,
pub changes_last_second: usize,
pub change_frequency: f64,
pub impact_distribution: HashMap<PerformanceImpact, usize>,
pub frame_rate: f64,
}
pub struct LayoutTracker {
tracked_elements: HashMap<String, TrackedElement>,
change_history: Vec<LayoutChange>,
performance_monitor: PerformanceMonitor,
stats: LayoutStats,
enabled: bool,
}
#[derive(Debug, Clone)]
pub struct TrackedElement {
pub id: String,
pub element: Element,
pub current_layout: DomRect,
pub previous_layout: Option<DomRect>,
pub last_update: Instant,
pub change_count: usize,
}
pub struct PerformanceMonitor {
frame_times: Vec<f64>,
memory_usage: Vec<usize>,
#[allow(dead_code)]
performance_observer: Option<PerformanceObserver>,
pub last_frame_time: f64,
}
pub struct PerformanceObserver {
pub id: String,
pub callback: Box<dyn FnMut(f64)>,
}
impl LayoutTracker {
pub fn new() -> Self {
Self {
tracked_elements: HashMap::new(),
change_history: Vec::new(),
performance_monitor: PerformanceMonitor::new(),
stats: LayoutStats::default(),
enabled: true,
}
}
pub fn track_element(&mut self, element: Element) -> Result<(), String> {
let element_id = element.id();
let current_layout = element.get_bounding_client_rect();
let tracked_element = TrackedElement {
id: element_id.clone(),
element: element.clone(),
current_layout,
previous_layout: None,
last_update: Instant::now(),
change_count: 0,
};
self.tracked_elements.insert(element_id, tracked_element);
Ok(())
}
pub fn untrack_element(&mut self, element_id: &str) -> Result<(), String> {
if self.tracked_elements.remove(element_id).is_some() {
Ok(())
} else {
Err(format!("Element not being tracked: {}", element_id))
}
}
pub fn update(&mut self) -> Result<Vec<LayoutChange>, String> {
if !self.enabled {
return Ok(Vec::new());
}
let mut changes = Vec::new();
let current_time = js_sys::Date::now();
let element_ids: Vec<String> = self.tracked_elements.keys().cloned().collect();
for element_id in element_ids {
let new_layout = if let Some(tracked_element) = self.tracked_elements.get(&element_id) {
tracked_element.element.get_bounding_client_rect()
} else {
continue;
};
let current_layout =
if let Some(tracked_element) = self.tracked_elements.get(&element_id) {
tracked_element.current_layout.clone()
} else {
continue;
};
if self.has_layout_changed(¤t_layout, &new_layout) {
let change = LayoutChange {
element_id: element_id.clone(),
previous_layout: current_layout.clone(),
current_layout: new_layout.clone(),
timestamp: current_time,
change_type: LayoutChangeType::PositionAndSize, performance_impact: PerformanceImpact::Low, };
changes.push(change.clone());
if let Some(tracked_element) = self.tracked_elements.get_mut(&element_id) {
tracked_element.previous_layout = Some(current_layout);
tracked_element.current_layout = new_layout;
tracked_element.last_update = Instant::now();
tracked_element.change_count += 1;
}
self.change_history.push(change);
}
}
self.performance_monitor.update()?;
self.update_stats(&changes);
self.cleanup_history();
Ok(changes)
}
pub fn get_element_layout(&self, element: &Element) -> Result<DomRect, String> {
Ok(element.get_bounding_client_rect())
}
pub fn get_element_changes(&self, element_id: &str) -> Vec<&LayoutChange> {
self.change_history
.iter()
.filter(|change| change.element_id == element_id)
.collect()
}
pub fn get_recent_changes(&self, count: usize) -> Vec<&LayoutChange> {
let start = if self.change_history.len() > count {
self.change_history.len() - count
} else {
0
};
self.change_history[start..].iter().collect()
}
pub fn get_stats(&self) -> &LayoutStats {
&self.stats
}
pub fn get_frame_rate(&self) -> f64 {
self.performance_monitor.get_frame_rate()
}
pub fn get_memory_usage(&self) -> usize {
self.performance_monitor.get_memory_usage()
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
fn has_layout_changed(&self, old: &DomRect, new: &DomRect) -> bool {
const THRESHOLD: f64 = 0.1;
(old.left() - new.left()).abs() > THRESHOLD
|| (old.top() - new.top()).abs() > THRESHOLD
|| (old.width() - new.width()).abs() > THRESHOLD
|| (old.height() - new.height()).abs() > THRESHOLD
}
#[allow(dead_code)]
fn create_layout_change(
&self,
tracked_element: &TrackedElement,
new_layout: &DomRect,
timestamp: f64,
) -> Result<LayoutChange, String> {
let previous_layout = tracked_element.current_layout.clone();
let change_type = self.determine_change_type(&previous_layout, new_layout);
let performance_impact = self.calculate_performance_impact(&previous_layout, new_layout);
Ok(LayoutChange {
element_id: tracked_element.id.clone(),
previous_layout,
current_layout: new_layout.clone(),
timestamp,
change_type,
performance_impact,
})
}
#[allow(dead_code)]
fn determine_change_type(&self, old: &DomRect, new: &DomRect) -> LayoutChangeType {
let position_changed =
(old.left() - new.left()).abs() > 0.1 || (old.top() - new.top()).abs() > 0.1;
let size_changed =
(old.width() - new.width()).abs() > 0.1 || (old.height() - new.height()).abs() > 0.1;
match (position_changed, size_changed) {
(true, true) => LayoutChangeType::PositionAndSize,
(true, false) => LayoutChangeType::Position,
(false, true) => LayoutChangeType::Size,
(false, false) => LayoutChangeType::None,
}
}
#[allow(dead_code)]
fn calculate_performance_impact(&self, old: &DomRect, new: &DomRect) -> PerformanceImpact {
let area_change = (new.width() * new.height() - old.width() * old.height()).abs();
let position_change =
((new.left() - old.left()).powi(2) + (new.top() - old.top()).powi(2)).sqrt();
let total_change = area_change + position_change;
match total_change {
change if change < 100.0 => PerformanceImpact::Low,
change if change < 1000.0 => PerformanceImpact::Medium,
change if change < 10000.0 => PerformanceImpact::High,
_ => PerformanceImpact::Critical,
}
}
fn update_stats(&mut self, changes: &[LayoutChange]) {
self.stats.total_changes += changes.len();
let current_time = js_sys::Date::now();
let one_second_ago = current_time - 1000.0;
self.stats.changes_last_second = self
.change_history
.iter()
.filter(|change| change.timestamp >= one_second_ago)
.count();
if self.stats.total_changes > 0 {
let total_time = current_time / 1000.0; self.stats.change_frequency = self.stats.total_changes as f64 / total_time;
}
for change in changes {
let count = self
.stats
.impact_distribution
.entry(change.performance_impact.clone())
.or_insert(0);
*count += 1;
}
self.stats.frame_rate = self.performance_monitor.get_frame_rate();
}
fn cleanup_history(&mut self) {
const MAX_HISTORY_SIZE: usize = 1000;
if self.change_history.len() > MAX_HISTORY_SIZE {
let remove_count = self.change_history.len() - MAX_HISTORY_SIZE;
self.change_history.drain(0..remove_count);
}
}
}
impl PerformanceMonitor {
pub fn new() -> Self {
Self {
frame_times: Vec::new(),
memory_usage: Vec::new(),
performance_observer: None,
last_frame_time: js_sys::Date::now(),
}
}
pub fn update(&mut self) -> Result<(), String> {
let current_time = js_sys::Date::now();
let frame_time = current_time - self.last_frame_time;
self.frame_times.push(frame_time);
if self.frame_times.len() > 60 {
self.frame_times.remove(0);
}
self.last_frame_time = current_time;
self.memory_usage.push(0);
if self.memory_usage.len() > 60 {
self.memory_usage.remove(0);
}
Ok(())
}
pub fn get_frame_rate(&self) -> f64 {
if self.frame_times.is_empty() {
return 60.0; }
let total_time: f64 = self.frame_times.iter().sum();
let frame_count = self.frame_times.len() as f64;
if total_time > 0.0 {
1000.0 / (total_time / frame_count) } else {
60.0
}
}
pub fn get_memory_usage(&self) -> usize {
self.memory_usage.last().copied().unwrap_or(0)
}
}
impl Default for LayoutTracker {
fn default() -> Self {
Self::new()
}
}
impl Default for PerformanceMonitor {
fn default() -> Self {
Self::new()
}
}
impl Default for LayoutStats {
fn default() -> Self {
Self {
total_changes: 0,
changes_last_second: 0,
change_frequency: 0.0,
impact_distribution: HashMap::new(),
frame_rate: 60.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_layout_tracker_creation() {
let tracker = LayoutTracker::new();
assert_eq!(tracker.tracked_elements.len(), 0);
assert_eq!(tracker.change_history.len(), 0);
assert!(tracker.enabled);
}
#[wasm_bindgen_test]
fn test_performance_monitor_creation() {
let monitor = PerformanceMonitor::new();
assert_eq!(monitor.frame_times.len(), 0);
assert_eq!(monitor.memory_usage.len(), 0);
}
#[wasm_bindgen_test]
fn test_layout_change_type_determination() {
let tracker = LayoutTracker::new();
let old = DomRect::new().unwrap();
let new_position = DomRect::new().unwrap();
let new_size = DomRect::new().unwrap();
let new_both = DomRect::new().unwrap();
let change_type = tracker.determine_change_type(&old, &new_position);
assert!(matches!(change_type, LayoutChangeType::None));
let change_type = tracker.determine_change_type(&old, &new_size);
assert!(matches!(change_type, LayoutChangeType::None));
let change_type = tracker.determine_change_type(&old, &new_both);
assert!(matches!(change_type, LayoutChangeType::None));
let change_type = tracker.determine_change_type(&old, &old);
assert!(matches!(change_type, LayoutChangeType::None));
}
#[wasm_bindgen_test]
fn test_performance_impact_calculation() {
let tracker = LayoutTracker::new();
let old = DomRect::new().unwrap();
let small_change = DomRect::new().unwrap();
let large_change = DomRect::new().unwrap();
let impact = tracker.calculate_performance_impact(&old, &small_change);
assert!(matches!(impact, PerformanceImpact::Low));
let impact = tracker.calculate_performance_impact(&old, &large_change);
assert!(matches!(impact, PerformanceImpact::Low));
}
}