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

Add option to include blueprint in an .rrd when calling .save(…) #5572

Merged
merged 10 commits into from
Mar 21, 2024
7 changes: 6 additions & 1 deletion crates/re_build_examples/src/example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,15 @@ pub struct ExampleCategory {
#[derive(Default, Clone, Copy, serde::Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Channel {
/// Our main examples, built on each PR
#[default]
Main,
Nightly,

/// Examples built for each release, plus all `Main` examples.
Release,

/// Examples built nightly, plus all `Main` and `Nightly`.
Nightly,
}

impl Channel {
Expand Down
4 changes: 4 additions & 0 deletions crates/re_build_info/src/build_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ impl std::fmt::Display for BuildInfo {
write!(f, ", built {datetime}")?;
}

if cfg!(debug_assertions) {
write!(f, " (debug)")?;
}

Ok(())
}
}
Expand Down
79 changes: 65 additions & 14 deletions crates/re_sdk/src/recording_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1554,20 +1554,7 @@ impl RecordingStream {

// If a blueprint was provided, send it first.
if let Some(blueprint) = blueprint {
let mut store_id = None;
for msg in blueprint {
if store_id.is_none() {
store_id = Some(msg.store_id().clone());
}
sink.send(msg);
}
if let Some(store_id) = store_id {
// Let the viewer know that the blueprint has been fully received,
// and that it can now be activated.
// We don't want to activate half-loaded blueprints, because that can be confusing,
// and can also lead to problems with space-view heuristics.
sink.send(LogMsg::ActivateStore(store_id));
}
Self::send_blueprint(blueprint, &sink);
}

self.set_sink(Box::new(sink));
Expand Down Expand Up @@ -1656,13 +1643,35 @@ impl RecordingStream {
pub fn save(
&self,
path: impl Into<std::path::PathBuf>,
) -> Result<(), crate::sink::FileSinkError> {
self.save_opts(path, None)
}

/// Swaps the underlying sink for a [`crate::sink::FileSink`] at the specified `path`.
///
/// This is a convenience wrapper for [`Self::set_sink`] that upholds the same guarantees in
/// terms of data durability and ordering.
/// See [`Self::set_sink`] for more information.
///
/// If a blueprint was provided, it will be stored first in the file.
/// Blueprints are currently an experimental part of the Rust SDK.
pub fn save_opts(
&self,
path: impl Into<std::path::PathBuf>,
blueprint: Option<Vec<LogMsg>>,
) -> Result<(), crate::sink::FileSinkError> {
if forced_sink_path().is_some() {
re_log::debug!("Ignored setting new file since _RERUN_FORCE_SINK is set");
return Ok(());
}

let sink = crate::sink::FileSink::new(path)?;

// If a blueprint was provided, store it first.
if let Some(blueprint) = blueprint {
Self::send_blueprint(blueprint, &sink);
}

self.set_sink(Box::new(sink));

Ok(())
Expand All @@ -1677,6 +1686,24 @@ impl RecordingStream {
/// terms of data durability and ordering.
/// See [`Self::set_sink`] for more information.
pub fn stdout(&self) -> Result<(), crate::sink::FileSinkError> {
self.stdout_opts(None)
}

/// Swaps the underlying sink for a [`crate::sink::FileSink`] pointed at stdout.
///
/// If there isn't any listener at the other end of the pipe, the [`RecordingStream`] will
/// default back to `buffered` mode, in order not to break the user's terminal.
///
/// This is a convenience wrapper for [`Self::set_sink`] that upholds the same guarantees in
/// terms of data durability and ordering.
/// See [`Self::set_sink`] for more information.
///
/// If a blueprint was provided, it will be stored first in the file.
/// Blueprints are currently an experimental part of the Rust SDK.
pub fn stdout_opts(
&self,
blueprint: Option<Vec<LogMsg>>,
) -> Result<(), crate::sink::FileSinkError> {
if forced_sink_path().is_some() {
re_log::debug!("Ignored setting new file since _RERUN_FORCE_SINK is set");
return Ok(());
Expand All @@ -1689,6 +1716,12 @@ impl RecordingStream {
}

let sink = crate::sink::FileSink::stdout()?;

// If a blueprint was provided, write it first.
if let Some(blueprint) = blueprint {
Self::send_blueprint(blueprint, &sink);
}

self.set_sink(Box::new(sink));

Ok(())
Expand All @@ -1711,6 +1744,24 @@ impl RecordingStream {
re_log::warn_once!("Recording disabled - call to disconnect() ignored");
}
}

/// Send the blueprint to the sink, and then activate it.
pub fn send_blueprint(blueprint: Vec<LogMsg>, sink: &dyn crate::sink::LogSink) {
let mut store_id = None;
for msg in blueprint {
if store_id.is_none() {
store_id = Some(msg.store_id().clone());
}
sink.send(msg);
}
if let Some(store_id) = store_id {
// Let the viewer know that the blueprint has been fully received,
// and that it can now be activated.
// We don't want to activate half-loaded blueprints, because that can be confusing,
// and can also lead to problems with space-view heuristics.
sink.send(LogMsg::ActivateStore(store_id));
}
}
}

impl fmt::Debug for RecordingStream {
Expand Down
13 changes: 12 additions & 1 deletion examples/python/structure_from_motion/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import numpy.typing as npt
import requests
import rerun as rr # pip install rerun-sdk
import rerun.blueprint as rbl
emilk marked this conversation as resolved.
Show resolved Hide resolved
from read_write_model import Camera, read_model
from tqdm import tqdm

Expand Down Expand Up @@ -221,7 +222,17 @@ def main() -> None:
if args.resize:
args.resize = tuple(int(x) for x in args.resize.split("x"))

rr.script_setup(args, "rerun_example_structure_from_motion")
blueprint = rbl.Vertical(
rbl.Spatial3DView(name="3D", origin="/"),
rbl.Horizontal(
rbl.TextDocumentView(name="README", origin="/description"),
rbl.Spatial2DView(name="Camera", origin="/camera/image"),
rbl.TimeSeriesView(origin="/plot"),
),
row_shares=[3, 2],
)

rr.script_setup(args, "rerun_example_structure_from_motion", blueprint=blueprint)
dataset_path = get_downloaded_dataset_path(args.dataset)
read_and_log_sparse_reconstruction(dataset_path, filter_output=not args.unfiltered, resize=args.resize)
rr.script_teardown(args)
Expand Down
6 changes: 3 additions & 3 deletions rerun_py/rerun_sdk/rerun/script_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,16 @@ def script_setup(

# NOTE: mypy thinks these methods don't exist because they're monkey-patched.
if args.stdout:
rec.stdout() # type: ignore[attr-defined]
rec.stdout(blueprint=blueprint) # type: ignore[attr-defined]
elif args.serve:
rec.serve() # type: ignore[attr-defined]
rec.serve(blueprint=blueprint) # type: ignore[attr-defined]
elif args.connect:
# Send logging data to separate `rerun` process.
# You can omit the argument to connect to the default address,
# which is `127.0.0.1:9876`.
rec.connect(args.addr, blueprint=blueprint) # type: ignore[attr-defined]
elif args.save is not None:
rec.save(args.save) # type: ignore[attr-defined]
rec.save(args.save, blueprint=blueprint) # type: ignore[attr-defined]
elif not args.headless:
rec.spawn(blueprint=blueprint) # type: ignore[attr-defined]

Expand Down
74 changes: 67 additions & 7 deletions rerun_py/rerun_sdk/rerun/sinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ def connect(
See also: [`rerun.init`][], [`rerun.set_global_data_recording`][].

"""
application_id = get_application_id(recording=recording)
recording = RecordingStream.to_native(recording)

if not bindings.is_enabled():
logging.warning("Rerun is disabled - connect() call ignored")
return

application_id = get_application_id(recording=recording)
if application_id is None:
raise ValueError(
"No application id found. You must call rerun.init before connecting to a viewer, or provide a recording."
Expand All @@ -56,13 +59,17 @@ def connect(
if blueprint is not None:
blueprint_storage = create_in_memory_blueprint(application_id=application_id, blueprint=blueprint).storage

recording = RecordingStream.to_native(recording)

bindings.connect(addr=addr, flush_timeout_sec=flush_timeout_sec, blueprint=blueprint_storage, recording=recording)


_connect = connect # we need this because Python scoping is horrible


def save(path: str | pathlib.Path, recording: RecordingStream | None = None) -> None:
def save(
path: str | pathlib.Path, blueprint: BlueprintLike | None = None, recording: RecordingStream | None = None
) -> None:
"""
Stream all log-data to a file.

Expand All @@ -72,6 +79,9 @@ def save(path: str | pathlib.Path, recording: RecordingStream | None = None) ->
----------
path : str
The path to save the data to.
blueprint: Optional[BlueprintLike]
emilk marked this conversation as resolved.
Show resolved Hide resolved
An optional blueprint to configure the UI.
This will be written first to the .rrd file, before appending the recording data.
recording:
Specifies the [`rerun.RecordingStream`][] to use.
If left unspecified, defaults to the current active data recording, if there is one.
Expand All @@ -83,11 +93,23 @@ def save(path: str | pathlib.Path, recording: RecordingStream | None = None) ->
logging.warning("Rerun is disabled - save() call ignored. You must call rerun.init before saving a recording.")
return

application_id = get_application_id(recording=recording)
if application_id is None:
raise ValueError(
"No application id found. You must call rerun.init before connecting to a viewer, or provide a recording."
)

# If a blueprint is provided, we need to create a blueprint storage object
blueprint_storage = None
if blueprint is not None:
blueprint_storage = create_in_memory_blueprint(application_id=application_id, blueprint=blueprint).storage

recording = RecordingStream.to_native(recording)
bindings.save(path=str(path), recording=recording)

bindings.save(path=str(path), blueprint=blueprint_storage, recording=recording)


def stdout(recording: RecordingStream | None = None) -> None:
def stdout(blueprint: BlueprintLike | None = None, recording: RecordingStream | None = None) -> None:
"""
Stream all log-data to stdout.

Expand All @@ -100,6 +122,8 @@ def stdout(recording: RecordingStream | None = None) -> None:

Parameters
----------
blueprint: Optional[BlueprintLike]
An optional blueprint to configure the UI.
recording:
Specifies the [`rerun.RecordingStream`][] to use.
If left unspecified, defaults to the current active data recording, if there is one.
Expand All @@ -111,8 +135,19 @@ def stdout(recording: RecordingStream | None = None) -> None:
logging.warning("Rerun is disabled - save() call ignored. You must call rerun.init before saving a recording.")
return

application_id = get_application_id(recording=recording)
if application_id is None:
raise ValueError(
"No application id found. You must call rerun.init before connecting to a viewer, or provide a recording."
)

# If a blueprint is provided, we need to create a blueprint storage object
blueprint_storage = None
if blueprint is not None:
blueprint_storage = create_in_memory_blueprint(application_id=application_id, blueprint=blueprint).storage

recording = RecordingStream.to_native(recording)
bindings.stdout(recording=recording)
bindings.stdout(blueprint=blueprint_storage, recording=recording)


def disconnect(recording: RecordingStream | None = None) -> None:
Expand Down Expand Up @@ -165,6 +200,7 @@ def serve(
open_browser: bool = True,
web_port: int | None = None,
ws_port: int | None = None,
blueprint: BlueprintLike | None = None,
recording: RecordingStream | None = None,
server_memory_limit: str = "25%",
) -> None:
Expand All @@ -188,6 +224,8 @@ def serve(
The port to serve the web viewer on (defaults to 9090).
ws_port:
The port to serve the WebSocket server on (defaults to 9877)
blueprint: Optional[BlueprintLike]
An optional blueprint to configure the UI.
recording:
Specifies the [`rerun.RecordingStream`][] to use.
If left unspecified, defaults to the current active data recording, if there is one.
Expand All @@ -198,8 +236,30 @@ def serve(

"""

if not bindings.is_enabled():
logging.warning("Rerun is disabled - serve() call ignored")
return

application_id = get_application_id(recording=recording)
if application_id is None:
raise ValueError(
"No application id found. You must call rerun.init before connecting to a viewer, or provide a recording."
)

# If a blueprint is provided, we need to create a blueprint storage object
blueprint_storage = None
if blueprint is not None:
blueprint_storage = create_in_memory_blueprint(application_id=application_id, blueprint=blueprint).storage

recording = RecordingStream.to_native(recording)
bindings.serve(open_browser, web_port, ws_port, server_memory_limit=server_memory_limit, recording=recording)
bindings.serve(
open_browser,
web_port,
ws_port,
server_memory_limit=server_memory_limit,
blueprint=blueprint_storage,
recording=recording,
)


# TODO(#4019): application-level handshake
Expand Down