Add benchmark for saved state performance
- Introduced `benchmark_saved_state.gd` to evaluate the performance of the saved state restoration process. - Implemented JSON integer restoration to handle large integers accurately. - Added command-line argument parsing for specifying the path to the diagnostic JSON file and optional delta seconds. - Conducted performance tests at different speeds and logged the elapsed time for six consecutive steps. - Created a new UID file for the benchmark script.
This commit is contained in:
+1
-3
@@ -83,10 +83,8 @@ func _ready() -> void:
|
|||||||
Locales["en"] = _read_from_csv("res://data/locale.en.txt")
|
Locales["en"] = _read_from_csv("res://data/locale.en.txt")
|
||||||
Locales["ru"] = _read_from_csv("res://data/locale.ru.txt")
|
Locales["ru"] = _read_from_csv("res://data/locale.ru.txt")
|
||||||
|
|
||||||
Templates["en"] = _read_from_csv("res://data/templates.en.txt")
|
|
||||||
Templates["ru"] = _read_from_csv("res://data/templates.ru.txt")
|
|
||||||
|
|
||||||
#Templates = OwniverseTemplates.get_data()
|
Templates = OwniverseTemplate.get_data()
|
||||||
|
|
||||||
|
|
||||||
func _read_from_csv(file_path: String) -> Dictionary:
|
func _read_from_csv(file_path: String) -> Dictionary:
|
||||||
|
|||||||
+10
-10
@@ -1,15 +1,16 @@
|
|||||||
class_name CSVLoader
|
class_name CSVLoader
|
||||||
|
|
||||||
|
const SPLITTER = '|'
|
||||||
|
|
||||||
static func load(csv_text: String) -> Array[Dictionary]:
|
static func load(csv_text: String) -> Dictionary:
|
||||||
var rows: Array[Dictionary] = []
|
var result: Dictionary = {}
|
||||||
|
|
||||||
var lines := csv_text.strip_edges().split("\n")
|
var lines := csv_text.strip_edges().split("\n")
|
||||||
|
|
||||||
if lines.size() < 2:
|
if lines.size() < 2:
|
||||||
return rows
|
return result
|
||||||
|
|
||||||
var headers := lines[0].split(",")
|
var headers := lines[0].split(SPLITTER)
|
||||||
|
|
||||||
for i in range(1, lines.size()):
|
for i in range(1, lines.size()):
|
||||||
var line := lines[i].strip_edges()
|
var line := lines[i].strip_edges()
|
||||||
@@ -17,7 +18,7 @@ static func load(csv_text: String) -> Array[Dictionary]:
|
|||||||
if line.is_empty():
|
if line.is_empty():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var values := line.split(",")
|
var values := line.split(SPLITTER)
|
||||||
|
|
||||||
if values.size() != headers.size():
|
if values.size() != headers.size():
|
||||||
push_warning(
|
push_warning(
|
||||||
@@ -26,11 +27,10 @@ static func load(csv_text: String) -> Array[Dictionary]:
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var row: Dictionary = {}
|
var elem: Dictionary = {}
|
||||||
|
|
||||||
for j in range(headers.size()):
|
for j in range(headers.size()):
|
||||||
row[headers[j].strip_edges()] = values[j].strip_edges()
|
elem[headers[j].strip_edges()] = values[j].strip_edges()
|
||||||
|
|
||||||
rows.append(row)
|
result[elem[headers[0]]] = elem
|
||||||
print(rows)
|
return result
|
||||||
return rows
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class_name OwniverseTemplates
|
class_name OwniverseTemplate
|
||||||
|
|
||||||
#region RU
|
#region RU
|
||||||
const RU := """
|
const RU := """
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="keep"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="skip"
|
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
# Проверки поэтапного переноса из Owniverse.U
|
# Проверки поэтапного переноса из Owniverse.U
|
||||||
|
|
||||||
|
## Производительность на сохранении
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.gd -- /tmp/save.adventure.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Читает диагностический JSON формата 3 в отдельные экземпляры мира и замеряет
|
||||||
|
шесть последовательных шагов на скоростях 1, 4, 6. Запись сохранений не вызывается.
|
||||||
|
Последним аргументом можно задать длительность шага в реальных секундах
|
||||||
|
(по умолчанию 1/60). Это время расчёта симуляции, без отрисовки интерфейса.
|
||||||
|
|
||||||
|
При проверке регрессии на копии сохранения с 120 партиями шаг на скорости 6
|
||||||
|
занимал около 1,3 секунды. Главная причина — перебор пустых завершённых окон
|
||||||
|
коротких взрывов; дополнительная — повторное сокращение уже сокращённых дробей.
|
||||||
|
После пропуска таких окон и устранения повторных НОД замеры составили 90–99 мс
|
||||||
|
на шаг 1/60 секунды. Это ускорение примерно в 14 раз, но ещё не бюджет 60 FPS.
|
||||||
|
Размер партии, точность чисел и правила эволюции не изменены. Разреженные
|
||||||
|
сценарии дополнительно сравниваются с полным расчётом в `test_short_lived_events.gd`.
|
||||||
|
|
||||||
## Точная арифметика
|
## Точная арифметика
|
||||||
|
|
||||||
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Read-only benchmark. Loads a diagnostic JSON, never invokes SaveManager.
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
func _initialize():
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func restore_json_integers(value):
|
||||||
|
# JSON parses ordinary integer tokens as float. Large integers are $exact.
|
||||||
|
if value is float and value == floorf(value) and absf(value) <= 9007199254740991:
|
||||||
|
return int(value)
|
||||||
|
if value is Dictionary:
|
||||||
|
for key in value: value[key] = restore_json_integers(value[key])
|
||||||
|
elif value is Array:
|
||||||
|
for i in value.size(): value[i] = restore_json_integers(value[i])
|
||||||
|
return value
|
||||||
|
|
||||||
|
func run():
|
||||||
|
var args = OS.get_cmdline_user_args()
|
||||||
|
if args.is_empty():
|
||||||
|
print("Usage: --script tests/benchmark_saved_state.gd -- /path/to/save.adventure.json [delta_seconds]")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var data = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
|
||||||
|
if not data is Dictionary or data.get("format_version") != 3:
|
||||||
|
push_error("Expected diagnostic JSON format 3")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var initial: Dictionary = E.decode(restore_json_integers(data)).state
|
||||||
|
var delta: float = args[1].to_float() if args.size() > 1 else 1.0 / 60.0
|
||||||
|
if not is_finite(delta) or delta <= 0:
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
for speed in [1, 4, 6]:
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
world.state = initial.duplicate(true)
|
||||||
|
world.speed = speed
|
||||||
|
var elapsed: Array = []
|
||||||
|
for cycle in 6:
|
||||||
|
var start: int = Time.get_ticks_usec()
|
||||||
|
world.evolute(delta)
|
||||||
|
elapsed.append((Time.get_ticks_usec() - start) / 1000.0)
|
||||||
|
print("Speed %d, six consecutive %s-second steps, ms: %s" % [speed, delta, elapsed])
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dji1apht4pn5y
|
||||||
@@ -75,5 +75,20 @@ func run() -> void:
|
|||||||
for batch in fast.state.batches.SN: total = E.add(total, batch.amount)
|
for batch in fast.state.batches.SN: total = E.add(total, batch.amount)
|
||||||
check(E.eq(total, 10000), "Direct completions plus survivors conserve event count")
|
check(E.eq(total, 10000), "Direct completions plus survivors conserve event count")
|
||||||
dispose(fast)
|
dispose(fast)
|
||||||
|
# Sparse explosions: thousands of time windows but very few actual events.
|
||||||
|
# Exact interval edges and partial final windows exercise the jump boundary.
|
||||||
|
for count in [1, 3, 17]:
|
||||||
|
for year in [500, 1001]:
|
||||||
|
fast = fixture(false)
|
||||||
|
var legacy = fixture(true)
|
||||||
|
fast.state.amount.RHG = count
|
||||||
|
fast.state.batches.RHG = [{"amount": count, "start_year": E.of("0.25"), "end_year": E.of("1000.25")}]
|
||||||
|
legacy.state = fast.state.duplicate(true)
|
||||||
|
fast.evolution.evolve_batches(fast.state, year)
|
||||||
|
legacy.evolution.evolve_batches(legacy.state, year)
|
||||||
|
for field in fast.state:
|
||||||
|
check(equivalent(fast.state[field], legacy.state[field]), "Sparse %s at %s: %s" % [count, year, field])
|
||||||
|
dispose(fast)
|
||||||
|
dispose(legacy)
|
||||||
print(("Short-lived events: %d checks, %d failures" % [checks, failures]))
|
print(("Short-lived events: %d checks, %d failures" % [checks, failures]))
|
||||||
quit((0 if E.eq(failures, 0) else 1))
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ static func multiply(a: Array, b: Array) -> Array:
|
|||||||
|
|
||||||
static func divmod(a: Array, b: Array) -> Array:
|
static func divmod(a: Array, b: Array) -> Array:
|
||||||
assert(b != [0], "Division by zero")
|
assert(b != [0], "Division by zero")
|
||||||
|
if b == [1]: return [a.duplicate(), [0]]
|
||||||
if compare(a, b) < 0: return [[0], a.duplicate()]
|
if compare(a, b) < 0: return [[0], a.duplicate()]
|
||||||
if b.size() == 1:
|
if b.size() == 1:
|
||||||
var quotient: Array = []
|
var quotient: Array = []
|
||||||
@@ -96,6 +97,7 @@ static func divmod(a: Array, b: Array) -> Array:
|
|||||||
|
|
||||||
static func gcd(a: Array, b: Array) -> Array:
|
static func gcd(a: Array, b: Array) -> Array:
|
||||||
while b != [0]:
|
while b != [0]:
|
||||||
|
if b == [1]: return [1]
|
||||||
var remainder: Array = divmod(a, b)[1]
|
var remainder: Array = divmod(a, b)[1]
|
||||||
a = b
|
a = b
|
||||||
b = remainder
|
b = remainder
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ static func make(n: Array, d: Array = [1], polarity: int = 1):
|
|||||||
var common: Array = Big.gcd(n, d)
|
var common: Array = Big.gcd(n, d)
|
||||||
n = Big.divmod(n, common)[0]
|
n = Big.divmod(n, common)[0]
|
||||||
d = Big.divmod(d, common)[0]
|
d = Big.divmod(d, common)[0]
|
||||||
|
return _make_reduced(n, d, polarity)
|
||||||
|
|
||||||
|
## Internal: operands already reduced; do not repeat Euclid's algorithm.
|
||||||
|
static func _make_reduced(n: Array, d: Array, polarity: int):
|
||||||
|
if n == [0]: return 0
|
||||||
if d == [1] and (n.size() == 1 or (n.size() == 2 and n[1] < SMALL)):
|
if d == [1] and (n.size() == 1 or (n.size() == 2 and n[1] < SMALL)):
|
||||||
return polarity * (n[0] + (n[1] * SMALL if n.size() == 2 else 0))
|
return polarity * (n[0] + (n[1] * SMALL if n.size() == 2 else 0))
|
||||||
if d == [1] and n.size() == 3:
|
if d == [1] and n.size() == 3:
|
||||||
@@ -139,9 +144,18 @@ static func add(a, b):
|
|||||||
var left: Array = Big.multiply(x[0], yd)
|
var left: Array = Big.multiply(x[0], yd)
|
||||||
var right: Array = Big.multiply(y[0], xd)
|
var right: Array = Big.multiply(y[0], xd)
|
||||||
var denominator: Array = Big.multiply(xd, y[1])
|
var denominator: Array = Big.multiply(xd, y[1])
|
||||||
if x[2] == y[2]: return make(Big.add(left, right), denominator, x[2])
|
var numerator: Array
|
||||||
var order: int = Big.compare(left, right)
|
var polarity: int = x[2]
|
||||||
return make(Big.subtract(left, right) if order >= 0 else Big.subtract(right, left), denominator, x[2] if order >= 0 else y[2])
|
if x[2] == y[2]:
|
||||||
|
numerator = Big.add(left, right)
|
||||||
|
else:
|
||||||
|
var order: int = Big.compare(left, right)
|
||||||
|
numerator = Big.subtract(left, right) if order >= 0 else Big.subtract(right, left)
|
||||||
|
polarity = x[2] if order >= 0 else y[2]
|
||||||
|
# Reduced inputs can only share a remaining factor from gcd(d1, d2).
|
||||||
|
# Testing the entire new denominator repeats expensive long division.
|
||||||
|
var reduction: Array = Big.gcd(numerator, common)
|
||||||
|
return _make_reduced(Big.divmod(numerator, reduction)[0], Big.divmod(denominator, reduction)[0], polarity)
|
||||||
|
|
||||||
static func subtract(a, b):
|
static func subtract(a, b):
|
||||||
if not numeric(a) or not numeric(b): return a - b
|
if not numeric(a) or not numeric(b): return a - b
|
||||||
@@ -150,7 +164,7 @@ static func subtract(a, b):
|
|||||||
if not finite(a) or not finite(b): return approx(a) - approx(b)
|
if not finite(a) or not finite(b): return approx(a) - approx(b)
|
||||||
if b is int and b == 0: return of(a)
|
if b is int and b == 0: return of(a)
|
||||||
var y: Array = parts(b)
|
var y: Array = parts(b)
|
||||||
return add(a, make(y[0], y[1], -y[2]))
|
return add(a, _make_reduced(y[0], y[1], -y[2]))
|
||||||
|
|
||||||
static func multiply(a, b):
|
static func multiply(a, b):
|
||||||
if not numeric(a) or not numeric(b): return a * b
|
if not numeric(a) or not numeric(b): return a * b
|
||||||
@@ -164,7 +178,7 @@ static func multiply(a, b):
|
|||||||
var y: Array = parts(b)
|
var y: Array = parts(b)
|
||||||
var g1: Array = Big.gcd(x[0], y[1])
|
var g1: Array = Big.gcd(x[0], y[1])
|
||||||
var g2: Array = Big.gcd(y[0], x[1])
|
var g2: Array = Big.gcd(y[0], x[1])
|
||||||
return make(Big.multiply(Big.divmod(x[0], g1)[0], Big.divmod(y[0], g2)[0]), Big.multiply(Big.divmod(x[1], g2)[0], Big.divmod(y[1], g1)[0]), x[2] * y[2])
|
return _make_reduced(Big.multiply(Big.divmod(x[0], g1)[0], Big.divmod(y[0], g2)[0]), Big.multiply(Big.divmod(x[1], g2)[0], Big.divmod(y[1], g1)[0]), x[2] * y[2])
|
||||||
|
|
||||||
static func divide(a, b):
|
static func divide(a, b):
|
||||||
if not numeric(a) or not numeric(b): return a / b
|
if not numeric(a) or not numeric(b): return a / b
|
||||||
@@ -174,7 +188,7 @@ static func divide(a, b):
|
|||||||
if a % b == 0: return a / b
|
if a % b == 0: return a / b
|
||||||
var y: Array = parts(b)
|
var y: Array = parts(b)
|
||||||
assert(y[0] != [0], "Exact division by zero")
|
assert(y[0] != [0], "Exact division by zero")
|
||||||
return multiply(a, make(y[1], y[0], y[2]))
|
return multiply(a, _make_reduced(y[1], y[0], y[2]))
|
||||||
|
|
||||||
static func floor_value(a):
|
static func floor_value(a):
|
||||||
if not finite(a): return a
|
if not finite(a): return a
|
||||||
|
|||||||
@@ -55,12 +55,26 @@ func append_batch(target: Dictionary, id: String, amount: Variant, created_from:
|
|||||||
if E.le(E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration)), cursor):
|
if E.le(E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration)), cursor):
|
||||||
window_index = E.add(window_index, 1)
|
window_index = E.add(window_index, 1)
|
||||||
var batches_by_start: Dictionary = {}
|
var batches_by_start: Dictionary = {}
|
||||||
|
var skip_empty_expired: bool = E.gt(duration, 0) and E.finite(completed_before) and E.eq(amount, E.floor_value(amount))
|
||||||
for existing : Dictionary in batches:
|
for existing : Dictionary in batches:
|
||||||
var start: Variant = existing.get("created_from", E.neg(INF))
|
var start: Variant = existing.get("created_from", E.neg(INF))
|
||||||
if (not batches_by_start.has(str(start))):
|
if (not batches_by_start.has(str(start))):
|
||||||
batches_by_start[str(start)] = []
|
batches_by_start[str(start)] = []
|
||||||
batches_by_start[str(start)].append(existing)
|
batches_by_start[str(start)].append(existing)
|
||||||
while true:
|
while true:
|
||||||
|
# Whole allocation changes only when its cumulative quota reaches the
|
||||||
|
# next integer. Skip expired empty windows without changing the span
|
||||||
|
# passed to the next generation or the lifetime production integral.
|
||||||
|
if skip_empty_expired:
|
||||||
|
var threshold = E.add(created_from, E.divide(E.multiply(duration, E.add(E.subtract(amount, remaining_amount), 1)), amount))
|
||||||
|
var next_nonempty = E.subtract(E.ceil_value(E.divide(E.subtract(threshold, origin), batch_duration)), 1)
|
||||||
|
var expired_end = E.floor_value(E.divide(E.subtract(E.subtract(completed_before, lifespan), origin), batch_duration))
|
||||||
|
var skip_to = E.minimum(next_nonempty, expired_end)
|
||||||
|
if E.gt(skip_to, window_index):
|
||||||
|
completed_from = E.minimum(completed_from, E.add(E.add(origin, E.multiply(window_index, batch_duration)), lifespan))
|
||||||
|
completed_to = E.maximum(completed_to, E.add(E.add(origin, E.multiply(skip_to, batch_duration)), lifespan))
|
||||||
|
window_index = skip_to
|
||||||
|
cursor = E.add(origin, E.multiply(window_index, batch_duration))
|
||||||
var window_start: Variant = E.add(origin, E.multiply(window_index, batch_duration))
|
var window_start: Variant = E.add(origin, E.multiply(window_index, batch_duration))
|
||||||
var window_end: Variant = E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration))
|
var window_end: Variant = E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration))
|
||||||
if E.le(window_end, cursor):
|
if E.le(window_end, cursor):
|
||||||
|
|||||||
@@ -1110,6 +1110,10 @@ autostart = true
|
|||||||
wait_time = 2.0
|
wait_time = 2.0
|
||||||
autostart = true
|
autostart = true
|
||||||
|
|
||||||
|
[node name="FXPlayer" type="AudioStreamPlayer" parent="." unique_id=1306204231]
|
||||||
|
|
||||||
|
[node name="MusicPlayer" type="AudioStreamPlayer" parent="." unique_id=707734847]
|
||||||
|
|
||||||
[connection signal="timeout" from="Background/Timer" to="Background" method="randomize_parallax"]
|
[connection signal="timeout" from="Background/Timer" to="Background" method="randomize_parallax"]
|
||||||
[connection signal="pressed" from="Stage/World/TopMenu/BurgerButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["MainMenu"]]
|
[connection signal="pressed" from="Stage/World/TopMenu/BurgerButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["MainMenu"]]
|
||||||
[connection signal="pressed" from="Stage/World/TopMenu/TimeMinus" to="Stage" method="_change_speed" binds= [-1]]
|
[connection signal="pressed" from="Stage/World/TopMenu/TimeMinus" to="Stage" method="_change_speed" binds= [-1]]
|
||||||
|
|||||||
Reference in New Issue
Block a user