use std::collections::{BTreeMap, HashMap, VecDeque};
use std::num::NonZeroU64;
use std::sync::atomic::AtomicU64;
use anyhow::{anyhow, ensure};
use arrow2::array::{Array, Int64Array, UInt64Array};
use arrow2::datatypes::{DataType, TimeUnit};
use nohash_hasher::{IntMap, IntSet};
use parking_lot::RwLock;
use re_format::{arrow, format_bytes, format_number};
use re_log_types::{
ComponentName, EntityPath, EntityPathHash, MsgId, TimeInt, TimePoint, TimeRange, Timeline,
};
pub type TimeIndex = Vec<i64>;
pub type SecondaryIndex = Vec<Option<RowIndex>>;
static_assertions::assert_eq_size!(u64, Option<RowIndex>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u64)]
pub enum RowIndexKind {
Temporal = 0,
Timeless = 1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct RowIndex(pub(crate) NonZeroU64);
impl RowIndex {
const KIND_MASK: u64 = 0x8000_0000_0000_0000;
pub(crate) fn from_u63(kind: RowIndexKind, v: u64) -> Self {
debug_assert!(v & Self::KIND_MASK == 0);
let v = v | ((kind as u64) << 63);
Self(v.try_into().unwrap())
}
pub(crate) fn as_u64(self) -> u64 {
self.0.get() & !Self::KIND_MASK
}
pub(crate) fn kind(self) -> RowIndexKind {
match self.0.get() & Self::KIND_MASK > 0 {
false => RowIndexKind::Temporal,
true => RowIndexKind::Timeless,
}
}
}
impl std::fmt::Display for RowIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.kind() {
RowIndexKind::Temporal => f.write_fmt(format_args!("Temporal({})", self.0)),
RowIndexKind::Timeless => f.write_fmt(format_args!("Timeless({})", self.0)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct IndexRowNr(pub(crate) u64);
impl std::fmt::Display for IndexRowNr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.0))
}
}
#[derive(Debug, Clone)]
pub struct DataStoreConfig {
pub component_bucket_size_bytes: u64,
pub component_bucket_nb_rows: u64,
pub index_bucket_size_bytes: u64,
pub index_bucket_nb_rows: u64,
pub store_insert_ids: bool,
}
impl Default for DataStoreConfig {
fn default() -> Self {
Self::DEFAULT
}
}
impl DataStoreConfig {
pub const DEFAULT: Self = Self {
component_bucket_size_bytes: 32 * 1024 * 1024, component_bucket_nb_rows: u64::MAX,
index_bucket_size_bytes: 32 * 1024, index_bucket_nb_rows: 1024,
store_insert_ids: cfg!(debug_assertions),
};
}
pub struct DataStore {
pub(crate) cluster_key: ComponentName,
pub(crate) config: DataStoreConfig,
pub(crate) messages: BTreeMap<MsgId, TimePoint>,
pub(crate) cluster_comp_cache: IntMap<usize, RowIndex>,
pub(crate) timeless_indices: IntMap<EntityPathHash, PersistentIndexTable>,
pub(crate) timeless_components: IntMap<ComponentName, PersistentComponentTable>,
pub(crate) indices: HashMap<(Timeline, EntityPathHash), IndexTable>,
pub(crate) components: IntMap<ComponentName, ComponentTable>,
pub(crate) insert_id: u64,
pub(crate) query_id: AtomicU64,
pub(crate) gc_id: u64,
}
impl DataStore {
pub fn new(cluster_key: ComponentName, config: DataStoreConfig) -> Self {
Self {
cluster_key,
config,
cluster_comp_cache: Default::default(),
messages: Default::default(),
indices: Default::default(),
components: Default::default(),
timeless_indices: Default::default(),
timeless_components: Default::default(),
insert_id: 0,
query_id: AtomicU64::new(0),
gc_id: 0,
}
}
pub fn insert_id_key() -> ComponentName {
"rerun.insert_id".into()
}
pub fn cluster_key(&self) -> ComponentName {
self.cluster_key
}
pub fn lookup_data_type(&self, component: &ComponentName) -> Option<&DataType> {
self.components.get(component).map(|c| &c.datatype)
}
pub fn sanity_check(&self) -> anyhow::Result<()> {
crate::profile_function!();
if self.gc_id == 0 {
let mut row_indices: IntMap<_, Vec<u64>> = IntMap::default();
for table in self.indices.values() {
for bucket in table.buckets.values() {
for (comp, index) in &bucket.indices.read().indices {
let row_indices = row_indices.entry(*comp).or_default();
row_indices.extend(index.iter().flatten().map(|row_idx| row_idx.as_u64()));
}
}
}
for (comp, mut row_indices) in row_indices {
if comp == DataStore::insert_id_key() {
continue;
}
row_indices.sort();
row_indices.dedup();
for pair in row_indices.windows(2) {
let &[i1, i2] = pair else { unreachable!() };
ensure!(
i1 + 1 == i2,
"found hole in index coverage for {comp:?}: \
in {row_indices:?}, {i1} -> {i2}"
);
}
}
}
{
let mut row_indices: IntMap<_, Vec<u64>> = IntMap::default();
for table in self.timeless_indices.values() {
for (comp, index) in &table.indices {
let row_indices = row_indices.entry(*comp).or_default();
row_indices.extend(index.iter().flatten().map(|row_idx| row_idx.as_u64()));
}
}
for (comp, mut row_indices) in row_indices {
if comp == DataStore::insert_id_key() {
continue;
}
row_indices.sort();
row_indices.dedup();
for pair in row_indices.windows(2) {
let &[i1, i2] = pair else { unreachable!() };
ensure!(
i1 + 1 == i2,
"found hole in timeless index coverage for {comp:?}: \
in {row_indices:?}, {i1} -> {i2}"
);
}
}
}
for table in self.timeless_indices.values() {
table.sanity_check()?;
}
for table in self.timeless_components.values() {
table.sanity_check()?;
}
for table in self.indices.values() {
table.sanity_check()?;
}
for table in self.components.values() {
table.sanity_check()?;
}
Ok(())
}
pub fn oldest_time_per_timeline(&self) -> BTreeMap<Timeline, TimeInt> {
crate::profile_function!();
let mut oldest_time_per_timeline = BTreeMap::default();
for component_table in self.components.values() {
for bucket in &component_table.buckets {
for (timeline, time_range) in &bucket.time_ranges {
let entry = oldest_time_per_timeline
.entry(*timeline)
.or_insert(TimeInt::MAX);
*entry = time_range.min.min(*entry);
}
}
}
oldest_time_per_timeline
}
}
impl std::fmt::Display for DataStore {
#[allow(clippy::string_add)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
cluster_key,
config,
cluster_comp_cache: _,
messages: _,
indices,
components,
timeless_indices,
timeless_components,
insert_id: _,
query_id: _,
gc_id: _,
} = self;
f.write_str("DataStore {\n")?;
f.write_str(&indent::indent_all_by(
4,
format!("cluster_key: {cluster_key:?}\n"),
))?;
f.write_str(&indent::indent_all_by(4, format!("config: {config:?}\n")))?;
{
f.write_str(&indent::indent_all_by(
4,
format!(
"{} timeless index tables, for a total of {} across {} total rows\n",
timeless_indices.len(),
format_bytes(self.total_timeless_index_size_bytes() as _),
format_number(self.total_timeless_index_rows() as _)
),
))?;
f.write_str(&indent::indent_all_by(4, "timeless_indices: [\n"))?;
for table in timeless_indices.values() {
f.write_str(&indent::indent_all_by(8, "PersistentIndexTable {\n"))?;
f.write_str(&indent::indent_all_by(12, table.to_string() + "\n"))?;
f.write_str(&indent::indent_all_by(8, "}\n"))?;
}
f.write_str(&indent::indent_all_by(4, "]\n"))?;
}
{
f.write_str(&indent::indent_all_by(
4,
format!(
"{} persistent component tables, for a total of {} across {} total rows\n",
timeless_components.len(),
format_bytes(self.total_timeless_component_size_bytes() as _),
format_number(self.total_timeless_component_rows() as _)
),
))?;
f.write_str(&indent::indent_all_by(4, "timeless_components: [\n"))?;
for table in timeless_components.values() {
f.write_str(&indent::indent_all_by(8, "PersistentComponentTable {\n"))?;
f.write_str(&indent::indent_all_by(12, table.to_string() + "\n"))?;
f.write_str(&indent::indent_all_by(8, "}\n"))?;
}
f.write_str(&indent::indent_all_by(4, "]\n"))?;
}
{
f.write_str(&indent::indent_all_by(
4,
format!(
"{} index tables, for a total of {} across {} total rows\n",
indices.len(),
format_bytes(self.total_temporal_index_size_bytes() as _),
format_number(self.total_temporal_index_rows() as _)
),
))?;
f.write_str(&indent::indent_all_by(4, "indices: [\n"))?;
for table in indices.values() {
f.write_str(&indent::indent_all_by(8, "IndexTable {\n"))?;
f.write_str(&indent::indent_all_by(12, table.to_string() + "\n"))?;
f.write_str(&indent::indent_all_by(8, "}\n"))?;
}
f.write_str(&indent::indent_all_by(4, "]\n"))?;
}
{
f.write_str(&indent::indent_all_by(
4,
format!(
"{} component tables, for a total of {} across {} total rows\n",
components.len(),
format_bytes(self.total_temporal_component_size_bytes() as _),
format_number(self.total_temporal_component_rows() as _)
),
))?;
f.write_str(&indent::indent_all_by(4, "components: [\n"))?;
for table in components.values() {
f.write_str(&indent::indent_all_by(8, "ComponentTable {\n"))?;
f.write_str(&indent::indent_all_by(12, table.to_string() + "\n"))?;
f.write_str(&indent::indent_all_by(8, "}\n"))?;
}
f.write_str(&indent::indent_all_by(4, "]\n"))?;
}
f.write_str("}")?;
Ok(())
}
}
#[derive(Debug)]
pub struct PersistentIndexTable {
pub(crate) ent_path: EntityPath,
pub(crate) cluster_key: ComponentName,
pub(crate) num_rows: u64,
pub(crate) indices: IntMap<ComponentName, SecondaryIndex>,
pub(crate) all_components: IntSet<ComponentName>,
}
impl std::fmt::Display for PersistentIndexTable {
#[allow(clippy::string_add)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
ent_path,
cluster_key: _,
num_rows: _,
indices: _,
all_components: _,
} = self;
f.write_fmt(format_args!("entity: {ent_path}\n"))?;
f.write_fmt(format_args!(
"size: {} across {} rows\n",
format_bytes(self.total_size_bytes() as _),
format_number(self.total_rows() as _),
))?;
let (col_names, cols) = self.named_indices();
let names = col_names.into_iter().map(|name| name.to_string());
let values = cols.into_iter().map(|c| c.boxed());
let table = arrow::format_table(values, names);
f.write_fmt(format_args!("data:\n{table}\n"))?;
Ok(())
}
}
impl PersistentIndexTable {
pub fn sanity_check(&self) -> anyhow::Result<()> {
crate::profile_function!();
let Self {
ent_path: _,
cluster_key,
num_rows,
indices,
all_components: _,
} = self;
{
for (comp, index) in indices {
let secondary_len = index.len() as u64;
ensure!(
*num_rows == secondary_len,
"found rogue secondary index for {comp:?}: \
expected {num_rows} rows, got {secondary_len} instead",
);
}
}
{
let cluster_idx = indices
.get(cluster_key)
.ok_or_else(|| anyhow!("no index found for cluster key: {cluster_key:?}"))?;
ensure!(
cluster_idx.iter().all(|row| row.is_some()),
"the cluster index ({cluster_key:?}) must be fully dense: \
got {cluster_idx:?}",
);
}
Ok(())
}
pub fn named_indices(&self) -> (Vec<ComponentName>, Vec<UInt64Array>) {
crate::profile_function!();
self.indices
.iter()
.map(|(name, index)| {
(
name,
UInt64Array::from(
index
.iter()
.map(|row_idx| row_idx.map(|row_idx| row_idx.as_u64()))
.collect::<Vec<_>>(),
),
)
})
.unzip()
}
}
#[derive(Debug)]
pub struct IndexTable {
pub(crate) timeline: Timeline,
pub(crate) ent_path: EntityPath,
pub(crate) buckets: BTreeMap<TimeInt, IndexBucket>,
pub(crate) cluster_key: ComponentName,
pub(crate) all_components: IntSet<ComponentName>,
}
impl std::fmt::Display for IndexTable {
#[allow(clippy::string_add)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
timeline,
ent_path,
buckets,
cluster_key: _,
all_components: _,
} = self;
f.write_fmt(format_args!("timeline: {}\n", timeline.name()))?;
f.write_fmt(format_args!("entity: {ent_path}\n"))?;
f.write_fmt(format_args!(
"size: {} buckets for a total of {} across {} total rows\n",
self.buckets.len(),
format_bytes(self.total_size_bytes() as _),
format_number(self.total_rows() as _),
))?;
f.write_str("buckets: [\n")?;
for (time, bucket) in buckets.iter() {
f.write_str(&indent::indent_all_by(4, "IndexBucket {\n"))?;
f.write_str(&indent::indent_all_by(
8,
format!("index time bound: >= {}\n", timeline.typ().format(*time),),
))?;
f.write_str(&indent::indent_all_by(8, bucket.to_string()))?;
f.write_str(&indent::indent_all_by(4, "}\n"))?;
}
f.write_str("]")?;
Ok(())
}
}
impl IndexTable {
pub fn entity_path(&self) -> &EntityPath {
&self.ent_path
}
pub fn sanity_check(&self) -> anyhow::Result<()> {
crate::profile_function!();
{
let time_ranges = self
.buckets
.values()
.map(|bucket| bucket.indices.read().time_range)
.collect::<Vec<_>>();
for time_ranges in time_ranges.windows(2) {
let &[t1, t2] = time_ranges else { unreachable!() };
ensure!(
t1.max.as_i64() < t2.min.as_i64(),
"found overlapping index buckets: {} ({}) <-> {} ({})",
self.timeline.typ().format(t1.max),
t1.max.as_i64(),
self.timeline.typ().format(t2.min),
t2.min.as_i64(),
);
}
}
for bucket in self.buckets.values() {
bucket.sanity_check()?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct IndexBucket {
pub(crate) timeline: Timeline,
pub(crate) indices: RwLock<IndexBucketIndices>,
pub(crate) cluster_key: ComponentName,
}
#[derive(Debug)]
pub struct IndexBucketIndices {
pub(crate) is_sorted: bool,
pub(crate) time_range: TimeRange,
pub(crate) times: TimeIndex,
pub(crate) indices: IntMap<ComponentName, SecondaryIndex>,
}
impl Default for IndexBucketIndices {
fn default() -> Self {
Self {
is_sorted: true,
time_range: TimeRange::new(i64::MAX.into(), i64::MIN.into()),
times: Default::default(),
indices: Default::default(),
}
}
}
impl std::fmt::Display for IndexBucket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"size: {} across {} rows\n",
format_bytes(self.total_size_bytes() as _),
format_number(self.total_rows() as _),
))?;
f.write_fmt(format_args!("{}\n", self.formatted_time_range()))?;
let (timeline_name, times) = self.times();
let (col_names, cols) = self.named_indices();
let names = std::iter::once(timeline_name)
.chain(col_names.into_iter().map(|name| name.to_string()));
let values = std::iter::once(times.boxed()).chain(cols.into_iter().map(|c| c.boxed()));
let table = arrow::format_table(values, names);
let is_sorted = self.is_sorted();
f.write_fmt(format_args!("data (sorted={is_sorted}):\n{table}\n"))?;
Ok(())
}
}
impl IndexBucket {
pub fn formatted_time_range(&self) -> String {
let time_range = &self.indices.read().time_range;
if time_range.min.as_i64() != i64::MAX && time_range.max.as_i64() != i64::MIN {
self.timeline.format_time_range(time_range)
} else {
"time range: N/A\n".to_owned()
}
}
pub fn times(&self) -> (String, Int64Array) {
crate::profile_function!();
let times = Int64Array::from_vec(self.indices.read().times.clone());
let logical_type = match self.timeline.typ() {
re_log_types::TimeType::Time => DataType::Timestamp(TimeUnit::Nanosecond, None),
re_log_types::TimeType::Sequence => DataType::Int64,
};
(self.timeline.name().to_string(), times.to(logical_type))
}
pub fn named_indices(&self) -> (Vec<ComponentName>, Vec<UInt64Array>) {
crate::profile_function!();
self.indices
.read()
.indices
.iter()
.map(|(name, index)| {
(
name,
UInt64Array::from(
index
.iter()
.map(|row_idx| row_idx.map(|row_idx| row_idx.as_u64()))
.collect::<Vec<_>>(),
),
)
})
.unzip()
}
pub fn sanity_check(&self) -> anyhow::Result<()> {
crate::profile_function!();
let IndexBucketIndices {
is_sorted: _,
time_range: _,
times,
indices,
} = &*self.indices.read();
{
let primary_len = times.len();
for (comp, index) in indices {
let secondary_len = index.len();
ensure!(
primary_len == secondary_len,
"found rogue secondary index for {comp:?}: \
expected {primary_len} rows, got {secondary_len} instead",
);
}
}
{
let cluster_key = self.cluster_key;
let cluster_idx = indices
.get(&cluster_key)
.ok_or_else(|| anyhow!("no index found for cluster key: {cluster_key:?}"))?;
ensure!(
cluster_idx.iter().all(|row| row.is_some()),
"the cluster index ({cluster_key:?}) must be fully dense: \
got {cluster_idx:?}",
);
}
Ok(())
}
}
#[derive(Debug)]
pub struct PersistentComponentTable {
pub(crate) name: ComponentName,
pub(crate) datatype: DataType,
pub(crate) chunks: Vec<Box<dyn Array>>,
pub(crate) total_rows: u64,
pub(crate) total_size_bytes: u64,
}
impl std::fmt::Display for PersistentComponentTable {
#[allow(clippy::string_add)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
name,
datatype,
chunks,
total_rows,
total_size_bytes,
} = self;
f.write_fmt(format_args!("name: {name}\n"))?;
if matches!(
std::env::var("RERUN_DATA_STORE_DISPLAY_SCHEMAS").as_deref(),
Ok("1")
) {
f.write_fmt(format_args!("datatype: {datatype:#?}\n"))?;
}
f.write_fmt(format_args!(
"size: {} across {} total rows\n",
format_bytes(*total_size_bytes as _),
format_number(*total_rows as _),
))?;
let data = {
use arrow2::compute::concatenate::concatenate;
let chunks = chunks.iter().map(|chunk| &**chunk).collect::<Vec<_>>();
concatenate(&chunks).unwrap()
};
let table = arrow::format_table([data], [self.name.as_str()]);
f.write_fmt(format_args!("{table}\n"))?;
Ok(())
}
}
impl PersistentComponentTable {
pub fn sanity_check(&self) -> anyhow::Result<()> {
crate::profile_function!();
{
for chunk in &self.chunks {
ensure!(
chunk.validity().is_none(),
"persistent component chunks should always be dense",
);
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct ComponentTable {
pub(crate) name: ComponentName,
pub(crate) datatype: DataType,
pub(crate) buckets: VecDeque<ComponentBucket>,
}
impl std::fmt::Display for ComponentTable {
#[allow(clippy::string_add)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
name,
datatype,
buckets,
} = self;
f.write_fmt(format_args!("name: {name}\n"))?;
if matches!(
std::env::var("RERUN_DATA_STORE_DISPLAY_SCHEMAS").as_deref(),
Ok("1")
) {
f.write_fmt(format_args!("datatype: {datatype:#?}\n"))?;
}
f.write_fmt(format_args!(
"size: {} buckets for a total of {} across {} total rows\n",
self.buckets.len(),
format_bytes(self.total_size_bytes() as _),
format_number(self.total_rows() as _),
))?;
f.write_str("buckets: [\n")?;
for bucket in buckets {
f.write_str(&indent::indent_all_by(4, "ComponentBucket {\n"))?;
f.write_str(&indent::indent_all_by(8, bucket.to_string()))?;
f.write_str(&indent::indent_all_by(4, "}\n"))?;
}
f.write_str("]")?;
Ok(())
}
}
impl ComponentTable {
pub fn sanity_check(&self) -> anyhow::Result<()> {
crate::profile_function!();
{
let row_ranges = self
.buckets
.iter()
.map(|bucket| bucket.row_offset..bucket.row_offset + bucket.total_rows())
.collect::<Vec<_>>();
for row_ranges in row_ranges.windows(2) {
let &[r1, r2] = &row_ranges else { unreachable!() };
ensure!(
!r1.contains(&r2.start),
"found overlapping component buckets: {r1:?} <-> {r2:?}"
);
}
}
for bucket in &self.buckets {
bucket.sanity_check()?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct ComponentBucket {
pub(crate) name: ComponentName,
pub(crate) row_offset: u64,
pub(crate) archived: bool,
pub(crate) time_ranges: HashMap<Timeline, TimeRange>,
pub(crate) chunks: Vec<Box<dyn Array>>,
pub(crate) total_rows: u64,
pub(crate) total_size_bytes: u64,
}
impl std::fmt::Display for ComponentBucket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"size: {} across {} rows\n",
format_bytes(self.total_size_bytes() as _),
format_number(self.total_rows() as _),
))?;
f.write_fmt(format_args!(
"row range: from {} to {} (all inclusive)\n",
self.row_offset,
self.row_offset
+ self
.chunks
.len()
.checked_sub(1)
.expect("buckets are never empty") as u64,
))?;
f.write_fmt(format_args!("archived: {}\n", self.archived))?;
f.write_str("time ranges:\n")?;
for (timeline, time_range) in &self.time_ranges {
f.write_fmt(format_args!(
"{}\n",
&timeline.format_time_range(time_range)
))?;
}
let data = {
use arrow2::compute::concatenate::concatenate;
let chunks = self.chunks.iter().map(|chunk| &**chunk).collect::<Vec<_>>();
concatenate(&chunks).unwrap()
};
let table = arrow::format_table([data], [self.name.as_str()]);
f.write_fmt(format_args!("{table}\n"))?;
Ok(())
}
}
impl ComponentBucket {
pub fn sanity_check(&self) -> anyhow::Result<()> {
crate::profile_function!();
{
for chunk in &self.chunks {
ensure!(
chunk.validity().is_none(),
"component bucket chunks should always be dense",
);
}
}
Ok(())
}
}