Skip to content

Commit

Permalink
Add option to include blueprint in an .rrd when calling .save(…) (#…
Browse files Browse the repository at this point in the history
…5572)

### What
* Closes #5558

This adds an optional `blueprint` argument to `rr.save` which if
provided, will end up first in the written .rrd file.

When using `rr.script_setup` (like all our examples do) with a
`blueprint`, `--save` will put that blueprint in the .rrd file.

This should mean that the examples build by CI will include blueprints.

**BONUS**: this also adds the blueprint to `rr.serve` and `rr.stdout`
(and `--serve/--stdout` in our examples).

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using newly built examples:
[app.rerun.io](https://app.rerun.io/pr/5572/index.html)
* Using examples from latest `main` build:
[app.rerun.io](https://app.rerun.io/pr/5572/index.html?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[app.rerun.io](https://app.rerun.io/pr/5572/index.html?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!

- [PR Build Summary](https://build.rerun.io/pr/5572)
- [Docs
preview](https://rerun.io/preview/3ec8fddde8caa12a00097353c257fdd60be9a48f/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/3ec8fddde8caa12a00097353c257fdd60be9a48f/examples)
<!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)
  • Loading branch information
emilk committed Mar 21, 2024
1 parent cc0e895 commit dc46941
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 55 deletions.
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 rrb
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 = rrb.Vertical(
rrb.Spatial3DView(name="3D", origin="/"),
rrb.Horizontal(
rrb.TextDocumentView(name="README", origin="/description"),
rrb.Spatial2DView(name="Camera", origin="/camera/image"),
rrb.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
94 changes: 77 additions & 17 deletions rerun_py/rerun_sdk/rerun/sinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,26 @@ def connect(
Parameters
----------
addr
addr:
The ip:port to connect to
flush_timeout_sec: float
flush_timeout_sec:
The minimum time the SDK will wait during a flush before potentially
dropping data if progress is not being made. Passing `None` indicates no timeout,
and can cause a call to `flush` to block indefinitely.
blueprint: Optional[BlueprintLike]
blueprint:
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.
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,22 +59,29 @@ 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.
Call this _before_ you log any data!
Parameters
----------
path : str
path:
The path to save the data to.
blueprint:
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:
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 @@ -182,12 +218,14 @@ def serve(
Parameters
----------
open_browser
open_browser:
Open the default browser to the viewer.
web_port:
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:
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 Expand Up @@ -236,19 +296,19 @@ def spawn(
Parameters
----------
port : int
port:
The port to listen on.
connect
connect:
also connect to the viewer and stream logging data to it.
memory_limit
memory_limit:
An upper limit on how much memory the Rerun Viewer should use.
When this limit is reached, Rerun will drop the oldest data.
Example: `16GB` or `50%` (of system total).
recording
recording:
Specifies the [`rerun.RecordingStream`][] to use if `connect = True`.
If left unspecified, defaults to the current active data recording, if there is one.
See also: [`rerun.init`][], [`rerun.set_global_data_recording`][].
blueprint: Optional[BlueprintLike]
blueprint:
An optional blueprint to configure the UI.
"""
Expand Down

0 comments on commit dc46941

Please sign in to comment.