Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions build/dashboard/mining_dashboard/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@
LOW_RAM_BASE_GB = 3 # p2pool + proxy + tor + caddy + dashboard
LOW_RAM_LOCAL_MONERO_GB = 5
LOW_RAM_LOCAL_TARI_GB = 6
# The built-in miner's own footprint, counted only when local_miner.enabled: its RandomX
# dataset is 3 GB outright as 3×1 GB pages (the unfragmented-pool case), or ~2.4 GB as 2 MB
# pages plus per-thread scratchpads and process overhead — bench-measured via smaps while both
# sides mined. 3 GB covers the reservation either way. Deliberately NOT the 6 GB headroom the
# miner's rendered config declares (hugepages_reserve_extra_mb): that is the stack's OWN
# hugepage budget, which the container floors above already count — adding it here would count
# p2pool's dataset twice.
LOW_RAM_LOCAL_MINER_GB = 3


def local_miner_enabled(path=None):
"""Whether this box runs the built-in RigForge miner (config.json: local_miner.enabled),
read live off the same read-only masked-config mount as the worker descriptors below — no
env plumb, and a dashboard toggle moves the floor without a container restart. A missing or
unreadable file reads False (DIY stacks without the mount never run the built-in miner)."""
try:
with open(path or HOST_CONFIG_PATH) as f:
return json.load(f).get("local_miner", {}).get("enabled") is True
except (OSError, ValueError, AttributeError):
return False


def low_ram_floor_gb(monero_local: bool, tari_local: bool) -> float:
Expand All @@ -42,6 +62,7 @@ def low_ram_floor_gb(monero_local: bool, tari_local: bool) -> float:
LOW_RAM_BASE_GB
+ (LOW_RAM_LOCAL_MONERO_GB if monero_local else 0)
+ (LOW_RAM_LOCAL_TARI_GB if tari_local else 0)
+ (LOW_RAM_LOCAL_MINER_GB if local_miner_enabled() else 0)
)


Expand Down
58 changes: 58 additions & 0 deletions build/dashboard/tests/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,61 @@ def test_missing_file_reads_defaults(self, tmp_path):
from mining_dashboard.config.config import load_energy_config

assert load_energy_config(str(tmp_path / "absent.json")) == self.DEFAULTS


class TestLowRamFloorLocalMiner:
"""The mode-aware floor counts the built-in miner's share when local_miner.enabled — read
live off the same read-only masked-config mount as the worker descriptors, so a dashboard
toggle moves the floor without a restart. The increment is the miner's OWN ~3 GB dataset
reservation, never the 6 GB headroom its rendered config declares (that is the stack's own
hugepage budget, which the per-container floors already count)."""

def _write(self, tmp_path, payload):
p = tmp_path / "config.json"
p.write_text(payload if isinstance(payload, str) else json.dumps(payload))
return str(p)

def test_enabled_reads_true(self, tmp_path):
from mining_dashboard.config.config import local_miner_enabled

assert local_miner_enabled(self._write(tmp_path, {"local_miner": {"enabled": True}}))

def test_anything_else_reads_false(self, tmp_path):
# Only the literal boolean true counts; every degraded shape (off, absent, hand-edited
# junk, a non-object document) reads False — the floor must never inflate on a typo.
from mining_dashboard.config.config import local_miner_enabled

for payload in (
{"local_miner": {"enabled": False}},
{"local_miner": {}},
{},
{"local_miner": "yes"},
{"local_miner": {"enabled": "true"}},
[],
"{not json",
):
assert local_miner_enabled(self._write(tmp_path, payload)) is False, payload
assert local_miner_enabled(str(tmp_path / "absent.json")) is False

def test_floor_adds_miner_share_only_when_enabled(self, tmp_path, monkeypatch):
import mining_dashboard.config.config as cfg

monkeypatch.setattr(
cfg, "HOST_CONFIG_PATH", self._write(tmp_path, {"local_miner": {"enabled": True}})
)
# The Both role with both nodes local: 3 + 5 + 6 + 3 — a 16 GB box now warns, which is
# exactly the OOM edge the adversarial pass flagged.
assert cfg.low_ram_floor_gb(True, True) == 17
assert cfg.low_ram_floor_gb(False, False) == 6
# No mount (a DIY stack) or the miner off: the floors are what they always were.
monkeypatch.setattr(cfg, "HOST_CONFIG_PATH", str(tmp_path / "absent.json"))
assert cfg.low_ram_floor_gb(True, True) == 14

def test_low_ram_gb_env_pin_still_wins(self, tmp_path, monkeypatch):
import mining_dashboard.config.config as cfg

monkeypatch.setattr(
cfg, "HOST_CONFIG_PATH", self._write(tmp_path, {"local_miner": {"enabled": True}})
)
monkeypatch.setattr(cfg, "_LOW_RAM_ENV", "9")
assert cfg.low_ram_floor_gb(True, True) == 9.0
15 changes: 15 additions & 0 deletions build/dashboard/tests/web/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,21 @@ def test_low_ram_badge_tracks_what_runs_locally(self, monkeypatch):
out = build_badges({"system": {"memory": {"total_gb": 8}}}, _metrics(), "ok")
assert not any("Low RAM" in b["text"] for b in out)

def test_low_ram_badge_counts_the_built_in_miner(self, monkeypatch):
# The Both role's risk case: both nodes local fits a 16 GB box (floor 14) — until the
# built-in miner's own dataset joins them, when the same box honestly warns (floor 17).
import mining_dashboard.config.config as cfg_mod
import mining_dashboard.web.views as views_mod

monkeypatch.setattr(views_mod, "monero_is_local", lambda: True)
monkeypatch.setattr(views_mod, "tari_is_local", lambda: True)
state = {"system": {"memory": {"total_gb": 15.6}}}
assert not any("Low RAM" in b["text"] for b in build_badges(state, _metrics(), "ok"))

monkeypatch.setattr(cfg_mod, "local_miner_enabled", lambda path=None: True)
out = build_badges(state, _metrics(), "ok")
assert any(b["variant"] == "warn" and "Low RAM (16 GB)" in b["text"] for b in out)

def test_no_low_ram_badge_at_or_above_threshold_or_unknown(self):
# 15.6 is what a NOMINAL 16 GB machine actually reports (reserved memory, GiB-vs-GB) —
# the documented minimum spec must never wear a permanent warning. Bench-reported.
Expand Down
2 changes: 1 addition & 1 deletion docs/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ The top bar also surfaces the persistent host conditions that `setup` warns abou
| Badge | Means | Fix |
|---|---|---|
| `⚠ HugePages off` | HugePages aren't reserved — RandomX hashrate is capped. | Run setup's tuning (or edit GRUB) and reboot; the badge clears once they're reserved. |
| `⚠ Low RAM (N GB)` | Under what this machine's workload wants: ~14 GB running both nodes locally, ~8 GB with one remote, ~3 GB with both remote. Remote nodes take their memory appetite with them; a nominal 16 GB machine reports ~15 and is fine for the full stack. | Add RAM, or point a node at a machine that has it. |
| `⚠ Low RAM (N GB)` | Under what this machine's workload wants: ~14 GB running both nodes locally, ~8 GB with one remote, ~3 GB with both remote, plus ~3 GB more when the appliance's built-in miner is on (its RandomX dataset holds that much alongside the stack's own). Remote nodes take their memory appetite with them; a nominal 16 GB machine reports ~15 and is fine for the full stack — but mining on the same box as both local nodes wants more. | Add RAM, point a node at a machine that has it, or mine from a separate rig. |
| `⚠ Memory pressure (N GB free)` | Live signal: under 1.5 GB actually available right now, whatever the machine's size — the next spike can OOM a container. | Check which service is growing on the System panel. |
| `⚠ No AVX2` | The CPU lacks AVX2, so RandomX mining is much slower. | A hardware limit; nothing to change at runtime. |
| `⚠ Payout wallet changed` | The wallet p2pool mines to changed within the last 72 hours (old → new, truncated). A confirmation if you changed it; an alarm if you didn't. | Verify `monero.wallet_address` in `config.json`; see [Operations › wallet changes](operations.md). The badge expires on its own after 72 h. |
Expand Down