Files

200 lines
8.2 KiB
Python

"""Tests that estimate() normalizes arbitrary-order node/core grids (issue #11)."""
from nix_estimator.estimate import estimate, peak_ram_gb_per_node
def _toy():
"""A root fanning out to heavy leaves, joined at a sink.
Wide enough that node count matters, heavy enough that cores matter.
Durations are pinned via ``history`` so the grid is deterministic.
"""
def p(name):
return f"/nix/store/{'0' * 32}-{name}.drv"
leaves = [p(f"heavylib{i}") for i in range(4)]
root, sink = p("root"), p("sink")
closure = {d: {} for d in [root, *leaves, sink]}
preds = {root: [], sink: leaves, **{leaf: [root] for leaf in leaves}}
nodes = set(closure)
history = {"root": 1.0, "sink": 1.0, **{f"heavylib{i}": 20.0 for i in range(4)}}
return closure, preds, nodes, history
def test_unsorted_core_grid_matches_sorted():
closure, preds, nodes, history = _toy()
ref = estimate(closure, preds, nodes, history=history, core_grid=(8, 16, 32))
shuf = estimate(closure, preds, nodes, history=history, core_grid=(32, 8, 16))
assert shuf.recommendation == ref.recommendation
# normalization must not silently swallow the top core count
assert (
shuf.recommendation["one_big_node_min"]
== ref.recommendation["one_big_node_min"]
)
def test_unsorted_node_grid_matches_sorted():
closure, preds, nodes, history = _toy()
ref = estimate(closure, preds, nodes, history=history, node_grid=(1, 2, 4, 8))
shuf = estimate(closure, preds, nodes, history=history, node_grid=(8, 1, 4, 2))
assert shuf.recommendation == ref.recommendation
def test_unsorted_grids_duplicates_deduped():
closure, preds, nodes, history = _toy()
ref = estimate(closure, preds, nodes, history=history, core_grid=(8, 16))
dup = estimate(closure, preds, nodes, history=history, core_grid=(16, 8, 8, 16))
assert dup.recommendation == ref.recommendation
def test_recommend_node_grid_min_above_peak():
# Issue #18: if every node_grid value exceeds the graph's peak concurrency,
# the clamp target (peak) is below all grid keys. _recommend must snap back
# onto an existing grid value instead of KeyError-ing on at[peak].
def p(name):
return f"/nix/store/{'0' * 32}-{name}.drv"
# linear chain: peak concurrency == 1, well below any node_grid entry.
a, b, c = p("a"), p("b"), p("c")
closure = {a: {}, b: {}, c: {}}
preds = {a: [], b: [a], c: [b]}
nodes = set(closure)
history = {"a": 1.0, "b": 1.0, "c": 1.0}
est = estimate(closure, preds, nodes, history=history, node_grid=(4, 8, 16))
assert est.peak_parallelism == 1
# snapped down to the smallest available grid value
assert est.recommendation["nodes"] == 4
assert "est_makespan_min" in est.recommendation
def test_estimate_max_jobs_helps_wide_small_graph():
# Issue #13: a fan of many small, weakly-parallel derivations. max_jobs>1
# packs several per node; the per-job core split barely hurts small drvs, so
# the single-node makespan drops.
def p(name):
return f"/nix/store/{'0' * 32}-{name}.drv"
leaves = [p(f"small{i}") for i in range(12)]
root, sink = p("root"), p("sink")
closure = {d: {} for d in [root, *leaves, sink]}
preds = {root: [], sink: leaves, **{leaf: [root] for leaf in leaves}}
nodes = set(closure)
history = {"root": 0.5, "sink": 0.5, **{f"small{i}": 2.0 for i in range(12)}}
base = estimate(closure, preds, nodes, history=history, max_jobs=1)
packed = estimate(closure, preds, nodes, history=history, max_jobs=4)
assert packed.max_jobs == 4
# single node, 8 cores: 12 small builds serialize at mj=1 but pack at mj=4
assert packed.grid[(1, 8)] < base.grid[(1, 8)]
def test_estimate_ram_budget_lengthens_makespan():
# Issue #15: many RAM-heavy builds (ghc ~6 GB each) on nodes with a tight
# RAM budget run fewer-at-a-time than max_jobs allows -> longer makespan than
# the unconstrained run at the same (nodes, cores, max_jobs).
def p(name):
return f"/nix/store/{'0' * 32}-{name}.drv"
leaves = [p(f"ghc-{i}") for i in range(6)]
root = p("root")
closure = {d: {} for d in [root, *leaves]}
preds = {root: [], **{leaf: [root] for leaf in leaves}}
nodes = set(closure)
history = {"root": 0.5} # leaves fall through to the ghc heuristic (RAM ~6GB)
unc = estimate(closure, preds, nodes, history=history, max_jobs=4)
capped = estimate(
closure, preds, nodes, history=history, max_jobs=4, node_ram_gb=8.0
)
assert capped.node_ram_gb == 8.0
# 8 GB fits only one 6-GB ghc build per node at a time, so a single node is
# slower under the RAM cap than when 4 could pack.
assert capped.grid[(1, 8)] > unc.grid[(1, 8)]
def test_knee_picks_biggest_core_that_helps():
# one heavy single-threaded-ish pole: recommendation stays sane and the
# reported one-big-node uses the largest core count regardless of input order.
closure, preds, nodes, history = _toy()
est = estimate(closure, preds, nodes, history=history, core_grid=(32, 16, 8))
assert est.recommendation["cores_per_node"] in (8, 16, 32)
# node choice never exceeds the graph width (4 heavy leaves)
assert est.recommendation["nodes"] <= 4
def _p(name):
return f"/nix/store/{'0' * 32}-{name}.drv"
def test_peak_ram_reflects_heavy_derivation():
"""A chromium (10 GB) build forces peak RAM >= 10, not the 1 GB default (#19)."""
root = _p("root")
heavy = _p("chromium")
closure = {root: {}, heavy: {}}
preds = {root: [], heavy: [root]}
nodes = {root, heavy}
history = {"root": 1.0, "chromium": 30.0}
peak = peak_ram_gb_per_node(
closure, preds, nodes, cores=16, machines=1, history=history, max_jobs=1
)
assert peak >= 10.0
def test_peak_ram_sums_co_resident_jobs():
"""With max_jobs>1, two heavy leaves co-resident on one node sum their RAM."""
root = _p("root")
# gcc-<v> matches the anchored ^gcc(-|$) RAM_HEAVY key -> 3 GB each
leaves = [_p(f"gcc-{v}") for v in (13, 14)]
closure = {d: {} for d in [root, *leaves]}
preds = {root: [], **{leaf: [root] for leaf in leaves}}
nodes = set(closure)
history = {"root": 0.1, "gcc-13": 20.0, "gcc-14": 20.0}
# one node, two job slots -> both gcc builds run together
peak = peak_ram_gb_per_node(
closure, preds, nodes, cores=16, machines=1, history=history, max_jobs=2
)
assert peak >= 6.0 # 3 + 3
def test_peak_ram_empty_closure_is_zero():
assert peak_ram_gb_per_node({}, {}, set(), cores=8, machines=1) == 0.0
def _fetch_drv():
"""A fixed-output derivation: an output with a hash makes is_fixed_output true."""
return {"outputs": {"out": {"hash": "abc123", "hashAlgo": "sha256"}}}
def test_parallelism_excludes_fixed_output_fetches():
# Issue #21: root fans out to 3 CPU builds and 5 fetches. peak_parallelism
# must count only the CPU-bound drvs (3), and the fetch stats must be exact.
root = _p("root")
cpu = [_p(f"lib{i}") for i in range(3)]
fetches = [_p(f"src{i}") for i in range(5)]
closure = {root: {}, **{d: {} for d in cpu}, **{d: _fetch_drv() for d in fetches}}
preds = {root: [], **{d: [root] for d in cpu}, **{d: [root] for d in fetches}}
nodes = set(closure)
history = {"root": 1.0, **{f"lib{i}": 5.0 for i in range(3)}}
est = estimate(closure, preds, nodes, history=history)
# 3 CPU leaves run concurrently; the 5 fetches don't occupy a build slot.
assert est.peak_parallelism == 3
assert est.cpu_bound_count == 4 # root + 3 libs
assert est.fetch_count == 5
# each fixed-output fetch costs FIXED_OUTPUT = 0.3 min at baseline cores
assert est.fetch_min == 5 * 0.3
assert est.avg_parallelism > 0.0
def test_peak_parallelism_ignores_pure_fetch_fanout():
# A root with only fetch children: without the exclusion peak would be huge;
# with it the CPU ceiling is just the single non-fetch root.
root = _p("root")
fetches = [_p(f"src{i}") for i in range(20)]
closure = {root: {}, **{d: _fetch_drv() for d in fetches}}
preds = {root: [], **{d: [root] for d in fetches}}
nodes = set(closure)
est = estimate(closure, preds, nodes, history={"root": 1.0})
assert est.fetch_count == 20
assert est.cpu_bound_count == 1
assert est.peak_parallelism == 1