Summary
IRustServerQuery.GetMonumentsAsync is served by the Rust+ GetMap endpoint, which returns the entire map JPEG alongside the monument list. MapComposer calls it on every compose, and MapHostedService composes once per MapRefreshInterval (30s). The result is a ~683 KB download every 30 seconds, forever, to read a static list that the bot has already fetched and discarded earlier in the same connected window.
#85 fixed the same defect for GetMapDimensionsAsync. This issue covers the remaining caller.
Measured on a live bot (after #85, develop @ ff7ef7f)
Per-second bytes_received on the Rust+ socket, 309 s window:
23:12:07 BURST +698476 23:14:48 BURST +698875
23:12:39 BURST +636276 23:15:21 BURST +698875
23:13:10 BURST +699361 23:15:53 BURST +698875 (spanned 3s)
23:13:42 BURST +665004 23:16:27 BURST +698875
23:14:15 BURST +699118 (spanned 2s) 23:16:59 BURST +699031
TOTAL = 7,016,744 B over 309 s | 10 bursts | 22,707 B/s | 77 MB/h
- 10 bursts / 309 s = one per 30.9 s — matches
MapRefreshInterval: "00:00:30" exactly.
- ~683 KB per burst.
- 10 x ~699,000 = 6.99 MB of the 7.02 MB total.
Waste ratio
|
Bytes / 309 s |
Rate |
| Redundant map re-downloads |
6,990,000 |
76.7 MB/h |
| Everything else (marker polls, heartbeat, team poll, chat) |
26,744 |
0.31 MB/h |
| Redundant share |
99.6 % |
|
Marker polls on this server are 106–545 B each. The useful traffic is a rounding error next to the map re-downloads.
For scale: 1.85 GB/day on one server, and it scales linearly with the number of connected servers.
Root cause
RustPlusApi.Data.ServerMap (2.0.0-beta.8) bundles everything into one response:
public sealed record ServerMap
{
public uint? Height { get; init; }
public uint? Width { get; init; }
public int? OceanMargin { get; init; }
public Color Background { get; init; }
public List<ServerMapMonument>? Monuments { get; init; }
public byte[]? JpgImage { get; init; } // <- the ~683 KB
}
There is one endpoint, GetMapAsync, and three wrappers in RustPlusSocketSource.cs call it — so all three pay for the JPEG, but only one wants it:
| Wrapper |
Line |
Wants |
Cached? |
Callers |
GetMapImageAsync |
742 |
the JPEG |
yes — BaseMapCache holds it until disconnect |
RustPlusBaseMapSource:12 |
GetMapDimensionsAsync |
634 |
Width/Height/OceanMargin |
yes — fixed in #85, served from LiveSocket.Dimensions |
MapComposer:69, RustPlusBaseMapSource:18, ServerTeamMessageRenderer:78 |
GetMonumentsAsync |
708 |
Monuments |
no |
MapComposer:90 |
MapComposer.cs:90 is gated on layers.Monuments || layers.Rigs || layers.Tunnels. Those default to on (MapLayerSet.AllOn; MapSettings.Tunnels = true) and ServerMapSettings is empty on a default install — so in practice it always fires.
The data is already in memory and thrown away
ConnectionSupervisor.GetRigPositionsAsync fetches the exact same list once per connected window at marker-poll start, keeps only the two oil rigs, and discards the rest:
var monuments = await connection.GetMonumentsAsync(_options.HeartbeatTimeout, ct).ConfigureAwait(false);
foreach (var m in monuments)
{
RigKind? kind = m.Token switch
{
"oil_rig_small" => RigKind.Small,
"large_oil_rig" => RigKind.Large,
_ => null,
};
...
}
return rigs; // everything else dropped
So the bot pays ~683 KB every 30 s to re-fetch a list it already downloaded once and threw away.
Proposed fix
Same shape as #85:
- Add a
MonumentsHolder to LiveSocket, beside the existing DimensionsHolder.
- Populate it from the single connect-time fetch in
GetRigPositionsAsync (which already has the full list).
- Serve
ConnectionSupervisor.GetMonumentsAsync from the holder, falling back to one socket fetch if a reader beats the marker poll.
Monuments are static for a wipe, and the connected window is torn down and re-resolved on reconnect — exactly when a new map can appear. Same invariant that makes the #85 dimensions cache correct.
Worth considering as a follow-up: since GetMap yields dimensions, monuments and the image in a single response, one fetch per window could populate all three caches, dropping the connected-window cost to a single GetMap call instead of the current two (GetMapImageAsync for BaseMapCache + GetMonumentsAsync for rigs).
Expected impact
- Full-map downloads: ~120/hour to 1 per connected window.
- Rust+ inbound: 77 MB/h to ~0.3 MB/h (~99.6% reduction), i.e. 1.85 GB/day to ~10 MB/day per server.
Historical context
Before #85, the same server sustained 1,125,184,125 bytes over ~5h26m = 202 MB/h, with bursts every ~9 s (dimensions from the team panel + dimensions from the composer + monuments from the composer, all per-cycle). #85 removed the dimensions calls and cut this to 77 MB/h. This issue is the remainder.
How to reproduce / measure
With a bot connected to a Rust server:
ss -tnpi dst <server-ip> | grep -oE 'bytes_received:[0-9]+'
Sample once per second and diff. Bursts of ~683 KB appear on exactly the MapRefreshInterval cadence.
Secondary observation (separate, lower priority)
PATCH channels/.../messages/... rate-limit warnings fire ~1.8/min against the server.team info-panel message. RenderGate already suppresses identical re-renders, so these are genuine content changes (live player positions and marker ages), not redundant writes — but the edit rate still exceeds Discord's per-channel bucket and may deserve its own coalescing interval. Not part of this issue.
Summary
IRustServerQuery.GetMonumentsAsyncis served by the Rust+GetMapendpoint, which returns the entire map JPEG alongside the monument list.MapComposercalls it on every compose, andMapHostedServicecomposes once perMapRefreshInterval(30s). The result is a ~683 KB download every 30 seconds, forever, to read a static list that the bot has already fetched and discarded earlier in the same connected window.#85 fixed the same defect for
GetMapDimensionsAsync. This issue covers the remaining caller.Measured on a live bot (after #85,
develop@ ff7ef7f)Per-second
bytes_receivedon the Rust+ socket, 309 s window:MapRefreshInterval: "00:00:30"exactly.Waste ratio
Marker polls on this server are 106–545 B each. The useful traffic is a rounding error next to the map re-downloads.
For scale: 1.85 GB/day on one server, and it scales linearly with the number of connected servers.
Root cause
RustPlusApi.Data.ServerMap(2.0.0-beta.8) bundles everything into one response:There is one endpoint,
GetMapAsync, and three wrappers inRustPlusSocketSource.cscall it — so all three pay for the JPEG, but only one wants it:GetMapImageAsyncBaseMapCacheholds it until disconnectRustPlusBaseMapSource:12GetMapDimensionsAsyncWidth/Height/OceanMarginLiveSocket.DimensionsMapComposer:69,RustPlusBaseMapSource:18,ServerTeamMessageRenderer:78GetMonumentsAsyncMonumentsMapComposer:90MapComposer.cs:90is gated onlayers.Monuments || layers.Rigs || layers.Tunnels. Those default to on (MapLayerSet.AllOn;MapSettings.Tunnels = true) andServerMapSettingsis empty on a default install — so in practice it always fires.The data is already in memory and thrown away
ConnectionSupervisor.GetRigPositionsAsyncfetches the exact same list once per connected window at marker-poll start, keeps only the two oil rigs, and discards the rest:So the bot pays ~683 KB every 30 s to re-fetch a list it already downloaded once and threw away.
Proposed fix
Same shape as #85:
MonumentsHoldertoLiveSocket, beside the existingDimensionsHolder.GetRigPositionsAsync(which already has the full list).ConnectionSupervisor.GetMonumentsAsyncfrom the holder, falling back to one socket fetch if a reader beats the marker poll.Monuments are static for a wipe, and the connected window is torn down and re-resolved on reconnect — exactly when a new map can appear. Same invariant that makes the #85 dimensions cache correct.
Worth considering as a follow-up: since
GetMapyields dimensions, monuments and the image in a single response, one fetch per window could populate all three caches, dropping the connected-window cost to a singleGetMapcall instead of the current two (GetMapImageAsyncforBaseMapCache+GetMonumentsAsyncfor rigs).Expected impact
Historical context
Before #85, the same server sustained 1,125,184,125 bytes over ~5h26m = 202 MB/h, with bursts every ~9 s (dimensions from the team panel + dimensions from the composer + monuments from the composer, all per-cycle). #85 removed the dimensions calls and cut this to 77 MB/h. This issue is the remainder.
How to reproduce / measure
With a bot connected to a Rust server:
Sample once per second and diff. Bursts of ~683 KB appear on exactly the
MapRefreshIntervalcadence.Secondary observation (separate, lower priority)
PATCH channels/.../messages/...rate-limit warnings fire ~1.8/min against theserver.teaminfo-panel message.RenderGatealready suppresses identical re-renders, so these are genuine content changes (live player positions and marker ages), not redundant writes — but the edit rate still exceeds Discord's per-channel bucket and may deserve its own coalescing interval. Not part of this issue.