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

Turn on garbage-collection (--memory-limit) by default #3161

Merged
merged 11 commits into from
Aug 31, 2023
2 changes: 1 addition & 1 deletion crates/re_log_types/src/data_cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ impl DataCell {
};

if iter.next().is_some() {
re_log::warn_once!("Unexpected batch for {}", C::name());
re_log::warn_once!("Expected only one {}, got {}", C::name(), iter.count() + 2);
}

result
Expand Down
33 changes: 30 additions & 3 deletions crates/re_memory/src/memory_limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,37 @@ pub struct MemoryLimit {
}

impl MemoryLimit {
/// The limit can either be absolute (e.g. "16GB") or relative (e.g. "50%").
pub fn parse(limit: &str) -> Result<Self, String> {
re_format::parse_bytes(limit)
.map(|limit| Self { limit: Some(limit) })
.ok_or_else(|| format!("expected e.g. '16GB', got {limit:?}"))
if let Some(percentage) = limit.strip_suffix('%') {
let percentage = percentage
.parse::<f32>()
.map_err(|_err| format!("expected e.g. '50%', got {limit:?}"))?;

let total_memory = crate::total_ram_in_bytes();
if total_memory == 0 {
re_log::info!(
"Couldn't determine total available memory. Setting no memory limit."
);
Ok(Self { limit: None })
} else {
let limit = (total_memory as f64 * (percentage as f64 / 100.0)).round();

re_log::debug!(
"Setting memory limit to {}, which is {percentage}% of total available memory ({}).",
re_format::format_bytes(limit),
re_format::format_bytes(total_memory as _),
);

Ok(Self {
limit: Some(limit as _),
})
}
} else {
re_format::parse_bytes(limit)
.map(|limit| Self { limit: Some(limit) })
.ok_or_else(|| format!("expected e.g. '16GB', got {limit:?}"))
}
}

/// Returns how large fraction of memory we should free to go down to the exact limit.
Expand Down
15 changes: 4 additions & 11 deletions crates/re_memory/src/ram_warner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,7 @@ pub fn total_ram_in_bytes() -> u64 {
let mut sys = sysinfo::System::new_all();
sys.refresh_all();

let total_memory = sys.total_memory();

re_log::debug!(
"Total RAM: {}",
re_format::format_bytes(sys.total_memory() as _)
);

total_memory
sys.total_memory()
}

/// Amount of available RAM on this machine.
Expand All @@ -26,7 +19,7 @@ pub fn total_ram_in_bytes() -> u64 {

pub struct RamLimitWarner {
total_ram_in_bytes: u64,
limit: u64,
warn_limit: u64,
has_warned: bool,
}

Expand All @@ -36,7 +29,7 @@ impl RamLimitWarner {
let limit = (fraction as f64 * total_ram_in_bytes as f64).round() as _;
Self {
total_ram_in_bytes,
limit,
warn_limit: limit,
has_warned: false,
}
}
Expand All @@ -47,7 +40,7 @@ impl RamLimitWarner {
let used = crate::MemoryUse::capture();
let used = used.counted.or(used.resident);
if let Some(used) = used {
if 0 <= used && self.limit <= used as u64 {
if 0 <= used && self.warn_limit <= used as u64 {
self.has_warned = true;
re_log::warn!(
"RAM usage is {} (with a total of {} system RAM). You may want to start Rerun with the --memory-limit flag to limit RAM usage.",
Expand Down
2 changes: 1 addition & 1 deletion crates/re_sdk_comms/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ fn spawn_client(
return;
}
}
re_log::warn!("Closing connection to client at {addr_string}: {err}");
re_log::warn_once!("Closing connection to client at {addr_string}: {err}");
let err: Box<dyn std::error::Error + Send + Sync + 'static> = err.to_string().into();
tx.quit(Some(err)).ok(); // best-effort at this point
}
Expand Down
2 changes: 1 addition & 1 deletion crates/re_sdk_comms/src/tcp_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ impl TcpClient {
re_log::trace!("Attempting to flush TCP stream…");
match &mut self.stream_state {
TcpStreamState::Pending { .. } => {
re_log::warn!(
re_log::warn_once!(
"Tried to flush while TCP stream was still Pending. Data was possibly dropped."
);
}
Expand Down
5 changes: 5 additions & 0 deletions crates/re_viewer/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,11 @@ impl App {
let mem_use_before = MemoryUse::capture();

if let Some(minimum_fraction_to_purge) = limit.is_exceeded_by(&mem_use_before) {
re_log::info_once!(
"Reached memory limit of {}, dropping oldest data.",
format_limit(limit.limit)
);

let fraction_to_purge = (minimum_fraction_to_purge + 0.2).clamp(0.25, 1.0);

re_log::trace!("RAM limit: {}", format_limit(limit.limit));
Expand Down
14 changes: 6 additions & 8 deletions crates/rerun/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ struct Args {

/// An upper limit on how much memory the Rerun Viewer should use.
///
/// When this limit is used, Rerun will purge the oldest data.
/// When this limit is reached, Rerun will drop the oldest data.
///
/// Example: `16GB`
#[clap(long)]
memory_limit: Option<String>,
/// Example: `16GB` or `50%` (of system total).
#[clap(long, default_value = "75%")]
memory_limit: String,

/// Whether the Rerun Viewer should persist the state of the viewer to disk.
///
Expand Down Expand Up @@ -415,10 +415,8 @@ async fn run_impl(

#[cfg(feature = "native_viewer")]
let startup_options = re_viewer::StartupOptions {
memory_limit: args.memory_limit.as_ref().map_or(Default::default(), |l| {
re_memory::MemoryLimit::parse(l)
.unwrap_or_else(|err| panic!("Bad --memory-limit: {err}"))
}),
memory_limit: re_memory::MemoryLimit::parse(&args.memory_limit)
.unwrap_or_else(|err| panic!("Bad --memory-limit: {err}")),
persist_state: args.persist_state,
screenshot_to_path_then_quit: args.screenshot_to.clone(),

Expand Down
6 changes: 2 additions & 4 deletions docs/content/howto/limit-ram.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ description: How to limit the memory of Rerun so that it doesn't run out of RAM.

### `--memory-limit`

The Rerun Viewer can not yet view more data than fits in RAM. The more data you log, the more RAM the Rerun Viewer will use. The RAM use will build up until you run out of memory. This can be fixed by starting the viewer from the command-line with the `--memory-limit` argument.
The Rerun Viewer can not yet view more data than fits in RAM. The more data you log, the more RAM the Rerun Viewer will use. When it reaches a certain limit, the oldest data will be dropped. The default limit it to use up to 75% of the total system RAM.

For instance, if you run `rerun --memory-limit 16GB` then the viewer will start throwing away the oldest logged so as not to go over that 16 GB limit.

NOTE: This currently only work when you are using [`rr.connect`](https://ref.rerun.io/docs/python/latest/common/initialization/#rerun.connect) to connect to a separate `rerun` process. There is currently no way of specifying a memory limit when using `rr.spawn`.
You can set the limit by with the `--memory-limit` command-lint argument, or the `memory_limit` argument of [`rr.spawn`](https://ref.rerun.io/docs/python/latest/common/initialization/#rerun.spawn).

### `--drop-at-latency`

Expand Down
11 changes: 2 additions & 9 deletions examples/python/live_camera_edge_detection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,7 @@ Very simple example of capturing from a live camera.

Runs the opencv canny edge detector on the image stream.

NOTE: this example currently runs forever and will eventually exhaust your
system memory. It is advised you run an independent rerun viewer with a memory
limit:
Usage:
```
rerun --memory-limit 4GB
```

And then connect using:
```
python examples/python/live_camera_edge_detection/main.py --connect
python examples/python/live_camera_edge_detection/main.py
```
32 changes: 0 additions & 32 deletions examples/python/live_camera_edge_detection/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,6 @@
Very simple example of capturing from a live camera.

Runs the opencv canny edge detector on the image stream.

NOTE: this example currently runs forever and will eventually exhaust your
system memory. It is advised you run an independent rerun viewer with a memory
limit:
```
rerun --memory-limit 4GB
```

And then connect using:
```
python examples/python/live_camera_edge_detection/main.py --connect
```

"""
from __future__ import annotations

Expand Down Expand Up @@ -74,25 +61,6 @@ def main() -> None:

rr.script_setup(args, "rerun_example_live_camera_edge_detection")

if not args.connect:
print(
"""
################################################################################
NOTE: this example currently runs forever and will eventually exhaust your
system memory. It is advised you run an independent rerun viewer with a memory
limit:
```
rerun --memory-limit 4GB
```

And then connect using:
```
python examples/python/live_camera_edge_detection/main.py --connect
```
################################################################################
"""
)

run_canny(args.num_frames)

rr.script_teardown(args)
Expand Down
11 changes: 2 additions & 9 deletions examples/python/live_depth_sensor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,7 @@ thumbnail: https://static.rerun.io/8b7fe937b90b05972e01b0e79b4b87dde4a47914_live

A minimal example of streaming frames live from an Intel RealSense depth sensor.

NOTE: this example currently runs forever and will eventually exhaust your
system memory. It is advised you run an independent rerun viewer with a memory
limit:
Usage:
```
rerun --memory-limit 4GB
```

And then connect using:
```
python examples/python/live_depth_sensor/main.py --connect
examples/python/live_depth_sensor/main.py
```
36 changes: 1 addition & 35 deletions examples/python/live_depth_sensor/main.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
#!/usr/bin/env python3
"""
A minimal example of streaming frames live from an Intel RealSense depth sensor.

NOTE: this example currently runs forever and will eventually exhaust your
system memory. It is advised you run an independent rerun viewer with a memory
limit:
```
rerun --memory-limit 4GB
```

And then connect using:
```
python examples/python/live_depth_sensor/main.py --connect
```

"""
"""A minimal example of streaming frames live from an Intel RealSense depth sensor."""
from __future__ import annotations

import argparse
Expand Down Expand Up @@ -108,25 +93,6 @@ def main() -> None:

rr.script_setup(args, "rerun_example_live_depth_sensor")

if not args.connect:
print(
"""
################################################################################
NOTE: this example currently runs forever and will eventually exhaust your
system memory. It is advised you run an independent rerun viewer with a memory
limit:
```
rerun --memory-limit 4GB
```

And then connect using:
```
python examples/python/live_depth_sensor/main.py --connect
```
################################################################################
"""
)

run_realsense(args.num_frames)

rr.script_teardown(args)
Expand Down
15 changes: 11 additions & 4 deletions rerun_py/rerun_sdk/rerun/sinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


def connect(
addr: str | None = None, flush_timeout_sec: float | None = 2.0, recording: RecordingStream | None = None
addr: str | None = None, *, flush_timeout_sec: float | None = 2.0, recording: RecordingStream | None = None
) -> None:
"""
Connect to a remote Rerun Viewer on the given ip:port.
Expand Down Expand Up @@ -109,6 +109,7 @@ def memory_recording(recording: RecordingStream | None = None) -> MemoryRecordin


def serve(
*,
open_browser: bool = True,
web_port: int | None = None,
ws_port: int | None = None,
Expand Down Expand Up @@ -141,7 +142,9 @@ def serve(
bindings.serve(open_browser, web_port, ws_port, recording=recording)


def spawn(port: int = 9876, connect: bool = True, recording: RecordingStream | None = None) -> None:
def spawn(
*, port: int = 9876, connect: bool = True, memory_limit: str = "75%", recording: RecordingStream | None = None
) -> None:
"""
Spawn a Rerun Viewer, listening on the given port.

Expand All @@ -156,7 +159,11 @@ def spawn(port: int = 9876, connect: bool = True, recording: RecordingStream | N
The port to listen on.
connect
also connect to the viewer and stream logging data to it.
recording:
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
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`][].
Expand All @@ -180,7 +187,7 @@ def spawn(port: int = 9876, connect: bool = True, recording: RecordingStream | N
# start_new_session=True ensures the spawned process does NOT die when
# we hit ctrl-c in the terminal running the parent Python process.
subprocess.Popen(
[python_executable, "-m", "rerun", "--port", str(port), "--skip-welcome-screen"],
[python_executable, "-m", "rerun", f"--port={port}", f"--memory-limit={memory_limit}", "--skip-welcome-screen"],
env=new_env,
start_new_session=True,
)
Expand Down