gdharness

Spread benches

gdharness spread plays a bench's runs over several headless engines at once and prints the same table the bench prints when it runs alone, to the last digit. A bench whose runs share nothing (run i depends on i alone, its seed worked out from the index) is cut into one share per worker, and the outcomes of every run are gathered back in run order before the bench folds them.

gdharness spread res://bench/balance.tscn
gdharness spread res://bench/balance.tscn --workers 4
gdharness spread res://bench/balance.tscn -- --seed=3

The first takes as many workers as the machine has room for, and the last hands --seed=3 to every engine.

It exits 0 when every worker played its whole share and the gathering run printed no error, 1 when anything went wrong, and 2 when the command line itself is wrong.

Writing the bench

The bench is a scene. For each arm, it asks GdhShare.outcomes for the outcomes of that arm's runs, handing it the arm's name, the number of runs and a callable that plays run i and answers what the run came to. GdhShare is in the tests addon, addons/gdharness_tests, which gdharness setup installs.

extends Node


func _ready() -> void:
 var balance: Array = GdhShare.outcomes("balance", 120, _played)
 if not GdhShare.working():
  var totals: Dictionary[String, int] = GdhShare.added(balance, ["most_turns"])
  print("won %d of 120, longest %d turns" % [totals["won"], totals["most_turns"]])
 get_tree().quit()


func _played(run: int) -> Dictionary:
 var duel: Duel = Duel.new(SEED + run)
 duel.play()
 return {"won": 1 if duel.won else 0, "most_turns": duel.turns}

The same scene runs three ways, and GdhShare.outcomes answers in each:

The bench folds the outcomes the same way in all three, so a float summed in run order comes out the same as when the bench runs alone. GdhShare.added adds up outcomes that are dictionaries of whole numbers key by key, and keeps the largest for the keys it is given.

What a bench has to keep to:

No engine of a spread run announces itself to the gdharness server: the runtime stays off in an engine handed the spread's arguments, whatever folder its scene is in.

What fails a run

A spread run fails before anything is gathered when a worker:

It fails after the gathering when the gathering engine printed an error or exited with a code other than 0, or never asked for an arm the workers played. The gathering engine's output is printed either way, followed by the faults, so a table printed by a run that failed is marked as one. Each engine's output is in .godot/gdharness/spread-runs/<start>-<pid>/<engine>/engine.log, which a failed run names and keeps until five later runs have failed.