Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion rust/foxglove/src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod raw_channel;

pub use channel_descriptor::ChannelDescriptor;
pub use lazy_channel::{LazyChannel, LazyRawChannel};
pub use raw_channel::RawChannel;
pub use raw_channel::{RawChannel, TransportKind};

/// Stack buffer size to use for encoding messages.
const STACK_BUFFER_SIZE: usize = 256 * 1024;
Expand Down
26 changes: 26 additions & 0 deletions rust/foxglove/src/channel/raw_channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@ use crate::{nanoseconds_since_epoch, Context, Metadata, PartialMetadata, Schema,
/// Interval for throttled warnings.
static WARN_THROTTLER_INTERVAL: Duration = Duration::from_secs(10);

/// Options that control how data for the channel is sent over the network with applicable sinks.
#[derive(Debug, Clone, Copy, Default)]
#[repr(u8)]
pub enum TransportKind {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming: TransportReliability?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or TransportDelivery? Naming things...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lean towards TransportReliability. TransportKind and TransportDelivery are a bit too generic for my taste; that could encapsulate things like History and Durability, which are orthogonal to this axes of delivery.

/// Delivery of messages is best effort and may not be retried. This is the default.
/// This only affects [`CloudSink`](crate::CloudSink) which supports UDP,
/// not currently [`WebSocketServer`](crate::WebSocketServer) or [`McapWriter`](crate::McapWriter).
/// Messages are still delivered to the Foxglove in-order.
///
/// Note: this only applies to messages under 15kb, larger messages will use Reliable transport.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we planning on dispatching based on message size? Or is this more saying "you must use Reliable transport if message size exceeds 15K"?

Copy link
Contributor Author

@eloff eloff Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do either. I think dynamic dispatch on message size is the best user experience (at least when not limited by network bandwidth), so I lean that way.

#[default]
Lossy,
/// Delivery of messages is reliable, retrying if need need be.
/// This causes head-of-line blocking, and if there isn't enough bandwidth available
/// buffers and queues will grow longer. Be very sparing with this over poor quality networks.
///
/// Doesn't affect [`McapWriter`](crate::McapWriter).
/// Note: delivery is still not guaranteed, because buffers and queues are not infinite.
/// Messages may still be dropped, or the OOM killer may reap the process.
Reliable,
}

/// A log channel that can be used to log binary messages.
///
/// A "channel" is conceptually the same as a [MCAP channel]: it is a stream of messages which all
Expand All @@ -38,6 +60,8 @@ pub struct RawChannel {
context: Weak<Context>,
sinks: LogSinkSet,
closed: AtomicBool,
#[allow(dead_code)]
transport_kind: TransportKind,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of exposing this at the top-level of a Channel, I wonder if we nest this in a TransportOptions struct, so the Channel API doesn't need to change as we introduce new QoS and transport configuration settings. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's a good idea

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this belongs on the channel at all. Transport options are particular to a (channel, sink) pair. I'd prefer that we rework this along the lines of sink channel filters, but of course only for those sinks that can offer differentiated QoS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with that, it's specific to the (channel, sink) pair. But, it's imperative that it be efficient to access. If it requires a hash table lookup on each log call, I think it's too much. Given that, I think the Channel gives a logical and efficient place to store the setting, and sinks are free to ignore it unless they need it.

Can you think of another way we could implement this efficiently?

Copy link
Contributor

@gasmith gasmith Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really worried about a hashmap lookup on the fast path. If this becomes a measurable bottleneck, and we want to optimize, we could potentially store it during channel subscription, as part of the LogSinkSet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep in mind it's not just a hashmap lookup, but taking a read() lock on a RwLock, doing the lookup, and releasing the lock. About ~400 cycles as a rough estimate, depending on caching and architecture.

At 10k log messages/sec it'd be around 4 million cycles / sec just for checking a flag, which could have been a pointer indirection. If we don't make this part of the public interface, having it on the channel as an implementation detail seems ok to me. Less code to maintain too. We can always refactor it if we want to later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so then let's see what it would take to configure this via the sink, and store it in the LogSinkSet. At this point I care more about interfacing than performance.

warn_throttler: Mutex<Throttler>,
}

Expand All @@ -48,6 +72,7 @@ impl RawChannel {
message_encoding: String,
schema: Option<Schema>,
metadata: BTreeMap<String, String>,
transport_kind: TransportKind,
) -> Arc<Self> {
Arc::new(Self {
descriptor: ChannelDescriptor::new(
Expand All @@ -60,6 +85,7 @@ impl RawChannel {
context: Arc::downgrade(context),
sinks: LogSinkSet::new(),
closed: AtomicBool::new(false),
transport_kind,
warn_throttler: Mutex::new(Throttler::new(WARN_THROTTLER_INTERVAL)),
})
}
Expand Down
12 changes: 11 additions & 1 deletion rust/foxglove/src/channel_builder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use std::sync::Arc;

use crate::{Channel, Context, Encode, FoxgloveError, RawChannel, Schema};
use crate::{Channel, Context, Encode, FoxgloveError, RawChannel, Schema, TransportKind};

/// A builder for creating a [`Channel`] or [`RawChannel`].
#[must_use]
Expand All @@ -12,6 +12,7 @@ pub struct ChannelBuilder {
schema: Option<Schema>,
metadata: BTreeMap<String, String>,
context: Arc<Context>,
transport_kind: TransportKind,
}

impl ChannelBuilder {
Expand All @@ -25,6 +26,7 @@ impl ChannelBuilder {
schema: None,
metadata: BTreeMap::new(),
context: Context::get_default(),
transport_kind: TransportKind::Lossy,
}
}

Expand Down Expand Up @@ -65,6 +67,13 @@ impl ChannelBuilder {
self
}

/// Sets the [`TransportKind`](crate::TransportKind) for the channel.
/// It controls how data for the channel is sent over the network with applicable sinks.
pub fn transport_kind(mut self, transport_kind: TransportKind) -> Self {
self.transport_kind = transport_kind;
self
}

/// Builds a [`RawChannel`].
///
/// Returns [`FoxgloveError::MessageEncodingRequired`] if no message encoding was specified.
Expand All @@ -76,6 +85,7 @@ impl ChannelBuilder {
.ok_or_else(|| FoxgloveError::MessageEncodingRequired)?,
self.schema,
self.metadata,
self.transport_kind,
);
channel = self.context.add_channel(channel);
Ok(channel)
Expand Down
4 changes: 3 additions & 1 deletion rust/foxglove/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,9 @@ pub mod stream;
pub use app_url::AppUrl;
// Re-export bytes crate for convenience when implementing the `Encode` trait
pub use bytes;
pub use channel::{Channel, ChannelDescriptor, ChannelId, LazyChannel, LazyRawChannel, RawChannel};
pub use channel::{
Channel, ChannelDescriptor, ChannelId, LazyChannel, LazyRawChannel, RawChannel, TransportKind,
};
pub use channel_builder::ChannelBuilder;
pub use context::{Context, LazyContext};
#[doc(hidden)]
Expand Down