Skip to content

Commit 79af754

Browse files
authored
Introduce a simpler cache dedicated to just decode JPEGs (#1550)
* New cache for decoded tensors * Use the tensor_decode_cache * Actually track/purge the decode cache * At least log warning if the image can't be decoded * Pull in the zero-copy deserialization for ArrowBinary * Bump up decode cache to 4G on non-wasm targets
1 parent 90f583e commit 79af754

File tree

14 files changed

+362
-67
lines changed

14 files changed

+362
-67
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//! Assorted shims that should make their way back to [arrow2-convert](https://github.com/DataEngineeringLabs/arrow2-convert/)
2+
3+
use std::ops::Index;
4+
5+
use arrow2::{
6+
array::{Array, BinaryArray},
7+
buffer::Buffer,
8+
};
9+
use arrow2_convert::{
10+
deserialize::{ArrowArray, ArrowDeserialize},
11+
ArrowField, ArrowSerialize,
12+
};
13+
14+
/// Shim to enable zero-copy arrow deserialization for `Buffer<u8>`
15+
/// Can be removed when: [arrow2-convert#103](https://github.com/DataEngineeringLabs/arrow2-convert/pull/103) lands
16+
#[derive(Clone, Debug, PartialEq, ArrowField, ArrowSerialize)]
17+
#[arrow_field(transparent)]
18+
pub struct BinaryBuffer(pub Buffer<u8>);
19+
20+
impl BinaryBuffer {
21+
pub fn as_slice(&self) -> &[u8] {
22+
self.0.as_slice()
23+
}
24+
}
25+
26+
impl Index<usize> for BinaryBuffer {
27+
type Output = u8;
28+
fn index(&self, i: usize) -> &u8 {
29+
&self.0[i]
30+
}
31+
}
32+
33+
impl From<Vec<u8>> for BinaryBuffer {
34+
fn from(v: Vec<u8>) -> Self {
35+
Self(v.into())
36+
}
37+
}
38+
39+
/// Iterator for for [`BufferBinaryArray`]
40+
pub struct BufferBinaryArrayIter<'a> {
41+
index: usize,
42+
array: &'a BinaryArray<i32>,
43+
}
44+
45+
impl<'a> Iterator for BufferBinaryArrayIter<'a> {
46+
type Item = Option<Buffer<u8>>;
47+
48+
fn next(&mut self) -> Option<Self::Item> {
49+
if self.index >= self.array.len() {
50+
None
51+
} else {
52+
if let Some(validity) = self.array.validity() {
53+
if !validity.get_bit(self.index) {
54+
self.index += 1;
55+
return Some(None);
56+
}
57+
}
58+
let (start, end) = self.array.offsets().start_end(self.index);
59+
self.index += 1;
60+
Some(Some(self.array.values().clone().slice(start, end - start)))
61+
}
62+
}
63+
}
64+
65+
/// Internal `ArrowArray` helper to iterate over a `BinaryArray` while exposing Buffer slices
66+
pub struct BufferBinaryArray;
67+
68+
extern "C" {
69+
fn do_not_call_into_iter(); // we never define this function, so the linker will fail
70+
}
71+
72+
impl<'a> IntoIterator for &'a BufferBinaryArray {
73+
type Item = Option<Buffer<u8>>;
74+
75+
type IntoIter = BufferBinaryArrayIter<'a>;
76+
77+
fn into_iter(self) -> Self::IntoIter {
78+
#[allow(unsafe_code)]
79+
// SAFETY:
80+
// This exists so we get a link-error if some code tries to call into_iter
81+
// Iteration should only happen via iter_from_array_ref.
82+
// This is a quirk of the way the traits work in arrow2_convert.
83+
unsafe {
84+
do_not_call_into_iter();
85+
}
86+
unreachable!()
87+
}
88+
}
89+
90+
impl ArrowArray for BufferBinaryArray {
91+
type BaseArrayType = BinaryArray<i32>;
92+
#[inline]
93+
fn iter_from_array_ref(a: &dyn Array) -> <&Self as IntoIterator>::IntoIter {
94+
let b = a.as_any().downcast_ref::<Self::BaseArrayType>().unwrap();
95+
96+
BufferBinaryArrayIter { index: 0, array: b }
97+
}
98+
}
99+
100+
impl ArrowDeserialize for BinaryBuffer {
101+
type ArrayType = BufferBinaryArray;
102+
103+
#[inline]
104+
fn arrow_deserialize(v: Option<Buffer<u8>>) -> Option<Self> {
105+
v.map(BinaryBuffer)
106+
}
107+
}

crates/re_log_types/src/component_types/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use lazy_static::lazy_static;
1717
use crate::msg_bundle::Component;
1818

1919
mod arrow;
20+
mod arrow_convert_shims;
2021
mod bbox;
2122
mod class_id;
2223
mod color;
@@ -59,6 +60,8 @@ pub use radius::Radius;
5960
pub use rect::Rect2D;
6061
pub use scalar::{Scalar, ScalarPlotProps};
6162
pub use size::Size3D;
63+
#[cfg(feature = "image")]
64+
pub use tensor::TensorImageError;
6265
pub use tensor::{
6366
Tensor, TensorCastError, TensorData, TensorDataMeaning, TensorDimension, TensorId, TensorTrait,
6467
};

crates/re_log_types/src/component_types/tensor.rs

+35-15
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use arrow2_convert::{serialize::ArrowSerialize, ArrowDeserialize, ArrowField, Ar
77
use crate::msg_bundle::Component;
88
use crate::{TensorDataType, TensorElement};
99

10+
use super::arrow_convert_shims::BinaryBuffer;
11+
1012
pub trait TensorTrait {
1113
fn id(&self) -> TensorId;
1214
fn shape(&self) -> &[TensorDimension];
@@ -16,6 +18,7 @@ pub trait TensorTrait {
1618
fn meaning(&self) -> TensorDataMeaning;
1719
fn get(&self, index: &[u64]) -> Option<TensorElement>;
1820
fn dtype(&self) -> TensorDataType;
21+
fn size_in_bytes(&self) -> usize;
1922
}
2023

2124
// ----------------------------------------------------------------------------
@@ -154,7 +157,7 @@ impl ArrowDeserialize for TensorId {
154157
#[derive(Clone, Debug, PartialEq, ArrowField, ArrowSerialize, ArrowDeserialize)]
155158
#[arrow_field(type = "dense")]
156159
pub enum TensorData {
157-
U8(Vec<u8>),
160+
U8(BinaryBuffer),
158161
U16(Buffer<u16>),
159162
U32(Buffer<u32>),
160163
U64(Buffer<u64>),
@@ -168,7 +171,7 @@ pub enum TensorData {
168171
//F16(Vec<arrow2::types::f16>),
169172
F32(Buffer<f32>),
170173
F64(Buffer<f64>),
171-
JPEG(Vec<u8>),
174+
JPEG(BinaryBuffer),
172175
}
173176

174177
/// Flattened `Tensor` data payload
@@ -404,6 +407,21 @@ impl TensorTrait for Tensor {
404407
TensorData::F64(_) => TensorDataType::F64,
405408
}
406409
}
410+
411+
fn size_in_bytes(&self) -> usize {
412+
match &self.data {
413+
TensorData::U8(buf) | TensorData::JPEG(buf) => buf.0.len(),
414+
TensorData::U16(buf) => buf.len(),
415+
TensorData::U32(buf) => buf.len(),
416+
TensorData::U64(buf) => buf.len(),
417+
TensorData::I8(buf) => buf.len(),
418+
TensorData::I16(buf) => buf.len(),
419+
TensorData::I32(buf) => buf.len(),
420+
TensorData::I64(buf) => buf.len(),
421+
TensorData::F32(buf) => buf.len(),
422+
TensorData::F64(buf) => buf.len(),
423+
}
424+
}
407425
}
408426

409427
impl Component for Tensor {
@@ -535,7 +553,7 @@ impl<'a> TryFrom<&'a Tensor> for ::ndarray::ArrayViewD<'a, half::f16> {
535553

536554
#[cfg(feature = "image")]
537555
#[derive(thiserror::Error, Debug)]
538-
pub enum ImageError {
556+
pub enum TensorImageError {
539557
#[error(transparent)]
540558
Image(#[from] image::ImageError),
541559

@@ -575,20 +593,22 @@ impl Tensor {
575593
#[cfg(not(target_arch = "wasm32"))]
576594
pub fn tensor_from_jpeg_file(
577595
image_path: impl AsRef<std::path::Path>,
578-
) -> Result<Self, ImageError> {
596+
) -> Result<Self, TensorImageError> {
579597
let jpeg_bytes = std::fs::read(image_path)?;
580598
Self::tensor_from_jpeg_bytes(jpeg_bytes)
581599
}
582600

583601
/// Construct a tensor from the contents of a JPEG file.
584602
///
585603
/// Requires the `image` feature.
586-
pub fn tensor_from_jpeg_bytes(jpeg_bytes: Vec<u8>) -> Result<Self, ImageError> {
604+
pub fn tensor_from_jpeg_bytes(jpeg_bytes: Vec<u8>) -> Result<Self, TensorImageError> {
587605
use image::ImageDecoder as _;
588606
let jpeg = image::codecs::jpeg::JpegDecoder::new(std::io::Cursor::new(&jpeg_bytes))?;
589607
if jpeg.color_type() != image::ColorType::Rgb8 {
590608
// TODO(emilk): support gray-scale jpeg as well
591-
return Err(ImageError::UnsupportedJpegColorType(jpeg.color_type()));
609+
return Err(TensorImageError::UnsupportedJpegColorType(
610+
jpeg.color_type(),
611+
));
592612
}
593613
let (w, h) = jpeg.dimensions();
594614

@@ -599,7 +619,7 @@ impl Tensor {
599619
TensorDimension::width(w as _),
600620
TensorDimension::depth(3),
601621
],
602-
data: TensorData::JPEG(jpeg_bytes),
622+
data: TensorData::JPEG(jpeg_bytes.into()),
603623
meaning: TensorDataMeaning::Unknown,
604624
meter: None,
605625
})
@@ -608,20 +628,20 @@ impl Tensor {
608628
/// Construct a tensor from something that can be turned into a [`image::DynamicImage`].
609629
///
610630
/// Requires the `image` feature.
611-
pub fn from_image(image: impl Into<image::DynamicImage>) -> Result<Self, ImageError> {
631+
pub fn from_image(image: impl Into<image::DynamicImage>) -> Result<Self, TensorImageError> {
612632
Self::from_dynamic_image(image.into())
613633
}
614634

615635
/// Construct a tensor from [`image::DynamicImage`].
616636
///
617637
/// Requires the `image` feature.
618-
pub fn from_dynamic_image(image: image::DynamicImage) -> Result<Self, ImageError> {
638+
pub fn from_dynamic_image(image: image::DynamicImage) -> Result<Self, TensorImageError> {
619639
let (w, h) = (image.width(), image.height());
620640

621641
let (depth, data) = match image {
622-
image::DynamicImage::ImageLuma8(image) => (1, TensorData::U8(image.into_raw())),
623-
image::DynamicImage::ImageRgb8(image) => (3, TensorData::U8(image.into_raw())),
624-
image::DynamicImage::ImageRgba8(image) => (4, TensorData::U8(image.into_raw())),
642+
image::DynamicImage::ImageLuma8(image) => (1, TensorData::U8(image.into_raw().into())),
643+
image::DynamicImage::ImageRgb8(image) => (3, TensorData::U8(image.into_raw().into())),
644+
image::DynamicImage::ImageRgba8(image) => (4, TensorData::U8(image.into_raw().into())),
625645
image::DynamicImage::ImageLuma16(image) => {
626646
(1, TensorData::U16(image.into_raw().into()))
627647
}
@@ -649,7 +669,7 @@ impl Tensor {
649669
}
650670
_ => {
651671
// It is very annoying that DynamicImage is #[non_exhaustive]
652-
return Err(ImageError::UnsupportedImageColorType(image.color()));
672+
return Err(TensorImageError::UnsupportedImageColorType(image.color()));
653673
}
654674
};
655675

@@ -741,7 +761,7 @@ fn test_concat_and_slice() {
741761
size: 4,
742762
name: None,
743763
}],
744-
data: TensorData::JPEG(vec![1, 2, 3, 4]),
764+
data: TensorData::JPEG(vec![1, 2, 3, 4].into()),
745765
meaning: TensorDataMeaning::Unknown,
746766
meter: Some(1000.0),
747767
}];
@@ -752,7 +772,7 @@ fn test_concat_and_slice() {
752772
size: 4,
753773
name: None,
754774
}],
755-
data: TensorData::JPEG(vec![5, 6, 7, 8]),
775+
data: TensorData::JPEG(vec![5, 6, 7, 8].into()),
756776
meaning: TensorDataMeaning::Unknown,
757777
meter: None,
758778
}];

crates/re_tensor_ops/tests/tensor_tests.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ fn convert_tensor_to_ndarray_u8() {
1111
TensorDimension::unnamed(4),
1212
TensorDimension::unnamed(5),
1313
],
14-
TensorData::U8(vec![0; 60]),
14+
TensorData::U8(vec![0; 60].into()),
1515
TensorDataMeaning::Unknown,
1616
None,
1717
);
@@ -130,7 +130,7 @@ fn check_tensor_shape_error() {
130130
TensorDimension::unnamed(4),
131131
TensorDimension::unnamed(5),
132132
],
133-
TensorData::U8(vec![0; 59]),
133+
TensorData::U8(vec![0; 59].into()),
134134
TensorDataMeaning::Unknown,
135135
None,
136136
);

crates/re_viewer/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ re_log.workspace = true
4545
re_log_types = { workspace = true, features = [
4646
"ecolor",
4747
"glam",
48+
"image",
4849
"save",
4950
"load",
5051
] }
@@ -99,6 +100,7 @@ serde = { version = "1", features = ["derive"] }
99100
slotmap = { version = "1.0.6", features = ["serde"] }
100101
smallvec = { version = "1.10", features = ["serde"] }
101102
time = { workspace = true, default-features = false, features = ["formatting"] }
103+
thiserror.workspace = true
102104
uuid = { version = "1.1", features = ["serde", "v4", "js"] }
103105
vec1 = "1.8"
104106
wgpu.workspace = true

crates/re_viewer/src/app.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ impl eframe::App for App {
457457

458458
self.purge_memory_if_needed();
459459

460-
self.state.cache.new_frame();
460+
self.state.cache.begin_frame();
461461

462462
self.receive_messages(egui_ctx);
463463

crates/re_viewer/src/misc/caches/mod.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod mesh_cache;
2+
mod tensor_decode_cache;
23
mod tensor_image_cache;
34

45
use re_log_types::component_types::{self, TensorTrait};
@@ -9,6 +10,7 @@ pub use tensor_image_cache::{AsDynamicImage, TensorImageView};
910
pub struct Caches {
1011
/// For displaying images efficiently in immediate mode.
1112
pub image: tensor_image_cache::ImageCache,
13+
pub decode: tensor_decode_cache::DecodeCache,
1214

1315
/// For displaying meshes efficiently in immediate mode.
1416
pub mesh: mesh_cache::MeshCache,
@@ -18,18 +20,28 @@ pub struct Caches {
1820

1921
impl Caches {
2022
/// Call once per frame to potentially flush the cache(s).
21-
pub fn new_frame(&mut self) {
23+
pub fn begin_frame(&mut self) {
2224
let max_image_cache_use = 1_000_000_000;
23-
self.image.new_frame(max_image_cache_use);
25+
26+
#[cfg(not(target_arch = "wasm32"))]
27+
let max_decode_cache_use = 4_000_000_000;
28+
29+
#[cfg(target_arch = "wasm32")]
30+
let max_decode_cache_use = 1_000_000_000;
31+
32+
self.image.begin_frame(max_image_cache_use);
33+
self.decode.begin_frame(max_decode_cache_use);
2434
}
2535

2636
pub fn purge_memory(&mut self) {
2737
let Self {
2838
image,
39+
decode,
2940
tensor_stats,
3041
mesh: _, // TODO(emilk)
3142
} = self;
3243
image.purge_memory();
44+
decode.purge_memory();
3345
tensor_stats.clear();
3446
}
3547

0 commit comments

Comments
 (0)