Skip to content

feat(s57): cold-cache mitigation — faster polling, timing logs, precompute, buffer param#20

Merged
rolker merged 3 commits into
jazzyfrom
feature/issue-19
Apr 22, 2026
Merged

feat(s57): cold-cache mitigation — faster polling, timing logs, precompute, buffer param#20
rolker merged 3 commits into
jazzyfrom
feature/issue-19

Conversation

@rolker

@rolker rolker commented Apr 22, 2026

Copy link
Copy Markdown
Owner

Summary

Four independent knobs/mechanisms in s57_grids and s57_layer to mitigate the S57 chart cold-cache latency identified in the deployment-log analysis. None couple the costmap layer to mission_manager / BT publishing patterns.

s57_grids

  1. check_new_grids_period parameter, default 0.1 s (was hardcoded 1.0 s in grid_publisher.cpp:79). Faster reaping of completed std::async futures.

  2. Per-chart timing logs at INFO level: "Grid ready: <label> (X.X s from request to publish)". Records wall-clock from first request to publish. Lets us measure S57Dataset::getGrid cost in the field without recompile.

  3. Precompute on first available pose. New parameters:

    • robot_base_frame (default "" = disabled)
    • precompute_radius (meters, default 5000.0)

    On first tf_buffer_->lookupTransform(map_frame, robot_base_frame) success, queues all charts within precompute_radius of the boat for std::async processing via the existing requested_grids_ path. Single-shot — sets did_precompute_ on success and stops checking. TF-only trigger; no goal-source coupling.

s57_layer

  1. buffer_fraction parameter, default 0.05 (preserves the prior hardcoded value at s57_layer.cpp:190). Configs that want a larger pre-load ring around the rolling costmap window can override without code changes. Negative values rejected with warning, fall back to default.

Closes #19. Related: rolker/unh_marine_navigation#19.

Test plan

  • colcon build --packages-select s57_grids s57_layer — clean.
  • colcon test --packages-select s57_layer — 23 tests, 0 errors, 0 failures, 5 skipped. TideOffsetTest 5/5 passing (default buffer_fraction unchanged).
  • Sim run with default params on deadpool 2026-04-22 — per-chart timing logs visible at INFO. Sample output:
    Grid ready: US5PSMBC.000 (0.776515 s from request to publish)
    Grid ready: US5PSMBD.000 (1.05579 s from request to publish)
    Grid ready: US5PSMCC.000 (4.48606 s from request to publish)
    Grid ready: US4NH1BD.000 (4.97269 s from request to publish)
    Grid ready: US5PSMCD.000 (5.20193 s from request to publish)
    Grid ready: US2EC03M.000 (7.14701 s from request to publish)
    Grid ready: US3EC10M.000 (11.251 s from request to publish)
    Grid ready: US2EC04M.000 (13.0761 s from request to publish)
    
    Wide-area East Coast charts (US3EC10M, US2EC04M) take 11–13 s — S57Dataset::getGrid is the dominant component, not the polling loop.
  • Precompute live verification: requires a companion change in rolker/ben_project11 to set robot_base_frame: ben/base_link (and optionally precompute_radius) on /**/s57_grids in ben.yaml. Will follow as a separate small PR. The fall-through path (defaults) is exercised by the sim run above.

Out of scope (separate work)

  • Goal-driven prefetch (subscribing s57_layer to mission goals). Would couple costmap layer to mission_manager / BT; explicitly avoided.
  • Direction-of-travel asymmetric buffer in s57_layer. Possible follow-up if symmetric buffer_fraction isn't enough.
  • Persistent on-disk grid cache. Bigger architectural change with cache-invalidation design questions.
  • Parallelizing the feature loop inside S57Dataset::getGrid. Now that timing data identifies it as the bottleneck for wide-area charts, it's worth its own issue.

Authored-By: Claude Code Agent
Model: Claude Opus 4.7 (1M context)

…mpute, buffer param

Four bundled changes to reduce S57 chart cold-cache latency for the
costmap stack. None of them couple s57_layer to mission_manager / BT
publishing patterns; all use signals already available to the costmap
layer (TF, costmap window, lifecycle).

s57_grids changes:

1. check_new_grids_period parameter, default 0.1 s (was hardcoded
   1.0 s). Reaps completed std::async futures sooner, removing up to
   1 s per chart from publish latency. Verified via per-chart timing
   log: US5PSMBC published 0.78 s after request (would have been
   0.78–1.78 s with old 1 s timer).

2. Per-chart timing logs at INFO level. Records wall-clock time from
   first request to publish. Format:
     "Grid ready: <label> (X.X s from request to publish)"
   Sim run on deadpool 2026-04-22 showed the slowest two charts
   (US3EC10M, US2EC04M) take 11–13 s — pinpoints S57Dataset::getGrid
   as the dominant cost component for wide-area East Coast charts,
   not the polling loop.

3. Precompute on first available pose. New parameters:
     robot_base_frame  — when set, polls TF (map_frame, this) until
                         lookup succeeds, then queues all charts
                         within precompute_radius of that position.
                         Default empty = disabled.
     precompute_radius — meters, default 5000.0 (matches typical
                         global-costmap window with buffer).
   Trigger uses TF only; no goal subscription. Single-shot — sets
   did_precompute_ on success and stops trying. Equirectangular
   approximation for the bounding box (good to <1% for ~50 km radii
   outside polar regions). Queues via the existing requested_grids_
   path so cached grids are published latched and any S57Layer that
   subscribes later (including post-precompute) gets them.

s57_layer change:

4. buffer_fraction parameter, default 0.05 (preserves the prior
   hardcoded value). Configs that want a wider pre-load ring around
   the rolling costmap window can override without code changes.
   Negative values are rejected with a warning and fall back to 0.05.

Existing TideOffsetTest suite (5/5) passes unchanged. Build clean.

Closes #19
Related: rolker/unh_marine_navigation#19

---
Authored-By: Claude Code Agent
Model: Claude Opus 4.7 (1M context)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 22, 2026 05:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds several parameterized mechanisms in s57_grids and s57_layer to reduce S57 cold-cache latency by polling completed grid futures more frequently, logging per-chart latency at INFO, optionally precomputing nearby charts once TF is available, and making the S57Layer bounds buffer configurable.

Changes:

  • Add buffer_fraction parameter to S57Layer to control buffered chart-request bounds (default preserves prior 5% behavior).
  • Add check_new_grids_period parameter and INFO timing logs to s57_grids grid publishing path.
  • Add TF-triggered, one-shot precompute (robot_base_frame, precompute_radius) to queue nearby charts early.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
s57_layer/src/s57_layer.h Documents and introduces buffer_fraction_ member (default 0.05).
s57_layer/src/s57_layer.cpp Declares/reads buffer_fraction parameter and uses it to compute buffered bounds.
s57_grids/src/grid_publisher.h Adds members for timing logs, polling period parameter, and precompute configuration/state.
s57_grids/src/grid_publisher.cpp Implements configurable polling period, per-chart INFO timing logs, and TF-based precompute queuing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread s57_grids/src/grid_publisher.cpp Outdated
Comment on lines 167 to 175
auto now = get_clock()->now();
for(const auto& d: response->datasets)
{
requested_grids_.push_back(d.label);
requested_grids_to_publish_.push_back(d.label);
// Only record start time on the first request for this label, so
// re-requests don't reset the measurement.
grid_request_start_times_.emplace(d.label, now);
}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

requested_grids_to_publish_ is append-only (labels are pushed here on every request, including precompute) but is never pruned, and later membership is checked with a linear std::find(...). With the new precompute path potentially queuing many charts at once, this vector can grow without bound and make the publish check increasingly expensive. Consider using a std::unordered_set for membership and removing labels once published (or otherwise bounding its size).

Copilot uses AI. Check for mistakes.
Comment thread s57_grids/src/grid_publisher.cpp Outdated
Comment on lines +313 to +318
geographic_msgs::msg::BoundingBox bounds;
bounds.min_pt.latitude = lat - precompute_radius_ / meters_per_deg_lat;
bounds.max_pt.latitude = lat + precompute_radius_ / meters_per_deg_lat;
bounds.min_pt.longitude = lon - precompute_radius_ / meters_per_deg_lon;
bounds.max_pt.longitude = lon + precompute_radius_ / meters_per_deg_lon;

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

The catalog’s intersects() check assumes minLon <= maxLon and does not handle dateline wrapping (see marine_charts::S57Dataset::intersects). Here lon ± delta can exceed the valid [-180, 180] range or cross the dateline, which will make intersectingCharts(bounds) miss charts (or behave incorrectly). Consider normalizing/clamping longitudes and handling the wrap case by splitting into two bounding boxes when needed (and clamping latitudes to [-90, 90]).

Copilot uses AI. Check for mistakes.
Comment thread s57_layer/src/s57_layer.cpp Outdated
Comment on lines +57 to +63
declareParameter("buffer_fraction", rclcpp::ParameterValue(buffer_fraction_));
node->get_parameter(name_+".buffer_fraction", buffer_fraction_);
if(buffer_fraction_ < 0.0)
{
RCLCPP_WARN_STREAM(logger_,
"Invalid buffer_fraction value " << buffer_fraction_ << "; using 0.05 instead.");
buffer_fraction_ = 0.05;

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

buffer_fraction_ is only checked for negative values. If it is NaN/Inf (or otherwise non-finite), buffer becomes non-finite in updateBounds, which can prevent bounds updates and block dataset requests. Consider validating with std::isfinite(buffer_fraction_) && buffer_fraction_ >= 0.0, and fall back to the previous default value (captured before get_parameter) instead of hardcoding 0.05 in the warning path.

Suggested change
declareParameter("buffer_fraction", rclcpp::ParameterValue(buffer_fraction_));
node->get_parameter(name_+".buffer_fraction", buffer_fraction_);
if(buffer_fraction_ < 0.0)
{
RCLCPP_WARN_STREAM(logger_,
"Invalid buffer_fraction value " << buffer_fraction_ << "; using 0.05 instead.");
buffer_fraction_ = 0.05;
const double default_buffer_fraction = buffer_fraction_;
declareParameter("buffer_fraction", rclcpp::ParameterValue(buffer_fraction_));
node->get_parameter(name_+".buffer_fraction", buffer_fraction_);
if(!std::isfinite(buffer_fraction_) || buffer_fraction_ < 0.0)
{
RCLCPP_WARN_STREAM(logger_,
"Invalid buffer_fraction value " << buffer_fraction_
<< "; using " << default_buffer_fraction << " instead.");
buffer_fraction_ = default_buffer_fraction;

Copilot uses AI. Check for mistakes.
Comment on lines 66 to 100
@@ -76,10 +95,16 @@ GridPublisher::on_configure(const rclcpp_lifecycle::State &state)
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);

new_grids_timer_ = create_wall_timer(
std::chrono::milliseconds(1000),
std::chrono::milliseconds(int(1000.0 * check_new_grids_period_)),
std::bind(&GridPublisher::checkForNewGrids, this)
);

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

check_new_grids_period_ is taken from a ROS parameter but not validated. Values <= 0, NaN/Inf, or very small values can result in a 0ms/negative timer period after int(1000.0 * check_new_grids_period_), which can throw or create a tight loop. Please validate the parameter (finite and > 0), and clamp the resulting timer duration to a sane minimum (e.g., >= 1ms) before creating the wall timer.

Copilot uses AI. Check for mistakes.
{
declare_parameter("precompute_radius", rclcpp::ParameterValue(precompute_radius_));
}
precompute_radius_ = get_parameter("precompute_radius").as_double();

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

precompute_radius_ is read from a parameter but never validated. NaN/Inf or negative values will produce invalid bounding boxes (NaNs/Infs) and can cause catalog intersection queries to behave unpredictably. Consider validating it (finite and >= 0) at configure time and disabling precompute (or falling back to the default) when invalid.

Suggested change
precompute_radius_ = get_parameter("precompute_radius").as_double();
const double precompute_radius = get_parameter("precompute_radius").as_double();
if(std::isfinite(precompute_radius) && precompute_radius >= 0.0)
{
precompute_radius_ = precompute_radius;
}
else
{
RCLCPP_WARN_STREAM(
get_logger(),
"Invalid precompute_radius parameter (" << precompute_radius
<< "). Expected a finite value >= 0. Disabling precompute."
);
precompute_radius_ = 0.0;
}

Copilot uses AI. Check for mistakes.
Comment thread s57_grids/src/grid_publisher.cpp Outdated
Comment on lines +316 to +318
bounds.min_pt.longitude = lon - precompute_radius_ / meters_per_deg_lon;
bounds.max_pt.longitude = lon + precompute_radius_ / meters_per_deg_lon;

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

meters_per_deg_lon can become 0 (or extremely small) near the poles because it is scaled by cos(lat). This makes the longitude bounds division blow up (Inf/NaN) and can queue essentially the entire catalog. Add a guard for small |meters_per_deg_lon| (or avoid the equirectangular approximation by using a more robust geodesic/bounds computation) before computing bounds.min_pt.longitude/max_pt.longitude.

Suggested change
bounds.min_pt.longitude = lon - precompute_radius_ / meters_per_deg_lon;
bounds.max_pt.longitude = lon + precompute_radius_ / meters_per_deg_lon;
// Near the poles, meters_per_deg_lon approaches zero and the simple
// equirectangular longitude span can become non-finite or effectively
// global. In that case, search across the full longitude range.
constexpr double kMinMetersPerDegLon = 1e-9;
if(std::abs(meters_per_deg_lon) <= kMinMetersPerDegLon)
{
bounds.min_pt.longitude = -180.0;
bounds.max_pt.longitude = 180.0;
}
else
{
const double lon_delta = precompute_radius_ / meters_per_deg_lon;
if(!std::isfinite(lon_delta) || std::abs(lon_delta) >= 180.0)
{
bounds.min_pt.longitude = -180.0;
bounds.max_pt.longitude = 180.0;
}
else
{
bounds.min_pt.longitude = lon - lon_delta;
bounds.max_pt.longitude = lon + lon_delta;
}
}

Copilot uses AI. Check for mistakes.
@rolker

rolker commented Apr 22, 2026

Copy link
Copy Markdown
Owner Author

Precompute path verified end-to-end in a follow-up sim run on deadpool 2026-04-22 with robot_base_frame=ben/base_link and precompute_radius=5000.0 overridden via install-only yaml hack:

[s57_grids_node-10] [INFO] Precompute enabled: will queue charts within 5000 m of ben/base_link when TF first becomes available.
[s57_grids_node-10] [INFO] Precompute: queued 10 of 10 charts within 5000 m of (43.0734, -70.7106).

Precompute fired 0.6 s after node activation (TF availability). Queued 10 charts vs 8 in the costmap-layer-driven baseline — the 5 km radius caught two additional charts (US5PSMCE, US5PSMBE) outside the costmap window, pre-warming them for future use.

Companion ben_project11 yaml PR (to enable precompute by default) will follow.


Authored-By: Claude Code Agent
Model: Claude Opus 4.7 (1M context)

…teline/pole handling, bounded request set

Copilot flagged six issues; all are valid for global deployment.

s57_layer:
- buffer_fraction: validate with isfinite (was negative-only) and fall
  back to the captured default instead of hardcoding 0.05. Matches the
  established tide_invalidate_threshold pattern in this file.

s57_grids:
- check_new_grids_period: validate (isfinite + > 0); clamp the wall-timer
  duration to >= 1 ms so a misconfigured value can never produce a
  zero-duration timer.
- precompute_radius: validate (isfinite + >= 0).
- tryPrecompute: clamp latitude to [-90, 90]; near the poles
  (cos(lat) ≈ 0) the equirectangular lon span becomes ill-defined, so
  fall back to a full longitude sweep. When the radius wraps the
  dateline, split the query into two bounding boxes and dedupe results
  by chart label — S57Dataset::intersects assumes minLon <= maxLon and
  does not handle wrap.
- requested_grids_to_publish_: convert from std::vector to
  std::unordered_set and erase entries after publish. The vector was
  append-only and queried with linear std::find, which would grow
  unbounded across long missions.

Tested in worktree (./core_ws/build.sh + ./core_ws/test.sh): 41 tests, 0
failures. Field verification of the precompute path on PR #20 still
applies; these changes don't alter the NH-coast code path, only the
behaviour at high latitudes / on the dateline.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 6 to +7
#include "nav_msgs/msg/occupancy_grid.hpp"
#include <cmath>

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

This file now directly uses std::max/std::min (and already uses std::getenv) but doesn’t include <algorithm> / <cstdlib> explicitly. It may compile today due to transitive includes, but that’s not guaranteed. Consider adding the standard headers for the standard-library symbols used here to keep the translation unit self-contained.

Suggested change
#include "nav_msgs/msg/occupancy_grid.hpp"
#include <cmath>
#include "nav_msgs/msg/occupancy_grid.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib>

Copilot uses AI. Check for mistakes.
Comment on lines 185 to 195
listDatasets(request, response);
std::lock_guard<std::mutex> lock(requested_grids_mutex);
auto now = get_clock()->now();
for(const auto& d: response->datasets)
{
requested_grids_.push_back(d.label);
requested_grids_to_publish_.push_back(d.label);
requested_grids_to_publish_.insert(d.label);
// Only record start time on the first request for this label, so
// re-requests don't reset the measurement.
grid_request_start_times_.emplace(d.label, now);
}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

getDatasets() unconditionally adds every returned label to requested_grids_to_publish_ and grid_request_start_times_. If a grid was already published earlier (so grid_publishers_ already contains the label and no new future will be started), these entries will never be erased because cleanup only happens when a pending future completes. This will leak memory over time and can also leave stale start times around indefinitely. Consider only inserting into requested_grids_to_publish_ / grid_request_start_times_ when the label is not already available (e.g., no existing publisher/grid and no pending future), or add a cleanup path that removes entries for labels that are already published/cached.

Copilot uses AI. Check for mistakes.
Comment on lines +249 to +262
// Report time from first request to publish for this chart.
// Useful for diagnosing cold-cache behaviour without recompiling.
auto start_it = grid_request_start_times_.find(pg.first);
if(start_it != grid_request_start_times_.end())
{
auto elapsed = (get_clock()->now() - start_it->second).seconds();
RCLCPP_INFO_STREAM(get_logger(),
"Grid ready: " << pg.first << " (" << elapsed << " s from request to publish)");
grid_request_start_times_.erase(start_it);
}
else
{
RCLCPP_INFO_STREAM(get_logger(), "Grid ready: " << pg.first);
}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

The timing log is described as “wall-clock” but uses get_clock()->now() / rclcpp::Time (ROS time). With use_sim_time or time jumps, elapsed durations can be inaccurate or even negative. For latency measurements, record timestamps using a steady/monotonic source (e.g., std::chrono::steady_clock or an rclcpp::Clock(RCL_STEADY_TIME)) and compute elapsed from that.

Suggested change
// Report time from first request to publish for this chart.
// Useful for diagnosing cold-cache behaviour without recompiling.
auto start_it = grid_request_start_times_.find(pg.first);
if(start_it != grid_request_start_times_.end())
{
auto elapsed = (get_clock()->now() - start_it->second).seconds();
RCLCPP_INFO_STREAM(get_logger(),
"Grid ready: " << pg.first << " (" << elapsed << " s from request to publish)");
grid_request_start_times_.erase(start_it);
}
else
{
RCLCPP_INFO_STREAM(get_logger(), "Grid ready: " << pg.first);
}
// A request-to-publish latency should be measured with a steady clock.
// Do not compute it from ROS time here, because ROS time may jump,
// pause, or run under use_sim_time.
auto start_it = grid_request_start_times_.find(pg.first);
if(start_it != grid_request_start_times_.end())
{
grid_request_start_times_.erase(start_it);
}
RCLCPP_INFO_STREAM(get_logger(), "Grid ready: " << pg.first);

Copilot uses AI. Check for mistakes.
Comment thread s57_grids/src/grid_publisher.cpp Outdated
Comment on lines +333 to +334
const double meters_per_deg_lat = 111000.0;
const double meters_per_deg_lon = 111000.0 * std::cos(lat * M_PI / 180.0);

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

M_PI is a non-standard macro and may be undefined depending on platform / standard library feature macros. To avoid a portability build break, prefer a standard C++17 approach (e.g., std::acos(-1.0) or a local constexpr pi constant) when converting degrees to radians.

Suggested change
const double meters_per_deg_lat = 111000.0;
const double meters_per_deg_lon = 111000.0 * std::cos(lat * M_PI / 180.0);
constexpr double kPi = 3.14159265358979323846;
const double meters_per_deg_lat = 111000.0;
const double meters_per_deg_lon = 111000.0 * std::cos(lat * kPi / 180.0);

Copilot uses AI. Check for mistakes.
Comment thread s57_grids/src/grid_publisher.cpp Outdated
Comment on lines +438 to +447
for(const auto& chart: charts)
{
const std::string& label = chart->label();
if(grid_request_start_times_.count(label) == 0)
{
requested_grids_.push_back(label);
requested_grids_to_publish_.insert(label);
grid_request_start_times_.emplace(label, now);
++queued;
}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

tryPrecompute() queues labels into requested_grids_to_publish_ / grid_request_start_times_ based only on grid_request_start_times_. If precompute runs after some charts were already published, those labels can be re-inserted even though no new async future will be created (because grid_publishers_ already exists), leaving entries that never get erased. The same gating/cleanup suggested for getDatasets() should be applied here (skip labels that are already published/cached, or actively remove them).

Copilot uses AI. Check for mistakes.
…eak gating, steady-clock latency, portability

- Gate insertions in getDatasets and tryPrecompute on
  grid_publishers_ / pending_dataset_grids_ membership. Previously a
  re-request for an already-published chart would add entries to
  requested_grids_to_publish_ and grid_request_start_times_ that never
  got cleaned up (cleanup only runs when a pending future completes,
  and no future is created for charts already in grid_publishers_).
- Switch request-to-publish latency tracking from rclcpp::Time to
  std::chrono::steady_clock. ROS time can pause, jump, or run under
  use_sim_time — our deadpool sim in fact uses sim time — so the
  reported "request to publish" seconds could be inaccurate or
  negative. Steady clock measures actual wall-clock elapsed regardless.
- Replace M_PI (POSIX, non-standard C++) with a constexpr kPi.
- Add explicit <algorithm>, <cstdlib>, <chrono> includes for symbols
  already used (std::max, std::getenv, steady_clock) — they compile
  today via transitive includes but the translation unit should be
  self-contained.

Build + tests green in worktree: 41 tests, 0 failures.
@rolker
rolker merged commit e8d57d3 into jazzy Apr 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S57 cold-cache mitigation: faster polling, timing logs, precompute, buffer param

2 participants