use async_trait::async_trait;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use sysinfo::Disks;
use tokio::sync::{Mutex, RwLock};
use tokio::time::timeout;
type ProcessCache = std::sync::RwLock<HashMap<u32, ProcessInfo>>;
use crate::app_state::AppState;
#[cfg(target_os = "linux")]
use crate::device::platform_detection::has_tenstorrent;
use crate::device::{
create_chassis_reader, get_cpu_readers, get_gpu_readers, get_memory_readers,
get_nvml_status_message,
platform_detection::has_nvidia,
process_list::{merge_gpu_processes, update_process_cache},
ChassisInfo, ChassisReader, CpuInfo, CpuReader, GpuInfo, GpuReader, MemoryInfo, MemoryReader,
ProcessInfo,
};
#[cfg(target_os = "linux")]
use crate::device::get_tenstorrent_status_message;
#[cfg(target_os = "linux")]
use crate::device::get_tpu_status_message;
#[cfg(target_os = "linux")]
use crate::device::platform_detection::has_google_tpu;
use crate::storage::info::StorageInfo;
use crate::utils::{filter_docker_aware_disks, get_hostname, with_global_system};
use super::aggregator::DataAggregator;
use super::strategy::{
CollectionConfig, CollectionData, CollectionError, CollectionResult, DataCollectionStrategy,
};
const MAX_DISPLAY_PROCESSES: usize = 500;
const FULL_REFRESH_INTERVAL: u32 = 5;
pub struct LocalCollector {
gpu_readers: Arc<RwLock<Vec<Box<dyn GpuReader>>>>,
cpu_readers: Arc<RwLock<Vec<Box<dyn CpuReader>>>>,
memory_readers: Arc<RwLock<Vec<Box<dyn MemoryReader>>>>,
chassis_reader: Arc<RwLock<Option<Box<dyn ChassisReader>>>>,
aggregator: DataAggregator,
initialized: Arc<Mutex<bool>>,
tracked_pids: Arc<RwLock<Vec<sysinfo::Pid>>>,
refresh_cycle: Arc<AtomicU32>,
process_cache: Arc<ProcessCache>,
}
impl LocalCollector {
pub fn new() -> Self {
Self {
gpu_readers: Arc::new(RwLock::new(Vec::new())),
cpu_readers: Arc::new(RwLock::new(Vec::new())),
memory_readers: Arc::new(RwLock::new(Vec::new())),
chassis_reader: Arc::new(RwLock::new(None)),
aggregator: DataAggregator::new(),
initialized: Arc::new(Mutex::new(false)),
tracked_pids: Arc::new(RwLock::new(Vec::new())),
refresh_cycle: Arc::new(AtomicU32::new(0)),
process_cache: Arc::new(std::sync::RwLock::new(HashMap::with_capacity(
MAX_DISPLAY_PROCESSES,
))),
}
}
async fn initialize_readers(&self, app_state: Arc<Mutex<AppState>>) {
let initialized_result = timeout(Duration::from_secs(5), self.initialized.lock()).await;
let mut initialized = match initialized_result {
Ok(lock) => lock,
Err(_) => {
eprintln!("Warning: Timeout acquiring initialized lock");
return;
}
};
if *initialized {
return;
}
{
let state_result = timeout(Duration::from_secs(2), app_state.lock()).await;
if let Ok(mut state) = state_result {
state
.startup_status_lines
.push("✓ Initializing GPU readers...".to_string());
}
}
let gpu_readers = get_gpu_readers();
{
let mut state = app_state.lock().await;
state
.startup_status_lines
.push("✓ Initializing CPU readers...".to_string());
}
let cpu_readers = get_cpu_readers();
{
let mut state = app_state.lock().await;
state
.startup_status_lines
.push("✓ Initializing memory readers...".to_string());
}
let memory_readers = get_memory_readers();
let chassis_reader = create_chassis_reader();
{
if let Ok(mut gpu_lock) =
timeout(Duration::from_secs(2), self.gpu_readers.write()).await
{
*gpu_lock = gpu_readers;
} else {
eprintln!("Warning: Timeout acquiring GPU readers lock");
}
}
{
if let Ok(mut cpu_lock) =
timeout(Duration::from_secs(2), self.cpu_readers.write()).await
{
*cpu_lock = cpu_readers;
} else {
eprintln!("Warning: Timeout acquiring CPU readers lock");
}
}
{
if let Ok(mut mem_lock) =
timeout(Duration::from_secs(2), self.memory_readers.write()).await
{
*mem_lock = memory_readers;
} else {
eprintln!("Warning: Timeout acquiring memory readers lock");
}
}
{
if let Ok(mut chassis_lock) =
timeout(Duration::from_secs(2), self.chassis_reader.write()).await
{
*chassis_lock = Some(chassis_reader);
} else {
eprintln!("Warning: Timeout acquiring chassis reader lock");
}
}
*initialized = true;
}
async fn collect_parallel_first_iteration(
&self,
app_state: Arc<Mutex<AppState>>,
) -> CollectionData {
use tokio::sync::mpsc;
use tokio::task;
{
let mut state = app_state.lock().await;
state
.startup_status_lines
.push("â—‹ Collecting GPU information...".to_string());
state
.startup_status_lines
.push("â—‹ Collecting CPU information...".to_string());
state
.startup_status_lines
.push("â—‹ Collecting memory information...".to_string());
state
.startup_status_lines
.push("â—‹ Collecting process information...".to_string());
state
.startup_status_lines
.push("â—‹ Collecting storage information...".to_string());
}
let (status_tx, mut status_rx) = mpsc::channel(10);
let app_state_clone = Arc::clone(&app_state);
let status_handler = task::spawn(async move {
while let Some((index, message)) = status_rx.recv().await {
let mut state = app_state_clone.lock().await;
if index < state.startup_status_lines.len() {
state.startup_status_lines[3 + index] = message;
}
}
});
let gpu_readers_1 = Arc::clone(&self.gpu_readers);
let gpu_readers_2 = Arc::clone(&self.gpu_readers);
let cpu_readers = Arc::clone(&self.cpu_readers);
let memory_readers = Arc::clone(&self.memory_readers);
let chassis_reader = Arc::clone(&self.chassis_reader);
let process_cache = Arc::clone(&self.process_cache);
let (
all_gpu_info,
all_cpu_info,
all_memory_info,
gpu_processes_result,
all_processes,
all_storage_info,
all_chassis_info,
) = {
let status_tx_gpu = status_tx.clone();
let status_tx_cpu = status_tx.clone();
let status_tx_mem = status_tx.clone();
let status_tx_proc = status_tx.clone();
let status_tx_storage = status_tx.clone();
tokio::join!(
async move {
let readers = gpu_readers_1.read().await;
let info: Vec<GpuInfo> = readers
.iter()
.flat_map(|reader| reader.get_gpu_info())
.collect();
let _ = status_tx_gpu
.send((0, "✓ GPU information collected".to_string()))
.await;
info
},
async move {
let readers = cpu_readers.read().await;
let info: Vec<CpuInfo> = readers
.iter()
.flat_map(|reader| reader.get_cpu_info())
.collect();
let _ = status_tx_cpu
.send((1, "✓ CPU information collected".to_string()))
.await;
info
},
async move {
let readers = memory_readers.read().await;
let info: Vec<MemoryInfo> = readers
.iter()
.flat_map(|reader| reader.get_memory_info())
.collect();
let _ = status_tx_mem
.send((2, "✓ Memory information collected".to_string()))
.await;
info
},
async move {
let readers = gpu_readers_2.read().await;
let mut all_gpu_procs = Vec::new();
let mut all_gpu_pids = HashSet::new();
for reader in readers.iter() {
let (procs, pids) = reader.get_gpu_processes();
all_gpu_procs.extend(procs);
all_gpu_pids.extend(pids);
}
(all_gpu_procs, all_gpu_pids)
},
async move {
let all_processes = tokio::task::spawn_blocking(move || {
with_global_system(|system| {
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, UpdateKind};
let refresh_kind = ProcessRefreshKind::nothing()
.with_cpu()
.with_memory()
.with_user(UpdateKind::OnlyIfNotSet);
system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
refresh_kind,
);
system.refresh_memory();
let gpu_pids: HashSet<u32> = HashSet::new();
let mut cache = process_cache.write().unwrap();
update_process_cache(system, &gpu_pids, &mut cache)
})
})
.await
.unwrap_or_default();
let _ = status_tx_proc
.send((3, "✓ Process information collected".to_string()))
.await;
all_processes
},
async move {
let storage_info = Self::collect_storage_info();
let _ = status_tx_storage
.send((4, "✓ Storage information collected".to_string()))
.await;
storage_info
},
async move {
let reader = chassis_reader.read().await;
let info: Vec<ChassisInfo> = reader
.as_ref()
.and_then(|r| r.get_chassis_info())
.into_iter()
.collect();
info
}
)
};
drop(status_tx);
let _ = status_handler.await;
let (gpu_processes, _gpu_pids) = gpu_processes_result;
let mut all_processes_merged = merge_gpu_processes(all_processes, gpu_processes);
all_processes_merged.sort_by(|a, b| {
b.cpu_percent
.partial_cmp(&a.cpu_percent)
.unwrap_or(std::cmp::Ordering::Equal)
});
if all_processes_merged.len() > MAX_DISPLAY_PROCESSES {
all_processes_merged.truncate(MAX_DISPLAY_PROCESSES);
}
let new_tracked_pids: Vec<sysinfo::Pid> = all_processes_merged
.iter()
.map(|p| sysinfo::Pid::from_u32(p.pid))
.collect();
*self.tracked_pids.write().await = new_tracked_pids;
self.refresh_cycle.store(1, Ordering::Relaxed);
CollectionData {
gpu_info: all_gpu_info,
cpu_info: all_cpu_info,
memory_info: all_memory_info,
process_info: all_processes_merged,
storage_info: all_storage_info,
chassis_info: all_chassis_info,
connection_statuses: Vec::new(),
}
}
async fn collect_sequential(&self) -> CollectionData {
let gpu_readers = self.gpu_readers.read().await;
let all_gpu_info: Vec<GpuInfo> = gpu_readers
.iter()
.flat_map(|reader| reader.get_gpu_info())
.collect();
let cpu_readers = self.cpu_readers.read().await;
let all_cpu_info: Vec<CpuInfo> = cpu_readers
.iter()
.flat_map(|reader| reader.get_cpu_info())
.collect();
let memory_readers = self.memory_readers.read().await;
let all_memory_info: Vec<MemoryInfo> = memory_readers
.iter()
.flat_map(|reader| reader.get_memory_info())
.collect();
let mut gpu_processes = Vec::new();
let mut gpu_pids = HashSet::new();
for reader in gpu_readers.iter() {
let (procs, pids) = reader.get_gpu_processes();
gpu_processes.extend(procs);
gpu_pids.extend(pids);
}
let cycle = self.refresh_cycle.fetch_add(1, Ordering::Relaxed);
let do_full_refresh = cycle.is_multiple_of(FULL_REFRESH_INTERVAL);
let tracked_pids_for_refresh: Vec<sysinfo::Pid> = if do_full_refresh {
Vec::new() } else {
self.tracked_pids.read().await.clone()
};
let process_cache = Arc::clone(&self.process_cache);
let all_processes = with_global_system(|system| {
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, UpdateKind};
let refresh_kind = ProcessRefreshKind::nothing()
.with_cpu()
.with_memory()
.with_user(UpdateKind::OnlyIfNotSet);
if do_full_refresh || tracked_pids_for_refresh.is_empty() {
system.refresh_processes_specifics(ProcessesToUpdate::All, true, refresh_kind);
} else {
system.refresh_processes_specifics(
ProcessesToUpdate::Some(&tracked_pids_for_refresh),
true,
refresh_kind,
);
}
system.refresh_memory();
let mut cache = process_cache.write().unwrap();
update_process_cache(system, &gpu_pids, &mut cache)
});
let mut all_processes = merge_gpu_processes(all_processes, gpu_processes);
all_processes.sort_by(|a, b| {
b.cpu_percent
.partial_cmp(&a.cpu_percent)
.unwrap_or(std::cmp::Ordering::Equal)
});
if all_processes.len() > MAX_DISPLAY_PROCESSES {
all_processes.truncate(MAX_DISPLAY_PROCESSES);
}
let new_tracked_pids: Vec<sysinfo::Pid> = all_processes
.iter()
.map(|p| sysinfo::Pid::from_u32(p.pid))
.collect();
*self.tracked_pids.write().await = new_tracked_pids;
let all_storage_info = Self::collect_storage_info();
let chassis_reader = self.chassis_reader.read().await;
let all_chassis_info: Vec<ChassisInfo> = chassis_reader
.as_ref()
.and_then(|r| r.get_chassis_info())
.into_iter()
.collect();
CollectionData {
gpu_info: all_gpu_info,
cpu_info: all_cpu_info,
memory_info: all_memory_info,
process_info: all_processes,
storage_info: all_storage_info,
chassis_info: all_chassis_info,
connection_statuses: Vec::new(),
}
}
fn collect_storage_info() -> Vec<StorageInfo> {
let mut all_storage_info = Vec::new();
let disks = Disks::new_with_refreshed_list();
let hostname = get_hostname();
let mut filtered_disks = filter_docker_aware_disks(&disks);
filtered_disks.sort_by(|a, b| {
a.mount_point()
.to_string_lossy()
.cmp(&b.mount_point().to_string_lossy())
});
for (index, disk) in filtered_disks.iter().enumerate() {
let mount_point_str = disk.mount_point().to_string_lossy();
all_storage_info.push(StorageInfo {
mount_point: mount_point_str.to_string(),
total_bytes: disk.total_space(),
available_bytes: disk.available_space(),
host_id: hostname.clone(),
hostname: hostname.clone(),
index: index as u32,
});
}
all_storage_info
}
fn update_notifications(state: &mut AppState) {
state.notifications.update();
if has_nvidia() {
if let Some(nvml_message) = get_nvml_status_message() {
if !state.nvml_notification_shown {
if let Err(e) = state.notifications.warning(nvml_message) {
eprintln!("Failed to show NVML notification: {e}");
}
state.nvml_notification_shown = true;
}
}
}
#[cfg(target_os = "linux")]
if has_tenstorrent() {
if let Some(tt_message) = get_tenstorrent_status_message() {
if !state.tenstorrent_notification_shown {
if let Err(e) = state.notifications.warning(tt_message) {
eprintln!("Failed to show Tenstorrent notification: {e}");
}
state.tenstorrent_notification_shown = true;
}
}
}
#[cfg(target_os = "linux")]
if has_google_tpu() {
if let Some(msg) = get_tpu_status_message() {
if msg.contains("Initializing") {
let _ = state.notifications.status(msg);
} else if (msg.contains("failed") || msg.contains("error"))
&& !state.tpu_notification_shown
{
let _ = state.notifications.error(msg);
state.tpu_notification_shown = true;
}
}
}
}
fn update_tabs(state: &mut AppState) {
let mut host_ids: Vec<String> = state
.gpu_info
.iter()
.map(|info| info.host_id.clone())
.collect::<HashSet<_>>()
.into_iter()
.collect();
if host_ids.is_empty() {
host_ids.push(get_hostname());
}
host_ids.sort();
let mut tabs = vec!["All".to_string()];
tabs.extend(host_ids);
state.tabs = tabs;
}
}
#[async_trait]
impl DataCollectionStrategy for LocalCollector {
async fn collect(&self, config: &CollectionConfig) -> CollectionResult {
if config.first_iteration {
return Err(CollectionError::Other(
"First iteration requires app_state initialization".to_string(),
));
}
Ok(self.collect_sequential().await)
}
async fn update_state(
&self,
app_state: Arc<Mutex<AppState>>,
data: CollectionData,
_config: &CollectionConfig,
) {
if !*self.initialized.lock().await {
self.initialize_readers(app_state.clone()).await;
}
let mut state = app_state.lock().await;
if state.gpu_info.is_empty() {
state.gpu_info = data.gpu_info;
} else {
for new_info in data.gpu_info {
if let Some(old_info) = state
.gpu_info
.iter_mut()
.find(|info| info.uuid == new_info.uuid)
{
*old_info = new_info;
}
}
}
state.cpu_info = data.cpu_info;
state.memory_info = data.memory_info;
let mut sorted_processes = data.process_info;
sorted_processes.sort_by(|a, b| {
state
.sort_criteria
.sort_processes(a, b, state.sort_direction)
});
state.process_info = sorted_processes;
state.storage_info = data.storage_info;
state.chassis_info = data.chassis_info;
state.mark_data_changed();
Self::update_notifications(&mut state);
self.aggregator.update_utilization_history(&mut state);
Self::update_tabs(&mut state);
state.loading = false;
}
fn strategy_type(&self) -> &str {
"local"
}
async fn is_ready(&self) -> bool {
*self.initialized.lock().await
}
}
impl LocalCollector {
pub async fn collect_with_app_state(
&self,
app_state: Arc<Mutex<AppState>>,
config: &CollectionConfig,
) -> CollectionResult {
if !*self.initialized.lock().await {
self.initialize_readers(app_state.clone()).await;
}
if config.first_iteration {
Ok(self.collect_parallel_first_iteration(app_state).await)
} else {
Ok(self.collect_sequential().await)
}
}
}