use std::fmt::Write as _;
use egui::NumExt as _;
use jiff::SignedDuration;
use jiff::fmt::friendly::{FractionalUnit, SpanPrinter};
use re_chunk_store::ChunkStoreConfig;
use re_entity_db::{EntityDb, entity_db::RedapConnectionState};
use re_format::{format_bytes, format_uint};
use re_log_channel::LogSource;
use re_log_types::StoreKind;
use re_ui::UiExt as _;
use re_viewer_context::{AppContext, UiLayout};
use crate::item_ui::{app_id_button_ui, data_source_button_ui};
impl crate::AppUi for EntityDb {
fn app_ui(&self, ctx: &AppContext<'_>, ui: &mut egui::Ui, ui_layout: UiLayout) {
re_tracing::profile_function!();
if ui_layout.is_single_line() {
let mut string = self.store_id().recording_id().to_string();
if let Some(data_source) = &self.data_source {
write!(string, ", {data_source}").ok();
}
write!(string, ", {}", self.store_id().application_id()).ok();
ui.label(string);
return;
}
egui::Grid::new("entity_db").num_columns(2).show(ui, |ui| {
grid_content_ui(ctx, self, ui, ui_layout);
});
let hub = ctx.store_hub();
match self.store_kind() {
StoreKind::Recording => {}
StoreKind::Blueprint => {
if let Some(active_app_id) = ctx.active_store_context.map(|sc| sc.application_id())
{
let is_active_app_id = self.application_id() == active_app_id;
if is_active_app_id {
let is_default = hub.default_blueprint_id_for_app(active_app_id)
== Some(self.store_id());
let is_active =
hub.active_blueprint_id_for_app(active_app_id) == Some(self.store_id());
match (is_default, is_active) {
(false, false) => {}
(true, false) => {
ui.add_space(8.0);
ui.label(
"This is the default blueprint for the current application.",
);
if let Some(active_blueprint) =
hub.active_blueprint_for_app(active_app_id)
&& active_blueprint.cloned_from() == Some(self.store_id())
{
if self.latest_row_id() == active_blueprint.latest_row_id() {
ui.label(
"The active blueprint is a clone of this blueprint.",
);
} else {
ui.label("The active blueprint is a modified clone of this blueprint.");
}
}
}
(false, true) => {
ui.add_space(8.0);
ui.label(format!("This is the active blueprint for the current application, '{active_app_id}'"));
}
(true, true) => {
ui.add_space(8.0);
ui.label(format!("This is both the active and default blueprint for the current application, '{active_app_id}'"));
}
}
} else {
ui.add_space(8.0);
ui.label("This blueprint is not for the active application");
}
}
}
}
if cfg!(debug_assertions) && !ctx.is_test {
ui.collapsing_header("Debug info", true, |ui| {
debug_ui(ui, self);
});
}
}
}
fn grid_content_ui(ctx: &AppContext<'_>, db: &EntityDb, ui: &mut egui::Ui, ui_layout: UiLayout) {
re_tracing::profile_function!();
{
ui.grid_left_hand_label(&format!("{} ID", db.store_id().kind()));
ui.label(db.store_id().recording_id().to_string());
ui.end_row();
}
if let Some(LogSource::RedapGrpcStream {
uri: re_uri::DatasetSegmentUri { segment_id, .. },
..
}) = &db.data_source
{
ui.grid_left_hand_label("Segment ID");
ui.label(segment_id);
ui.end_row();
}
if let Some(store_info) = db.store_info()
&& ui_layout.is_selection_panel()
{
let re_log_types::StoreInfo {
store_id,
cloned_from,
store_source,
store_version,
} = store_info;
if let Some(cloned_from) = cloned_from {
ui.grid_left_hand_label("Clone of");
crate::item_ui::store_id_button_ui(ctx, ui, cloned_from, ui_layout);
ui.end_row();
}
ui.grid_left_hand_label("Application ID");
app_id_button_ui(ctx, ui, store_id.application_id());
ui.end_row();
ui.grid_left_hand_label("Source");
ui.label(store_source.to_string());
ui.end_row();
if let Some(store_version) = store_version {
ui.grid_left_hand_label("Source RRD version");
ui.label(store_version.to_string());
ui.end_row();
} else {
re_log::trace_once!("store version is undefined for this recording, this is a bug");
}
ui.grid_left_hand_label("Kind");
ui.label(store_id.kind().to_string());
ui.end_row();
}
let show_last_modified_time = !ctx.is_test;
if show_last_modified_time
&& let Some(latest_row_id) = db.latest_row_id()
&& let Ok(nanos_since_epoch) = i64::try_from(latest_row_id.nanos_since_epoch())
{
let time = re_log_types::Timestamp::from_nanos_since_epoch(nanos_since_epoch);
ui.grid_left_hand_label("Modified");
ui.label(time.format(ctx.app_options.timestamp_format));
ui.end_row();
}
if let Some(tl_name) = db
.timelines()
.keys()
.find(|k| **k == re_log_types::TimelineName::log_time())
&& let Some(range) = db.time_range_for(tl_name)
&& let delta_ns = (range.max() - range.min()).as_i64()
&& delta_ns > 0
{
let duration = SignedDuration::from_nanos(delta_ns);
let printer = SpanPrinter::new()
.fractional(Some(FractionalUnit::Second))
.precision(Some(2));
let pretty = printer.duration_to_string(&duration);
ui.grid_left_hand_label("Duration");
ui.label(pretty)
.on_hover_text("Duration between earliest and latest log_time.");
ui.end_row();
}
{
ui.grid_left_hand_label("Size");
let current_size_bytes = db.byte_size_of_physical_chunks();
let full_size_bytes = if db.rrd_manifest_index().has_manifest() {
db.rrd_manifest_index()
.full_uncompressed_size()
.at_least(current_size_bytes)
} else {
current_size_bytes
};
ui.label(format_bytes(full_size_bytes as _)).on_hover_text(
"Approximate size in RAM (decompressed).\n\
If you hover an entity in the streams view (bottom panel) you can see the \
size of individual entities.",
);
ui.end_row();
if db.rrd_manifest_index().has_manifest() {
ui.grid_left_hand_label("Downloaded");
let memory_limit = ctx.app_options.memory_limit;
let max_downloaded_bytes = if db.rrd_manifest_index().is_fully_loaded() {
full_size_bytes
} else {
u64::min(full_size_bytes, memory_limit.as_bytes())
};
let current_size = format_bytes(current_size_bytes as _);
let max_downloaded = format_bytes(max_downloaded_bytes as _);
ui.horizontal(|ui| {
let mut num_root_chunks = 0_usize;
let mut num_fully_loaded = 0_usize;
for info in db.rrd_manifest_index().root_chunks() {
num_root_chunks += 1;
if info.is_fully_loaded() {
num_fully_loaded += 1;
}
}
if db.redap_connection_state() == RedapConnectionState::PartialManifest {
ui.label(format!("{current_size} / ?"));
ui.label(format!("({} / ? chunks)", format_uint(num_fully_loaded)));
ui.end_row();
} else if num_fully_loaded == num_root_chunks {
ui.label("100%");
} else {
ui.label(format!("{current_size} / {max_downloaded}"));
if max_downloaded_bytes < full_size_bytes {
let rect =
ui.small_icon(&re_ui::icons::INFO, Some(ui.visuals().text_color()));
ui.allocate_rect(rect, egui::Sense::hover())
.on_hover_text(format!(
"Download limited to {memory_limit} memory budget"
));
}
ui.label(format!(
"({} / {} chunks)",
format_uint(num_fully_loaded),
format_uint(num_root_chunks)
));
ui.end_row();
}
});
ui.end_row();
}
}
{
let storage_engine = db.storage_engine();
let store = storage_engine.store();
let schema = store.schema().chunk_column_descriptors();
ui.grid_left_hand_label("Entities")
.on_hover_text("In the ChunkStore");
ui.label(re_format::format_uint(store.all_entities().len()));
ui.end_row();
ui.grid_left_hand_label("Timeline columns");
ui.label(re_format::format_uint(schema.indices.len()));
ui.end_row();
ui.grid_left_hand_label("Data columns");
ui.label(re_format::format_uint(schema.components.len()));
ui.end_row();
ui.grid_left_hand_label("Rows");
ui.label(re_format::format_uint(store.stats().total().num_rows));
ui.end_row();
}
if ui_layout.is_selection_panel() {
let &ChunkStoreConfig {
enable_changelog: _,
chunk_max_bytes,
chunk_max_rows,
chunk_max_rows_if_unsorted,
} = db.storage_engine().store().config();
ui.grid_left_hand_label("Compaction");
ui.label(format!(
"{} rows ({} if unsorted) or {}",
re_format::format_uint(chunk_max_rows),
re_format::format_uint(chunk_max_rows_if_unsorted),
re_format::format_bytes(chunk_max_bytes as _),
))
.on_hover_text(
unindent::unindent(&format!("\
The current compaction configuration for this recording is to merge chunks until they \
reach either a maximum of {chunk_max_rows} rows ({chunk_max_rows_if_unsorted} if unsorted) or {chunk_max_bytes}, whichever comes first.
The viewer compacts chunks together as they come in, in order to find the right \
balance between space and compute overhead.
This is not to be confused with the SDK's batcher, which does a similar job, with \
different goals and constraints, on the logging side (SDK).
These two functions (SDK batcher & viewer compactor) complement each other.
Higher thresholds generally translate to better space overhead, but require more compute \
for both ingestion and queries.
Lower thresholds generally translate to worse space overhead, but faster ingestion times
and more responsive queries.
This is a broad oversimplification -- use the defaults if unsure, they fit most workfloads well.
To modify the current configuration, set these environment variables before starting the viewer:
* {ENV_CHUNK_MAX_ROWS}
* {ENV_CHUNK_MAX_ROWS_IF_UNSORTED}
* {ENV_CHUNK_MAX_BYTES}
This compaction process is an ephemeral, in-memory optimization of the Rerun viewer.\
It will not modify the recording itself: use the `Save` command of the viewer, or the \
`rerun rrd optimize` CLI tool if you wish to persist the compacted results, which will \
make future runs cheaper.
",
chunk_max_rows = re_format::format_uint(chunk_max_rows),
chunk_max_rows_if_unsorted = re_format::format_uint(chunk_max_rows_if_unsorted),
chunk_max_bytes = re_format::format_bytes(chunk_max_bytes as _),
ENV_CHUNK_MAX_ROWS = ChunkStoreConfig::ENV_CHUNK_MAX_ROWS,
ENV_CHUNK_MAX_ROWS_IF_UNSORTED = ChunkStoreConfig::ENV_CHUNK_MAX_ROWS_IF_UNSORTED,
ENV_CHUNK_MAX_BYTES = ChunkStoreConfig::ENV_CHUNK_MAX_BYTES,
)),
);
ui.end_row();
}
if let Some(data_source) = &db.data_source
&& ui_layout.is_selection_panel()
{
ui.grid_left_hand_label("Data source");
data_source_button_ui(ctx, ui, data_source);
ui.end_row();
}
}
fn debug_ui(ui: &mut egui::Ui, db: &EntityDb) {
ui.weak("(only visible in debug builds)");
egui::Grid::new("debug-info").show(ui, |ui| {
if let Some(manifest) = db.rrd_manifest_index().manifest() {
ui.label("Entities");
ui.label(format_uint(
manifest.recording_schema().all_entities().len(),
));
ui.end_row();
}
ui.label("is_buffering");
ui.label(db.is_buffering().to_string());
ui.end_row();
ui.label("Connection");
ui.label(format!("{:?}", db.redap_connection_state()));
ui.end_row();
ui.label("Physical chunks");
ui.label(format_bytes(db.byte_size_of_physical_chunks() as _));
ui.end_row();
});
}