Stabilize similarity and library status cards
This commit is contained in:
+48
-11
@@ -44,6 +44,10 @@ pub struct Runtime {
|
|||||||
pub similarity: Arc<crate::similarity::Manager>,
|
pub similarity: Arc<crate::similarity::Manager>,
|
||||||
/// When the last Federation-tab status snapshot was requested.
|
/// When the last Federation-tab status snapshot was requested.
|
||||||
pub fed_status_at: Option<std::time::Instant>,
|
pub fed_status_at: Option<std::time::Instant>,
|
||||||
|
/// Keeps the last successful local-data snapshot visible while a newer
|
||||||
|
/// one is calculated and collapses bursts of library-change events.
|
||||||
|
pub local_library_stats_refreshing: Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
pub local_library_stats_refresh_requested: Arc<std::sync::atomic::AtomicBool>,
|
||||||
pub library_network_refresh_at: Option<std::time::Instant>,
|
pub library_network_refresh_at: Option<std::time::Instant>,
|
||||||
pub library_network_refreshing: Arc<std::sync::atomic::AtomicBool>,
|
pub library_network_refreshing: Arc<std::sync::atomic::AtomicBool>,
|
||||||
pub library_network_cursors:
|
pub library_network_cursors:
|
||||||
@@ -102,11 +106,35 @@ fn refresh_local_content_ids(runtime: &Runtime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_local_library_stats(runtime: &Runtime) {
|
fn refresh_local_library_stats(runtime: &Runtime) {
|
||||||
|
runtime
|
||||||
|
.local_library_stats_refresh_requested
|
||||||
|
.store(true, std::sync::atomic::Ordering::Release);
|
||||||
|
if runtime
|
||||||
|
.local_library_stats_refreshing
|
||||||
|
.swap(true, std::sync::atomic::Ordering::AcqRel)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
let library = Arc::clone(&runtime.library);
|
let library = Arc::clone(&runtime.library);
|
||||||
let tx = runtime.event_tx.clone();
|
let tx = runtime.event_tx.clone();
|
||||||
|
let refreshing = Arc::clone(&runtime.local_library_stats_refreshing);
|
||||||
|
let requested = Arc::clone(&runtime.local_library_stats_refresh_requested);
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let result = library.local_stats().map_err(err_string);
|
loop {
|
||||||
let _ = tx.send(AppEvent::LocalLibraryStatsLoaded(result));
|
requested.store(false, std::sync::atomic::Ordering::Release);
|
||||||
|
let result = library.local_stats().map_err(err_string);
|
||||||
|
let _ = tx.send(AppEvent::LocalLibraryStatsLoaded(result));
|
||||||
|
if requested.load(std::sync::atomic::Ordering::Acquire) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
refreshing.store(false, std::sync::atomic::Ordering::Release);
|
||||||
|
if requested.swap(false, std::sync::atomic::Ordering::AcqRel)
|
||||||
|
&& !refreshing.swap(true, std::sync::atomic::Ordering::AcqRel)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +331,8 @@ pub async fn run(
|
|||||||
federation,
|
federation,
|
||||||
similarity,
|
similarity,
|
||||||
fed_status_at: None,
|
fed_status_at: None,
|
||||||
|
local_library_stats_refreshing: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
|
local_library_stats_refresh_requested: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
library_network_refresh_at: None,
|
library_network_refresh_at: None,
|
||||||
library_network_refreshing: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
library_network_refreshing: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
library_network_cursors: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
|
library_network_cursors: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
|
||||||
@@ -2746,7 +2776,12 @@ fn on_library_changed(state: &mut AppState, runtime: &mut Runtime) {
|
|||||||
// until then.
|
// until then.
|
||||||
state.likes_loaded = false;
|
state.likes_loaded = false;
|
||||||
state.local_content_ids_loaded = false;
|
state.local_content_ids_loaded = false;
|
||||||
state.local_library_stats = None;
|
// Refresh in place: status cards keep the last successful snapshot
|
||||||
|
// instead of flashing `loading` for every background library event.
|
||||||
|
if state.local_library_stats.is_none() {
|
||||||
|
state.local_library_stats = Some(state::Loadable::Loading);
|
||||||
|
}
|
||||||
|
refresh_local_library_stats(runtime);
|
||||||
|
|
||||||
// Fresh copies of whatever sits in the queue. Federated placeholders
|
// Fresh copies of whatever sits in the queue. Federated placeholders
|
||||||
// and ephemeral tracks (negative ids) are not library rows and keep
|
// and ephemeral tracks (negative ids) are not library rows and keep
|
||||||
@@ -3792,15 +3827,17 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
|
|||||||
tracing::warn!(%message, "local content id load failed");
|
tracing::warn!(%message, "local content id load failed");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
AppEvent::LocalLibraryStatsLoaded(result) => {
|
AppEvent::LocalLibraryStatsLoaded(result) => match result {
|
||||||
state.local_library_stats = Some(match result {
|
Ok(stats) => {
|
||||||
Ok(stats) => state::Loadable::Ready(stats),
|
state.local_library_stats = Some(state::Loadable::Ready(stats));
|
||||||
Err(message) => {
|
}
|
||||||
tracing::warn!(%message, "local library stats load failed");
|
Err(message) => {
|
||||||
state::Loadable::Failed(message)
|
tracing::warn!(%message, "local library stats load failed");
|
||||||
|
if !matches!(state.local_library_stats, Some(state::Loadable::Ready(_))) {
|
||||||
|
state.local_library_stats = Some(state::Loadable::Failed(message));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
},
|
||||||
AppEvent::LocalContentAvailable { content_id } => {
|
AppEvent::LocalContentAvailable { content_id } => {
|
||||||
if let Some(content_id) = music_dht::normalize_content_id(&content_id) {
|
if let Some(content_id) = music_dht::normalize_content_id(&content_id) {
|
||||||
state.local_content_ids.insert(content_id);
|
state.local_content_ids.insert(content_id);
|
||||||
|
|||||||
+131
-35
@@ -7,7 +7,7 @@
|
|||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ impl Phase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
pub struct SimilarityStatus {
|
pub struct SimilarityStatus {
|
||||||
pub phase: Phase,
|
pub phase: Phase,
|
||||||
pub active_profile: Option<String>,
|
pub active_profile: Option<String>,
|
||||||
@@ -144,6 +144,8 @@ pub struct Manager {
|
|||||||
settings: Mutex<SimilaritySettings>,
|
settings: Mutex<SimilaritySettings>,
|
||||||
workers: AtomicUsize,
|
workers: AtomicUsize,
|
||||||
generation: AtomicU64,
|
generation: AtomicU64,
|
||||||
|
pipeline_running: AtomicBool,
|
||||||
|
rescan_requested: AtomicBool,
|
||||||
status: Mutex<SimilarityStatus>,
|
status: Mutex<SimilarityStatus>,
|
||||||
index: RwLock<Index>,
|
index: RwLock<Index>,
|
||||||
model: Mutex<Option<(String, RunnableModel)>>,
|
model: Mutex<Option<(String, RunnableModel)>>,
|
||||||
@@ -169,13 +171,20 @@ impl Manager {
|
|||||||
Err(err) => tracing::warn!(%err, "similarity index restore failed"),
|
Err(err) => tracing::warn!(%err, "similarity index restore failed"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let target_profile = model_by_id(&settings.model)
|
||||||
|
.filter(|_| profile_by_id(&settings.profile).is_some())
|
||||||
|
.map(|model| profile_fingerprint(model, &settings.profile));
|
||||||
|
let restored_profile_is_current = index.profile_id == target_profile;
|
||||||
let status = SimilarityStatus {
|
let status = SimilarityStatus {
|
||||||
phase: if settings.enabled {
|
phase: if !settings.enabled {
|
||||||
Phase::Loading
|
|
||||||
} else {
|
|
||||||
Phase::Disabled
|
Phase::Disabled
|
||||||
|
} else if restored_profile_is_current {
|
||||||
|
Phase::Ready
|
||||||
|
} else {
|
||||||
|
Phase::Loading
|
||||||
},
|
},
|
||||||
active_profile: index.profile_id.clone(),
|
active_profile: index.profile_id.clone(),
|
||||||
|
target_profile,
|
||||||
model: settings.model.clone(),
|
model: settings.model.clone(),
|
||||||
..SimilarityStatus::default()
|
..SimilarityStatus::default()
|
||||||
};
|
};
|
||||||
@@ -184,6 +193,8 @@ impl Manager {
|
|||||||
event_tx,
|
event_tx,
|
||||||
workers: AtomicUsize::new(settings.workers.clamp(1, 16)),
|
workers: AtomicUsize::new(settings.workers.clamp(1, 16)),
|
||||||
generation: AtomicU64::new(0),
|
generation: AtomicU64::new(0),
|
||||||
|
pipeline_running: AtomicBool::new(false),
|
||||||
|
rescan_requested: AtomicBool::new(false),
|
||||||
settings: Mutex::new(settings),
|
settings: Mutex::new(settings),
|
||||||
status: Mutex::new(status),
|
status: Mutex::new(status),
|
||||||
index: RwLock::new(index),
|
index: RwLock::new(index),
|
||||||
@@ -223,23 +234,51 @@ impl Manager {
|
|||||||
|| previous.model != settings.model
|
|| previous.model != settings.model
|
||||||
|| previous.profile != settings.profile
|
|| previous.profile != settings.profile
|
||||||
{
|
{
|
||||||
|
self.generation.fetch_add(1, Ordering::AcqRel);
|
||||||
self.start();
|
self.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Requests a scan without cancelling useful work already in progress.
|
||||||
|
/// Bursts of library-change notifications collapse into one follow-up
|
||||||
|
/// pass, so metadata refreshes cannot repeatedly restart the model.
|
||||||
pub fn start(self: &Arc<Self>) {
|
pub fn start(self: &Arc<Self>) {
|
||||||
let generation = self.generation.fetch_add(1, Ordering::AcqRel) + 1;
|
self.rescan_requested.store(true, Ordering::Release);
|
||||||
|
if self.pipeline_running.swap(true, Ordering::AcqRel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let this = Arc::clone(self);
|
let this = Arc::clone(self);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(err) = this.run_pipeline(generation).await
|
loop {
|
||||||
&& this.generation.load(Ordering::Acquire) == generation
|
// This pass covers every notification received before it
|
||||||
{
|
// starts. A notification during the pass requests one more.
|
||||||
tracing::error!(%err, "similarity pipeline failed");
|
this.rescan_requested.store(false, Ordering::Release);
|
||||||
this.update_status(|status| {
|
let generation = this.generation.load(Ordering::Acquire);
|
||||||
status.phase = Phase::Error;
|
if let Err(err) = this.run_pipeline(generation).await
|
||||||
status.current_track = None;
|
&& this.generation.load(Ordering::Acquire) == generation
|
||||||
status.last_error = Some(format!("{err:#}"));
|
{
|
||||||
});
|
tracing::error!(%err, "similarity pipeline failed");
|
||||||
|
this.update_status(|status| {
|
||||||
|
status.phase = Phase::Error;
|
||||||
|
status.current_track = None;
|
||||||
|
status.last_error = Some(format!("{err:#}"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if this.rescan_requested.load(Ordering::Acquire) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pipeline_running.store(false, Ordering::Release);
|
||||||
|
// Close the small race between checking the request flag and
|
||||||
|
// releasing ownership of the worker. If another worker has
|
||||||
|
// already claimed it, that worker owns the pending pass.
|
||||||
|
if this.rescan_requested.swap(false, Ordering::AcqRel)
|
||||||
|
&& !this.pipeline_running.swap(true, Ordering::AcqRel)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -431,7 +470,6 @@ impl Manager {
|
|||||||
)?;
|
)?;
|
||||||
let stats = self.library.similarity_storage_stats(&profile_id)?;
|
let stats = self.library.similarity_storage_stats(&profile_id)?;
|
||||||
self.update_status(|status| {
|
self.update_status(|status| {
|
||||||
status.phase = Phase::Downloading;
|
|
||||||
status.target_profile = Some(profile_id.clone());
|
status.target_profile = Some(profile_id.clone());
|
||||||
status.model = spec.id.to_string();
|
status.model = spec.id.to_string();
|
||||||
status.total_tracks = stats.total_tracks;
|
status.total_tracks = stats.total_tracks;
|
||||||
@@ -442,21 +480,18 @@ impl Manager {
|
|||||||
status.current_track = None;
|
status.current_track = None;
|
||||||
status.last_error = None;
|
status.last_error = None;
|
||||||
});
|
});
|
||||||
let model_path = self.ensure_model(spec, generation).await?;
|
|
||||||
self.ensure_generation(generation)?;
|
|
||||||
self.update_status(|status| status.phase = Phase::Loading);
|
|
||||||
let model = self.load_model(&profile_id, &model_path).await?;
|
|
||||||
self.ensure_generation(generation)?;
|
|
||||||
|
|
||||||
let mut pending: VecDeque<_> = self.library.pending_similarity_tracks(&profile_id)?.into();
|
let mut pending: VecDeque<_> = self.library.pending_similarity_tracks(&profile_id)?.into();
|
||||||
let pending_total = pending.len();
|
if pending.is_empty() {
|
||||||
self.update_status(|status| {
|
self.ensure_generation(generation)?;
|
||||||
status.phase = if pending_total == 0 {
|
return self.activate_profile(profile_id);
|
||||||
Phase::Loading
|
}
|
||||||
} else {
|
|
||||||
Phase::Processing
|
let model_path = self.ensure_model(spec, generation).await?;
|
||||||
};
|
self.ensure_generation(generation)?;
|
||||||
});
|
let model = self.load_model(&profile_id, &model_path).await?;
|
||||||
|
self.ensure_generation(generation)?;
|
||||||
|
self.update_status(|status| status.phase = Phase::Processing);
|
||||||
|
|
||||||
let mut jobs = tokio::task::JoinSet::new();
|
let mut jobs = tokio::task::JoinSet::new();
|
||||||
while !pending.is_empty() || !jobs.is_empty() {
|
while !pending.is_empty() || !jobs.is_empty() {
|
||||||
@@ -506,6 +541,10 @@ impl Manager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.ensure_generation(generation)?;
|
self.ensure_generation(generation)?;
|
||||||
|
self.activate_profile(profile_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate_profile(&self, profile_id: String) -> Result<()> {
|
||||||
let entries = self.library.load_similarity_index(&profile_id)?;
|
let entries = self.library.load_similarity_index(&profile_id)?;
|
||||||
let total_tracks = self
|
let total_tracks = self
|
||||||
.library
|
.library
|
||||||
@@ -519,7 +558,12 @@ impl Manager {
|
|||||||
profile_id: Some(profile_id.clone()),
|
profile_id: Some(profile_id.clone()),
|
||||||
entries,
|
entries,
|
||||||
};
|
};
|
||||||
lock(&self.settings).active_profile = Some(profile_id.clone());
|
let profile_changed = {
|
||||||
|
let mut settings = lock(&self.settings);
|
||||||
|
let changed = settings.active_profile.as_deref() != Some(&profile_id);
|
||||||
|
settings.active_profile = Some(profile_id.clone());
|
||||||
|
changed
|
||||||
|
};
|
||||||
let stats = self.library.similarity_storage_stats(&profile_id)?;
|
let stats = self.library.similarity_storage_stats(&profile_id)?;
|
||||||
self.update_status(|status| {
|
self.update_status(|status| {
|
||||||
status.phase = Phase::Ready;
|
status.phase = Phase::Ready;
|
||||||
@@ -531,9 +575,11 @@ impl Manager {
|
|||||||
status.stored_bytes = stats.stored_bytes;
|
status.stored_bytes = stats.stored_bytes;
|
||||||
status.current_track = None;
|
status.current_track = None;
|
||||||
});
|
});
|
||||||
let _ = self
|
if profile_changed {
|
||||||
.event_tx
|
let _ = self
|
||||||
.send(AppEvent::SimilarityProfileActivated(Some(profile_id)));
|
.event_tx
|
||||||
|
.send(AppEvent::SimilarityProfileActivated(Some(profile_id)));
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,6 +607,7 @@ impl Manager {
|
|||||||
tokio::fs::remove_file(&path).await?;
|
tokio::fs::remove_file(&path).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.update_status(|status| status.phase = Phase::Downloading);
|
||||||
let response = reqwest::get(spec.url).await?.error_for_status()?;
|
let response = reqwest::get(spec.url).await?.error_for_status()?;
|
||||||
let tmp = path.with_extension(format!("part-{}-{generation}", std::process::id()));
|
let tmp = path.with_extension(format!("part-{}-{generation}", std::process::id()));
|
||||||
let mut file = tokio::fs::File::create(&tmp).await?;
|
let mut file = tokio::fs::File::create(&tmp).await?;
|
||||||
@@ -603,6 +650,7 @@ impl Manager {
|
|||||||
{
|
{
|
||||||
return Ok(Arc::clone(model));
|
return Ok(Arc::clone(model));
|
||||||
}
|
}
|
||||||
|
self.update_status(|status| status.phase = Phase::Loading);
|
||||||
let path = path.to_path_buf();
|
let path = path.to_path_buf();
|
||||||
let model = tokio::task::spawn_blocking(move || load_onnx(&path))
|
let model = tokio::task::spawn_blocking(move || load_onnx(&path))
|
||||||
.await
|
.await
|
||||||
@@ -614,10 +662,13 @@ impl Manager {
|
|||||||
fn update_status(&self, update: impl FnOnce(&mut SimilarityStatus)) {
|
fn update_status(&self, update: impl FnOnce(&mut SimilarityStatus)) {
|
||||||
let snapshot = {
|
let snapshot = {
|
||||||
let mut status = lock(&self.status);
|
let mut status = lock(&self.status);
|
||||||
|
let previous = status.clone();
|
||||||
update(&mut status);
|
update(&mut status);
|
||||||
status.clone()
|
(*status != previous).then(|| status.clone())
|
||||||
};
|
};
|
||||||
let _ = self.event_tx.send(AppEvent::SimilarityStatus(snapshot));
|
if let Some(snapshot) = snapshot {
|
||||||
|
let _ = self.event_tx.send(AppEvent::SimilarityStatus(snapshot));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,6 +1028,14 @@ fn write<T>(lock: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn unique_test_dir(label: &str) -> PathBuf {
|
||||||
|
let unique = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
std::env::temp_dir().join(format!("furumi-{label}-{}-{unique}", std::process::id()))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn profile_fingerprint_changes_with_contract() {
|
fn profile_fingerprint_changes_with_contract() {
|
||||||
let model = &MODELS[0];
|
let model = &MODELS[0];
|
||||||
@@ -1010,6 +1069,43 @@ mod tests {
|
|||||||
assert!(!is_near_duplicate(&distinct, &[&query]));
|
assert!(!is_near_duplicate(&distinct, &[&query]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn repeated_rescans_keep_an_up_to_date_profile_ready() {
|
||||||
|
let directory = unique_test_dir("similarity-stable-status");
|
||||||
|
let library = Arc::new(Library::open(&directory.join("library.db")).unwrap());
|
||||||
|
let profile_id = profile_fingerprint(&MODELS[0], DEFAULT_PROFILE_ID);
|
||||||
|
let settings = SimilaritySettings {
|
||||||
|
enabled: true,
|
||||||
|
active_profile: Some(profile_id),
|
||||||
|
..SimilaritySettings::default()
|
||||||
|
};
|
||||||
|
let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
let manager = Manager::new(Arc::clone(&library), event_tx, settings);
|
||||||
|
|
||||||
|
assert_eq!(manager.status().phase, Phase::Ready);
|
||||||
|
for _ in 0..32 {
|
||||||
|
manager.start();
|
||||||
|
}
|
||||||
|
tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
||||||
|
while manager.pipeline_running.load(Ordering::Acquire) {
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(manager.status().phase, Phase::Ready);
|
||||||
|
while let Ok(event) = event_rx.try_recv() {
|
||||||
|
if let AppEvent::SimilarityStatus(status) = event {
|
||||||
|
assert_eq!(status.phase, Phase::Ready);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(manager);
|
||||||
|
drop(library);
|
||||||
|
std::fs::remove_dir_all(directory).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resampling_keeps_a_constant_signal() {
|
fn resampling_keeps_a_constant_signal() {
|
||||||
let output = resample_sinc(&vec![0.25; 441], 44_100, 16_000);
|
let output = resample_sinc(&vec![0.25; 441], 44_100, 16_000);
|
||||||
|
|||||||
+117
-105
@@ -541,21 +541,20 @@ fn short_id(id: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status_column(frame: &mut Frame, area: Rect, state: &AppState) {
|
fn draw_status_column(frame: &mut Frame, area: Rect, state: &AppState) {
|
||||||
if area.height < 12 {
|
draw_status(frame, area, state);
|
||||||
draw_status(frame, area, state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let [similarity_area, _, federation_area] = Layout::vertical([
|
|
||||||
Constraint::Length(8),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.areas(area);
|
|
||||||
draw_similarity_status(frame, similarity_area, state);
|
|
||||||
draw_status(frame, federation_area, state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_similarity_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
fn draw_similarity_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
||||||
|
draw_summary_card(
|
||||||
|
frame,
|
||||||
|
area,
|
||||||
|
state,
|
||||||
|
" Similarity Processing ",
|
||||||
|
similarity_summary_lines(state),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn similarity_summary_lines(state: &AppState) -> Vec<Line<'static>> {
|
||||||
let status = &state.similarity.status;
|
let status = &state.similarity.status;
|
||||||
let progress = if status.total_tracks == 0 {
|
let progress = if status.total_tracks == 0 {
|
||||||
"0 / 0".to_string()
|
"0 / 0".to_string()
|
||||||
@@ -572,34 +571,28 @@ fn draw_similarity_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.map(short_id)
|
.map(short_id)
|
||||||
.unwrap_or_else(|| "—".to_string());
|
.unwrap_or_else(|| "—".to_string());
|
||||||
draw_summary_card(
|
vec![
|
||||||
frame,
|
summary_line("State", status.phase.label().to_string()),
|
||||||
area,
|
summary_line("Progress", progress),
|
||||||
state,
|
summary_line("Active", active),
|
||||||
" Similarity Processing ",
|
summary_line("Processing", target),
|
||||||
vec![
|
summary_line(
|
||||||
status_line("State", status.phase.label().to_string()),
|
"Stored",
|
||||||
status_line("Progress", progress),
|
format!(
|
||||||
status_line("Active", active),
|
"{} vectors / {}",
|
||||||
status_line("Processing", target),
|
status.stored_vectors,
|
||||||
status_line(
|
short_bytes_label(status.stored_bytes)
|
||||||
"Stored",
|
|
||||||
format!(
|
|
||||||
"{} vectors / {}",
|
|
||||||
status.stored_vectors,
|
|
||||||
short_bytes_label(status.stored_bytes)
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
status_line(
|
),
|
||||||
"Current / errors",
|
summary_line(
|
||||||
status
|
"Current",
|
||||||
.current_track
|
status
|
||||||
.clone()
|
.current_track
|
||||||
.or_else(|| status.last_error.clone())
|
.clone()
|
||||||
.unwrap_or_else(|| format!("{} errors", status.failed_tracks)),
|
.or_else(|| status.last_error.clone())
|
||||||
),
|
.unwrap_or_else(|| format!("{} errors", status.failed_tracks)),
|
||||||
],
|
),
|
||||||
);
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
||||||
@@ -616,69 +609,81 @@ fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if area.width >= 60 && area.height >= 20 {
|
if area.width >= 60 {
|
||||||
let protocols_height =
|
let paired_width = area.width.saturating_sub(1) / 2;
|
||||||
protocol_card_height(state, area.width.saturating_sub(2), area.height);
|
let paired_height =
|
||||||
let [top_area, _, bottom_area, _, protocols_area, _] = Layout::vertical([
|
protocol_card_height(state, paired_width.saturating_sub(2), area.height).max(8);
|
||||||
Constraint::Length(7),
|
if area.height >= 16 + paired_height {
|
||||||
Constraint::Length(1),
|
let [top_area, _, middle_area, _, paired_area, _] = Layout::vertical([
|
||||||
Constraint::Length(7),
|
Constraint::Length(7),
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
Constraint::Length(protocols_height),
|
Constraint::Length(7),
|
||||||
Constraint::Min(0),
|
Constraint::Length(1),
|
||||||
])
|
Constraint::Length(paired_height),
|
||||||
.areas(area);
|
Constraint::Min(0),
|
||||||
let [status_area, _, local_area] = Layout::horizontal([
|
])
|
||||||
Constraint::Percentage(50),
|
.areas(area);
|
||||||
Constraint::Length(1),
|
let [status_area, _, local_area] = Layout::horizontal([
|
||||||
Constraint::Percentage(50),
|
Constraint::Percentage(50),
|
||||||
])
|
Constraint::Length(1),
|
||||||
.areas(top_area);
|
Constraint::Percentage(50),
|
||||||
let [transport_area, _, devices_area] = Layout::horizontal([
|
])
|
||||||
Constraint::Percentage(50),
|
.areas(top_area);
|
||||||
Constraint::Length(1),
|
let [transport_area, _, devices_area] = Layout::horizontal([
|
||||||
Constraint::Percentage(50),
|
Constraint::Percentage(50),
|
||||||
])
|
Constraint::Length(1),
|
||||||
.areas(bottom_area);
|
Constraint::Percentage(50),
|
||||||
draw_summary_card(
|
])
|
||||||
frame,
|
.areas(middle_area);
|
||||||
status_area,
|
let [similarity_area, _, protocols_area] = Layout::horizontal([
|
||||||
state,
|
Constraint::Percentage(50),
|
||||||
" Status ",
|
Constraint::Length(1),
|
||||||
node_summary_lines(state),
|
Constraint::Percentage(50),
|
||||||
);
|
])
|
||||||
draw_summary_card(
|
.areas(paired_area);
|
||||||
frame,
|
draw_summary_card(
|
||||||
local_area,
|
frame,
|
||||||
state,
|
status_area,
|
||||||
" Local Data ",
|
state,
|
||||||
local_data_summary_lines(state),
|
" Status ",
|
||||||
);
|
node_summary_lines(state),
|
||||||
draw_summary_card(
|
);
|
||||||
frame,
|
draw_summary_card(
|
||||||
transport_area,
|
frame,
|
||||||
state,
|
local_area,
|
||||||
" Iroh Transport ",
|
state,
|
||||||
transport_summary_lines(state),
|
" Local Data ",
|
||||||
);
|
local_data_summary_lines(state),
|
||||||
draw_summary_card(
|
);
|
||||||
frame,
|
draw_summary_card(
|
||||||
devices_area,
|
frame,
|
||||||
state,
|
transport_area,
|
||||||
" Connected Devices ",
|
state,
|
||||||
device_summary_lines(state),
|
" Iroh Transport ",
|
||||||
);
|
transport_summary_lines(state),
|
||||||
draw_summary_card(
|
);
|
||||||
frame,
|
draw_summary_card(
|
||||||
protocols_area,
|
frame,
|
||||||
state,
|
devices_area,
|
||||||
" Protocol Versions ",
|
state,
|
||||||
protocol_summary_lines(state, protocols_area.width.saturating_sub(2)),
|
" Connected Devices ",
|
||||||
);
|
device_summary_lines(state),
|
||||||
return;
|
);
|
||||||
|
draw_similarity_status(frame, similarity_area, state);
|
||||||
|
draw_summary_card(
|
||||||
|
frame,
|
||||||
|
protocols_area,
|
||||||
|
state,
|
||||||
|
" Protocol Versions ",
|
||||||
|
protocol_summary_lines(state, protocols_area.width.saturating_sub(2)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if area.height < 39 {
|
let protocols_height = protocol_card_height(state, area.width.saturating_sub(2), area.height);
|
||||||
|
let required_vertical_height = 41u16.saturating_add(protocols_height);
|
||||||
|
if area.height < required_vertical_height {
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(compact_status_lines(state))
|
Paragraph::new(compact_status_lines(state))
|
||||||
.wrap(ratatui::widgets::Wrap { trim: false }),
|
.wrap(ratatui::widgets::Wrap { trim: false }),
|
||||||
@@ -696,6 +701,8 @@ fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
|||||||
_,
|
_,
|
||||||
local_area,
|
local_area,
|
||||||
_,
|
_,
|
||||||
|
similarity_area,
|
||||||
|
_,
|
||||||
protocols_area,
|
protocols_area,
|
||||||
_,
|
_,
|
||||||
] = Layout::vertical([
|
] = Layout::vertical([
|
||||||
@@ -707,11 +714,9 @@ fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
|||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
Constraint::Length(7),
|
Constraint::Length(7),
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
Constraint::Length(protocol_card_height(
|
Constraint::Length(8),
|
||||||
state,
|
Constraint::Length(1),
|
||||||
area.width.saturating_sub(2),
|
Constraint::Length(protocols_height),
|
||||||
area.height,
|
|
||||||
)),
|
|
||||||
Constraint::Min(0),
|
Constraint::Min(0),
|
||||||
])
|
])
|
||||||
.areas(area);
|
.areas(area);
|
||||||
@@ -744,6 +749,7 @@ fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
|||||||
" Local Data ",
|
" Local Data ",
|
||||||
local_data_summary_lines(state),
|
local_data_summary_lines(state),
|
||||||
);
|
);
|
||||||
|
draw_similarity_status(frame, similarity_area, state);
|
||||||
draw_summary_card(
|
draw_summary_card(
|
||||||
frame,
|
frame,
|
||||||
protocols_area,
|
protocols_area,
|
||||||
@@ -767,6 +773,12 @@ fn compact_status_lines(state: &AppState) -> Vec<Line<'static>> {
|
|||||||
lines.push(Line::styled("Connected Devices", theme::header_for(state)));
|
lines.push(Line::styled("Connected Devices", theme::header_for(state)));
|
||||||
lines.extend(device_summary_lines(state).into_iter().take(2));
|
lines.extend(device_summary_lines(state).into_iter().take(2));
|
||||||
lines.push(Line::default());
|
lines.push(Line::default());
|
||||||
|
lines.push(Line::styled(
|
||||||
|
"Similarity Processing",
|
||||||
|
theme::header_for(state),
|
||||||
|
));
|
||||||
|
lines.extend(similarity_summary_lines(state).into_iter().take(3));
|
||||||
|
lines.push(Line::default());
|
||||||
lines.push(Line::styled("Protocol Versions", theme::header_for(state)));
|
lines.push(Line::styled("Protocol Versions", theme::header_for(state)));
|
||||||
lines.extend(protocol_summary_lines(state, 0).into_iter().take(3));
|
lines.extend(protocol_summary_lines(state, 0).into_iter().take(3));
|
||||||
lines
|
lines
|
||||||
|
|||||||
Reference in New Issue
Block a user