Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove wait-time when opening settings panel #8464

Merged
merged 8 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6512,6 +6512,7 @@ dependencies = [
"js-sys",
"once_cell",
"parking_lot",
"poll-promise",
"re_build_info",
"re_build_tools",
"re_log",
Expand Down
1 change: 1 addition & 0 deletions crates/utils/re_video/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ econtext.workspace = true
itertools.workspace = true
once_cell.workspace = true
parking_lot.workspace = true
poll-promise.workspace = true
re_mp4.workspace = true
thiserror.workspace = true

Expand Down
2 changes: 1 addition & 1 deletion crates/utils/re_video/src/decode/ffmpeg_h264/ffmpeg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ impl FFmpegCliH264Decoder {

// Check the version once ahead of running FFmpeg.
// The error is still handled if it happens while running FFmpeg, but it's a bit unclear if we can get it to start in the first place then.
match FFmpegVersion::for_executable(ffmpeg_path.as_deref()) {
match FFmpegVersion::for_executable_blocking(ffmpeg_path.as_deref()) {
Ok(version) => {
if !version.is_compatible() {
return Err(Error::UnsupportedFFmpegVersion {
Expand Down
143 changes: 91 additions & 52 deletions crates/utils/re_video/src/decode/ffmpeg_h264/version.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use std::{collections::HashMap, path::PathBuf};
use std::{collections::HashMap, path::PathBuf, task::Poll};

use once_cell::sync::Lazy;
use parking_lot::Mutex;
use poll_promise::Promise;

// FFmpeg 5.1 "Riemann" is from 2022-07-22.
// It's simply the oldest I tested manually as of writing. We might be able to go lower.
// However, we also know that FFmpeg 4.4 is already no longer working.
pub const FFMPEG_MINIMUM_VERSION_MAJOR: u32 = 5;
pub const FFMPEG_MINIMUM_VERSION_MINOR: u32 = 1;

pub type FfmpegVersionResult = Result<FFmpegVersion, FFmpegVersionParseError>;

/// A successfully parsed `FFmpeg` version.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FFmpegVersion {
Expand Down Expand Up @@ -78,61 +81,31 @@ impl FFmpegVersion {
/// version string. Since version strings can get pretty wild, we don't want to fail in this case.
///
/// Internally caches the result per path together with its modification time to re-run/parse the version only if the file has changed.
pub fn for_executable(path: Option<&std::path::Path>) -> Result<Self, FFmpegVersionParseError> {
type VersionMap = HashMap<
PathBuf,
(
Option<std::time::SystemTime>,
Result<FFmpegVersion, FFmpegVersionParseError>,
),
>;
static CACHE: Lazy<Mutex<VersionMap>> = Lazy::new(|| Mutex::new(HashMap::new()));

pub fn for_executable_poll(path: Option<&std::path::Path>) -> Poll<FfmpegVersionResult> {
re_tracing::profile_function!();

// Retrieve file modification time first.
let modification_time = if let Some(path) = path {
path.metadata()
.map_err(|err| {
FFmpegVersionParseError::RetrieveFileModificationTime(err.to_string())
})?
.modified()
.ok()
} else {
None
};

// Check first if we already have the version cached.
let mut cache = CACHE.lock();
let cache_key = path.unwrap_or(std::path::Path::new("ffmpeg"));
if let Some(cached) = cache.get(cache_key) {
if modification_time == cached.0 {
return cached.1.clone();
}
}

// Run FFmpeg (or whatever was passed to us) to get the version.
let raw_version = if let Some(path) = path {
ffmpeg_sidecar::version::ffmpeg_version_with_path(path)
} else {
ffmpeg_sidecar::version::ffmpeg_version()
}
.map_err(|err| FFmpegVersionParseError::RunFFmpeg(err.to_string()))?;

let version = if let Some(version) = Self::parse(&raw_version) {
Ok(version)
} else {
Err(FFmpegVersionParseError::ParseVersion {
raw_version: raw_version.clone(),
})
};
let modification_time = file_modification_time(path)?;
VersionCache::global(|cache| {
cache
.version(path, modification_time)
.poll()
.map(|r| r.clone())
})
}

cache.insert(
cache_key.to_path_buf(),
(modification_time, version.clone()),
);
/// Like [`Self::for_executable_poll`], but blocks until the version is ready.
///
/// WARNING: this blocks for half a second on Mac, maybe more on other platforms.
pub fn for_executable_blocking(path: Option<&std::path::Path>) -> FfmpegVersionResult {
teh-cmc marked this conversation as resolved.
Show resolved Hide resolved
re_tracing::profile_function!();

version
let modification_time = file_modification_time(path)?;
VersionCache::global(|cache| {
cache
.version(path, modification_time)
.block_until_ready()
.clone()
})
}

/// Returns true if this version is compatible with Rerun's minimum requirements.
Expand All @@ -143,6 +116,72 @@ impl FFmpegVersion {
}
}

fn file_modification_time(
path: Option<&std::path::Path>,
) -> Result<Option<std::time::SystemTime>, FFmpegVersionParseError> {
Ok(if let Some(path) = path {
path.metadata()
.map_err(|err| FFmpegVersionParseError::RetrieveFileModificationTime(err.to_string()))?
.modified()
.ok()
} else {
None
})
}

#[derive(Default)]
struct VersionCache(
HashMap<PathBuf, (Option<std::time::SystemTime>, Promise<FfmpegVersionResult>)>,
);

impl VersionCache {
fn global<R>(f: impl FnOnce(&mut Self) -> R) -> R {
static CACHE: Lazy<Mutex<VersionCache>> = Lazy::new(|| Mutex::new(VersionCache::default()));
f(&mut CACHE.lock())
}

fn version(
&mut self,
path: Option<&std::path::Path>,
modification_time: Option<std::time::SystemTime>,
) -> &Promise<FfmpegVersionResult> {
let Self(cache) = self;

let cache_key = path.unwrap_or(std::path::Path::new("ffmpeg")).to_path_buf();

match cache.entry(cache_key) {
std::collections::hash_map::Entry::Occupied(entry) => &entry.into_mut().1,
std::collections::hash_map::Entry::Vacant(entry) => {
let path = path.map(|path| path.to_path_buf());
let version =
Promise::spawn_thread("ffmpeg_version", move || ffmpeg_version(path.as_ref()));
&entry.insert((modification_time, version)).1
}
}
}
}

fn ffmpeg_version(
path: Option<&std::path::PathBuf>,
) -> Result<FFmpegVersion, FFmpegVersionParseError> {
re_tracing::profile_function!("ffmpeg_version_with_path");

let raw_version = if let Some(path) = path {
ffmpeg_sidecar::version::ffmpeg_version_with_path(path)
} else {
ffmpeg_sidecar::version::ffmpeg_version()
}
.map_err(|err| FFmpegVersionParseError::RunFFmpeg(err.to_string()))?;

if let Some(version) = FFmpegVersion::parse(&raw_version) {
Ok(version)
} else {
Err(FFmpegVersionParseError::ParseVersion {
raw_version: raw_version.clone(),
})
}
}

#[cfg(test)]
mod tests {
use crate::decode::ffmpeg_h264::FFmpegVersion;
Expand Down
13 changes: 9 additions & 4 deletions crates/viewer/re_viewer/src/ui/settings_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ fn video_section_ui(ui: &mut Ui, app_options: &mut AppOptions) {
#[cfg(not(target_arch = "wasm32"))]
fn ffmpeg_path_status_ui(ui: &mut Ui, app_options: &AppOptions) {
use re_video::decode::{FFmpegVersion, FFmpegVersionParseError};
use std::task::Poll;

let path = app_options
.video_decoder_override_ffmpeg_path
Expand All @@ -211,25 +212,29 @@ fn ffmpeg_path_status_ui(ui: &mut Ui, app_options: &AppOptions) {
if path.is_some_and(|path| !path.is_file()) {
ui.error_label("The specified FFmpeg binary path does not exist or is not a file.");
} else {
let res = FFmpegVersion::for_executable(path);
let res = FFmpegVersion::for_executable_poll(path);

match res {
Ok(version) => {
Poll::Pending => {
ui.spinner();
}

Poll::Ready(Ok(version)) => {
if version.is_compatible() {
ui.success_label(format!("FFmpeg found (version {version})"));
} else {
ui.error_label(format!("Incompatible FFmpeg version: {version}"));
}
}
Err(FFmpegVersionParseError::ParseVersion { raw_version }) => {
Poll::Ready(Err(FFmpegVersionParseError::ParseVersion { raw_version })) => {
// We make this one a warning instead of an error because version parsing is flaky, and
// it might end up still working.
ui.warning_label(format!(
"FFmpeg binary found but unable to parse version: {raw_version}"
));
}

Err(err) => {
Poll::Ready(Err(err)) => {
ui.error_label(format!("Unable to check FFmpeg version: {err}"));
}
}
Expand Down
Loading