Разделить частоту обновления часов и расчёта симуляции
Счётчик времени UI обновляется до 10 Гц по запрошенному времени, а тяжёлые циклы по-прежнему запускаются с адаптивным интервалом и обрабатывают весь накопленный промежуток. Обновить тесты и описание модели.
This commit is contained in:
@@ -151,6 +151,8 @@ HP деградирует в RP. У эонов возможен откат к п
|
||||
|
||||
Порядок полного шага: производство за интервал → непрерывные потоки существующих цивилизаций → поколения звёзд и биологической жизни и бласты → итоговые ограничения среды → тренды. Возникшие из C0.1 цивилизации C0.2 получают равномерный профиль, смешанный с существующим населением, и начинают непрерывную эволюцию со следующего шага. Разные размеры полного шага могут отличаться из-за этого порядка, ограничений среды и нелинейных эффектов; чистая непрерывная цепочка без ограничений согласована при разделении интервала.
|
||||
|
||||
Счётчик времени UI показывает запрошенные часы и обновляется с частотой до 10 Гц независимо от тяжёлой транзакции. Состояние мира и его индикаторы публикуются по завершении транзакции; при высокой нагрузке отображаемые часы поэтому могут немного опережать рассчитанные объекты. Тяжёлые циклы адаптивно запускаются с целевым интервалом 0,2–0,5 реальной секунды, каждый использует весь накопленный игровой интервал.
|
||||
|
||||
В `civilization_remainders` сохраняются квоты и дробные остатки потоков; в `civilization_stats` — войны, колонии, побочный ущерб и поклонение. Возрастные партии сохраняются для звёзд, HP, эонов, C0.0 и C0.1.
|
||||
|
||||
### Структуры и периоды
|
||||
|
||||
+9
-4
@@ -98,6 +98,7 @@ var g_active_items: Dictionary = {}
|
||||
var pending_page: int = -1
|
||||
var active_actions_dirty: bool = true
|
||||
var active_actions_elapsed: float = 0.0
|
||||
var clock_display_elapsed: float = 0.0
|
||||
var interface_sounds_enabled: bool = false
|
||||
|
||||
var owniverse:Owniverse = Owniverse.new()
|
||||
@@ -389,7 +390,7 @@ func create_new_universe() -> void:
|
||||
func _refresh_universe_ui() -> void:
|
||||
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||
_change_speed(0)
|
||||
set_datetime(owniverse.state.time.year)
|
||||
set_datetime(owniverse.display_clock().year)
|
||||
set_owniverse_item_value()
|
||||
_unlock_items()
|
||||
if world_indicators != null: world_indicators.update(owniverse)
|
||||
@@ -397,6 +398,10 @@ func _refresh_universe_ui() -> void:
|
||||
func _process(delta: float) -> void:
|
||||
|
||||
owniverse.evolute_frame(delta, get_tree())
|
||||
clock_display_elapsed += delta
|
||||
if clock_display_elapsed >= 0.1:
|
||||
clock_display_elapsed = fmod(clock_display_elapsed, 0.1)
|
||||
set_datetime(owniverse.display_clock().year)
|
||||
indicator_elapsed += delta
|
||||
if indicator_elapsed >= 0.1 and indicator_revision != owniverse.presentation.revision:
|
||||
indicator_elapsed = 0.0
|
||||
@@ -404,9 +409,9 @@ func _process(delta: float) -> void:
|
||||
indicator_revision = owniverse.presentation.revision
|
||||
_update_descriptions(delta)
|
||||
if not owniverse.presentation.pending_amounts.is_empty(): set_owniverse_item_value()
|
||||
if displayed_year == null or E.ne(displayed_year, owniverse.state.time.year):
|
||||
set_datetime(owniverse.state.time.year)
|
||||
displayed_year = owniverse.state.time.year
|
||||
var committed_year: Variant = owniverse.state.time.year
|
||||
if displayed_year == null or E.ne(displayed_year, committed_year):
|
||||
displayed_year = committed_year
|
||||
|
||||
#owniverse.cycle(delta)
|
||||
|
||||
|
||||
+12
-1
@@ -253,7 +253,18 @@ godot --headless --path Owniverse.GI --script res://tests/test_flow_commit.gd
|
||||
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
|
||||
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
|
||||
на главном потоке, состояние публикуется только после полного завершения.
|
||||
Расчёт запускается после накопления 0,2 реальной секунды и обрабатывает весь ожидающий интервал. Время, поступившее во время расчёта, обрабатывается следующим циклом. При перегрузке шаг растёт; пять завершений в секунду остаются целевой частотой, а не гарантией.
|
||||
Запрошенные игровые часы и счётчик UI обновляются независимо от тяжёлого расчёта;
|
||||
Stage показывает запрошенное время раз в 0,1 секунды (10 Гц), а опубликованное
|
||||
состояние мира остаётся на последнем завершённом цикле. Расчёт запускается после
|
||||
накопления выбранного интервала и обрабатывает весь ожидающий интервал. Время,
|
||||
поступившее во время расчёта, обрабатывается следующим циклом. Контроллер нагрузки
|
||||
меняет целевой шаг между 0,2, 0,25, 0,333 и 0,5 секунды (5/4/3/2 цикла в секунду):
|
||||
вниз при EMA расчёта выше 90% текущего шага, вверх после 30 циклов ниже 65%.
|
||||
Временное разрешение укрупнения партий масштабируется с шагом, исходная точность
|
||||
сохраняется на 5 Гц. Контроллер не пропускает накопленное игровое время, но 2/с —
|
||||
нижняя целевая частота, не гарантия при перегрузке.
|
||||
|
||||
`benchmark_dynamic_cadence.gd` сравнивает пропускную способность полного цикла на двух диагностических сохранениях. `benchmark_dynamic_cadence_ui.gd` сравнивает 10 секунд работы полной сцены на сохранении со всеми объектами; результаты сохранены в `artifacts/dynamic_cadence_results.json` и `artifacts/dynamic_cadence_ui_results.json`.
|
||||
|
||||
Производство во время транзакции выполняется после её завершения. Запрошенное
|
||||
время, активный интервал, скорость и очередь производства входят в сохранение;
|
||||
|
||||
@@ -496,3 +496,23 @@ godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -
|
||||
Проверки: адаптивное качество — 18, хронология поколений — 28, границы партий — 117, оптимизации симуляции — 1889, планировщик кадров — 39; ошибок нет. Все 12 конечных диагностических состояний прошли валидацию. Хеши входных файлов: `44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67` и `1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a`.
|
||||
|
||||
Детали уровней, минимумы/максимумы и воспроизводимые результаты: `adaptive_quality_results.json`. Штатный benchmark принимает параметр `speed=1..6`.
|
||||
|
||||
## Адаптивная частота циклов 5 → 4 → 3 — 2026-10-17
|
||||
|
||||
Частота запуска полного цикла теперь снижается при устойчивой нагрузке: пороги шага — 0,2, 0,25 и 0,333 секунды. EMA времени расчёта переключает частоту вниз, когда цикл занимает больше 90% текущего интервала; возврат на одну ступень происходит после 30 циклов быстрее 65% интервала. Уровень частоты временный и после загрузки начинается с 5 Гц. Игровые часы продолжают накапливать весь прошедший интервал. Временное объединение партий масштабируется с выбранным интервалом, при этом при 5 Гц прежняя точность остаётся прежней.
|
||||
|
||||
Проверка сцены на диагностическом сохранении `all_objects_speed6.adventure.json`, скорость 6, Godot 4.7.2, Mac16,8. Сравнивались фиксированные 5 Гц и адаптивные 5/4/3 Гц, два прогона каждого режима в чередующемся порядке, по 10 секунд реального времени. Последняя транзакция каждого прогона завершалась до снятия результата.
|
||||
|
||||
| Метрика | Фиксированные 5 Гц | Адаптивно, установилось 3 Гц | Изменение |
|
||||
|---|---:|---:|---:|
|
||||
| Завершённые циклы/с | 4,64 | 2,96 | −36% |
|
||||
| Средний FPS сцены | 22,60 | 64,35 | ×2,85 |
|
||||
| p95 времени кадра | 115,6 мс | 66,5 мс | −42% |
|
||||
| Средний максимум кадра | 227 мс | 205 мс | −10% |
|
||||
| Опубликованные игровые годы/с | 99 017 | 98 049 | −1,0% |
|
||||
|
||||
На этих замерах реальное игровое время почти не замедлилось: симуляция завершала около трёх более крупных циклов вместо примерно 4,6 меньших. Средний FPS заметно вырос, но отдельные кадры всё ещё превышали 200 мс; это не гарантия отсутствия коротких задержек. UI-замер выполнен в headless-сцене на компьютере, поэтому его FPS не переносится напрямую на телефон.
|
||||
|
||||
Отдельно прогнан синхронный benchmark полного CPU-цикла на тех же двух тяжёлых сохранениях (скорость 6, 30 циклов × 2 повтора). При фиксированном шаге 0,2 секунды и адаптивном шаге итоговая обработка игрового времени за CPU-секунду выросла с 1,099 до 1,656 на сохранении со всеми объектами (+50,7%) и с 1,036 до 1,697 на сохранении со множеством объектов и партий (+63,9%). В каждом адаптивном прогоне частоты установились на 3 Гц; интервалы игровых часов сверены с запрошенными, все конечные состояния прошли проверку сохранения. Синхронный показатель показывает пропускную способность расчёта, а UI-прогон — фактический темп транзакций сцены.
|
||||
|
||||
Повторяемые результаты: `dynamic_cadence_results.json` (CPU-прогоны) и `dynamic_cadence_ui_results.json` (сцена и UI). Запуски выполняются скриптами `benchmark_dynamic_cadence.gd` и `benchmark_dynamic_cadence_ui.gd`; исходные диагностические сохранения не изменялись.
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"cycles_per_run": 30,
|
||||
"godot": {
|
||||
"build": "official",
|
||||
"hash": "ed1daf0bf001b61586d9930840f2f1394092c079",
|
||||
"hex": 263938,
|
||||
"major": 4,
|
||||
"minor": 7,
|
||||
"patch": 2,
|
||||
"status": "stable",
|
||||
"string": "4.7.2-stable (official)",
|
||||
"timestamp": 0
|
||||
},
|
||||
"input_saves_unchanged": true,
|
||||
"machine": "Mac16,8",
|
||||
"repeats": 2,
|
||||
"results": [
|
||||
{
|
||||
"applied_game_years": "600000",
|
||||
"cadence_steps": {
|
||||
"0.2": 30
|
||||
},
|
||||
"cpu_cycles_per_second": 5.5818824005294605,
|
||||
"cycles": 30,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 5.0,
|
||||
"max_ms": 351.156,
|
||||
"mean_ms_per_cycle": 179.15103333333334,
|
||||
"mode": "fixed_5",
|
||||
"p95_ms": 188.985,
|
||||
"repeat": 0,
|
||||
"requested_simulation_seconds": 6.000000000000003,
|
||||
"save": "all_objects_speed6.adventure.json",
|
||||
"simulated_seconds_per_cpu_second": 1.1163764801058926
|
||||
},
|
||||
{
|
||||
"applied_game_years": "978333",
|
||||
"cadence_steps": {
|
||||
"0.2": 1,
|
||||
"0.25": 1,
|
||||
"0.33333333333333": 28
|
||||
},
|
||||
"cpu_cycles_per_second": 5.126867760649103,
|
||||
"cycles": 30,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 3.0,
|
||||
"max_ms": 349.806,
|
||||
"mean_ms_per_cycle": 195.05086666666668,
|
||||
"mode": "adaptive_5_4_3",
|
||||
"p95_ms": 214.441,
|
||||
"repeat": 0,
|
||||
"requested_simulation_seconds": 9.783333333333333,
|
||||
"save": "all_objects_speed6.adventure.json",
|
||||
"simulated_seconds_per_cpu_second": 1.671928541945013
|
||||
},
|
||||
{
|
||||
"applied_game_years": "978333",
|
||||
"cadence_steps": {
|
||||
"0.2": 1,
|
||||
"0.25": 1,
|
||||
"0.33333333333333": 28
|
||||
},
|
||||
"cpu_cycles_per_second": 5.029407785556781,
|
||||
"cycles": 30,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 3.0,
|
||||
"max_ms": 340.747,
|
||||
"mean_ms_per_cycle": 198.83056666666667,
|
||||
"mode": "adaptive_5_4_3",
|
||||
"p95_ms": 233.775,
|
||||
"repeat": 1,
|
||||
"requested_simulation_seconds": 9.783333333333333,
|
||||
"save": "all_objects_speed6.adventure.json",
|
||||
"simulated_seconds_per_cpu_second": 1.6401457611787948
|
||||
},
|
||||
{
|
||||
"applied_game_years": "600000",
|
||||
"cadence_steps": {
|
||||
"0.2": 30
|
||||
},
|
||||
"cpu_cycles_per_second": 5.403485464263869,
|
||||
"cycles": 30,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 5.0,
|
||||
"max_ms": 351.698,
|
||||
"mean_ms_per_cycle": 185.06573333333333,
|
||||
"mode": "fixed_5",
|
||||
"p95_ms": 195.396,
|
||||
"repeat": 1,
|
||||
"requested_simulation_seconds": 6.000000000000003,
|
||||
"save": "all_objects_speed6.adventure.json",
|
||||
"simulated_seconds_per_cpu_second": 1.0806970928527742
|
||||
},
|
||||
{
|
||||
"applied_game_years": "600000",
|
||||
"cadence_steps": {
|
||||
"0.2": 30
|
||||
},
|
||||
"cpu_cycles_per_second": 5.218331511555821,
|
||||
"cycles": 30,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 5.0,
|
||||
"max_ms": 373.477,
|
||||
"mean_ms_per_cycle": 191.63213333333334,
|
||||
"mode": "fixed_5",
|
||||
"p95_ms": 250.5,
|
||||
"repeat": 0,
|
||||
"requested_simulation_seconds": 6.000000000000003,
|
||||
"save": "many_objects_fragmented_stars_cycle.adventure.json",
|
||||
"simulated_seconds_per_cpu_second": 1.0436663023111648
|
||||
},
|
||||
{
|
||||
"applied_game_years": "978333",
|
||||
"cadence_steps": {
|
||||
"0.2": 1,
|
||||
"0.25": 1,
|
||||
"0.33333333333333": 28
|
||||
},
|
||||
"cpu_cycles_per_second": 5.21296979935453,
|
||||
"cycles": 30,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 3.0,
|
||||
"max_ms": 373.168,
|
||||
"mean_ms_per_cycle": 191.82923333333332,
|
||||
"mode": "adaptive_5_4_3",
|
||||
"p95_ms": 258.047,
|
||||
"repeat": 0,
|
||||
"requested_simulation_seconds": 9.783333333333333,
|
||||
"save": "many_objects_fragmented_stars_cycle.adventure.json",
|
||||
"simulated_seconds_per_cpu_second": 1.7000073734561718
|
||||
},
|
||||
{
|
||||
"applied_game_years": "978333",
|
||||
"cadence_steps": {
|
||||
"0.2": 1,
|
||||
"0.25": 1,
|
||||
"0.33333333333333": 28
|
||||
},
|
||||
"cpu_cycles_per_second": 5.195109254879333,
|
||||
"cycles": 30,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 3.0,
|
||||
"max_ms": 372.878,
|
||||
"mean_ms_per_cycle": 192.48873333333333,
|
||||
"mode": "adaptive_5_4_3",
|
||||
"p95_ms": 254.848,
|
||||
"repeat": 1,
|
||||
"requested_simulation_seconds": 9.783333333333333,
|
||||
"save": "many_objects_fragmented_stars_cycle.adventure.json",
|
||||
"simulated_seconds_per_cpu_second": 1.694182851452316
|
||||
},
|
||||
{
|
||||
"applied_game_years": "600000",
|
||||
"cadence_steps": {
|
||||
"0.2": 30
|
||||
},
|
||||
"cpu_cycles_per_second": 5.137478056546851,
|
||||
"cycles": 30,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 5.0,
|
||||
"max_ms": 375.784,
|
||||
"mean_ms_per_cycle": 194.64803333333333,
|
||||
"mode": "fixed_5",
|
||||
"p95_ms": 255.446,
|
||||
"repeat": 1,
|
||||
"requested_simulation_seconds": 6.000000000000003,
|
||||
"save": "many_objects_fragmented_stars_cycle.adventure.json",
|
||||
"simulated_seconds_per_cpu_second": 1.0274956113093707
|
||||
}
|
||||
],
|
||||
"speed": 6
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
{
|
||||
"godot": {
|
||||
"build": "official",
|
||||
"hash": "ed1daf0bf001b61586d9930840f2f1394092c079",
|
||||
"hex": 263938,
|
||||
"major": 4,
|
||||
"minor": 7,
|
||||
"patch": 2,
|
||||
"status": "stable",
|
||||
"string": "4.7.2-stable (official)",
|
||||
"timestamp": 0
|
||||
},
|
||||
"machine": "Mac16,8",
|
||||
"results": [
|
||||
{
|
||||
"commits": 48,
|
||||
"completed_cycles_per_second": 4.743091908967023,
|
||||
"cpu_profile_usec_calls": {
|
||||
"assign_homes": [
|
||||
51095,
|
||||
170
|
||||
],
|
||||
"civilization_flow_capacity": [
|
||||
111005,
|
||||
48
|
||||
],
|
||||
"civilization_flow_colonies": [
|
||||
131724,
|
||||
48
|
||||
],
|
||||
"civilization_flow_finish": [
|
||||
325933,
|
||||
48
|
||||
],
|
||||
"civilization_flow_ledger": [
|
||||
928271,
|
||||
48
|
||||
],
|
||||
"civilization_flow_plan": [
|
||||
825706,
|
||||
48
|
||||
],
|
||||
"civilization_operator_build": [
|
||||
148485,
|
||||
48
|
||||
],
|
||||
"civilization_operator_solve": [
|
||||
104924,
|
||||
2064
|
||||
],
|
||||
"enforce_capacity": [
|
||||
626179,
|
||||
170
|
||||
],
|
||||
"generation_products": [
|
||||
235626,
|
||||
74
|
||||
],
|
||||
"generation_withdrawal": [
|
||||
455348,
|
||||
97
|
||||
],
|
||||
"generations": [
|
||||
3654469,
|
||||
48
|
||||
],
|
||||
"initialize": [
|
||||
215278,
|
||||
48
|
||||
],
|
||||
"newborn_batches": [
|
||||
237070,
|
||||
3088
|
||||
],
|
||||
"physical_amounts_build": [
|
||||
630353,
|
||||
437
|
||||
],
|
||||
"production": [
|
||||
1115470,
|
||||
48
|
||||
],
|
||||
"trends": [
|
||||
865132,
|
||||
48
|
||||
]
|
||||
},
|
||||
"elapsed_seconds": 10.119981,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 5.0,
|
||||
"frames": 246,
|
||||
"mean_render_fps": 24.584772192104204,
|
||||
"mode": "fixed_5",
|
||||
"published_to_requested": 0.9943600000000001,
|
||||
"published_years": "1006667",
|
||||
"render_max_ms": 237.132,
|
||||
"render_p95_ms": 111.449,
|
||||
"requested_years": "1012372"
|
||||
},
|
||||
{
|
||||
"commits": 21,
|
||||
"completed_cycles_per_second": 2.085573876935847,
|
||||
"cpu_profile_usec_calls": {
|
||||
"assign_homes": [
|
||||
30276,
|
||||
75
|
||||
],
|
||||
"civilization_flow_capacity": [
|
||||
51694,
|
||||
21
|
||||
],
|
||||
"civilization_flow_colonies": [
|
||||
52773,
|
||||
21
|
||||
],
|
||||
"civilization_flow_finish": [
|
||||
142952,
|
||||
21
|
||||
],
|
||||
"civilization_flow_ledger": [
|
||||
309711,
|
||||
21
|
||||
],
|
||||
"civilization_flow_plan": [
|
||||
435139,
|
||||
21
|
||||
],
|
||||
"civilization_operator_build": [
|
||||
74385,
|
||||
21
|
||||
],
|
||||
"civilization_operator_solve": [
|
||||
87284,
|
||||
903
|
||||
],
|
||||
"enforce_capacity": [
|
||||
414865,
|
||||
75
|
||||
],
|
||||
"generation_products": [
|
||||
105449,
|
||||
33
|
||||
],
|
||||
"generation_withdrawal": [
|
||||
152279,
|
||||
43
|
||||
],
|
||||
"generations": [
|
||||
1503309,
|
||||
21
|
||||
],
|
||||
"initialize": [
|
||||
255626,
|
||||
21
|
||||
],
|
||||
"newborn_batches": [
|
||||
114834,
|
||||
1633
|
||||
],
|
||||
"physical_amounts_build": [
|
||||
274341,
|
||||
193
|
||||
],
|
||||
"production": [
|
||||
685165,
|
||||
21
|
||||
],
|
||||
"trends": [
|
||||
377582,
|
||||
21
|
||||
]
|
||||
},
|
||||
"elapsed_seconds": 10.069171,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 2.0,
|
||||
"frames": 879,
|
||||
"mean_render_fps": 87.80117977216634,
|
||||
"mode": "adaptive_5_4_3_2",
|
||||
"published_to_requested": 0.9889800000000001,
|
||||
"published_years": "989844",
|
||||
"render_max_ms": 203.26,
|
||||
"render_p95_ms": 42.712,
|
||||
"requested_years": "1000874"
|
||||
},
|
||||
{
|
||||
"commits": 21,
|
||||
"completed_cycles_per_second": 2.0775335521668676,
|
||||
"cpu_profile_usec_calls": {
|
||||
"assign_homes": [
|
||||
31004,
|
||||
76
|
||||
],
|
||||
"civilization_flow_capacity": [
|
||||
51172,
|
||||
21
|
||||
],
|
||||
"civilization_flow_colonies": [
|
||||
52782,
|
||||
21
|
||||
],
|
||||
"civilization_flow_finish": [
|
||||
142851,
|
||||
21
|
||||
],
|
||||
"civilization_flow_ledger": [
|
||||
309607,
|
||||
21
|
||||
],
|
||||
"civilization_flow_plan": [
|
||||
431321,
|
||||
21
|
||||
],
|
||||
"civilization_operator_build": [
|
||||
74854,
|
||||
21
|
||||
],
|
||||
"civilization_operator_solve": [
|
||||
86348,
|
||||
903
|
||||
],
|
||||
"enforce_capacity": [
|
||||
423638,
|
||||
76
|
||||
],
|
||||
"generation_products": [
|
||||
105537,
|
||||
34
|
||||
],
|
||||
"generation_withdrawal": [
|
||||
151611,
|
||||
43
|
||||
],
|
||||
"generations": [
|
||||
1516916,
|
||||
21
|
||||
],
|
||||
"initialize": [
|
||||
261806,
|
||||
21
|
||||
],
|
||||
"newborn_batches": [
|
||||
112492,
|
||||
1621
|
||||
],
|
||||
"physical_amounts_build": [
|
||||
278871,
|
||||
195
|
||||
],
|
||||
"production": [
|
||||
696352,
|
||||
21
|
||||
],
|
||||
"trends": [
|
||||
377969,
|
||||
21
|
||||
]
|
||||
},
|
||||
"elapsed_seconds": 10.10814,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 2.0,
|
||||
"frames": 880,
|
||||
"mean_render_fps": 88.00051040296032,
|
||||
"mode": "adaptive_5_4_3_2",
|
||||
"published_to_requested": 0.9933700000000001,
|
||||
"published_years": "994002",
|
||||
"render_max_ms": 201.104,
|
||||
"render_p95_ms": 42.207,
|
||||
"requested_years": "1000640"
|
||||
},
|
||||
{
|
||||
"commits": 47,
|
||||
"completed_cycles_per_second": 4.646829581875308,
|
||||
"cpu_profile_usec_calls": {
|
||||
"assign_homes": [
|
||||
50139,
|
||||
165
|
||||
],
|
||||
"civilization_flow_capacity": [
|
||||
108674,
|
||||
47
|
||||
],
|
||||
"civilization_flow_colonies": [
|
||||
129977,
|
||||
47
|
||||
],
|
||||
"civilization_flow_finish": [
|
||||
323458,
|
||||
47
|
||||
],
|
||||
"civilization_flow_ledger": [
|
||||
911542,
|
||||
47
|
||||
],
|
||||
"civilization_flow_plan": [
|
||||
807459,
|
||||
47
|
||||
],
|
||||
"civilization_operator_build": [
|
||||
147856,
|
||||
47
|
||||
],
|
||||
"civilization_operator_solve": [
|
||||
96154,
|
||||
2021
|
||||
],
|
||||
"enforce_capacity": [
|
||||
619397,
|
||||
165
|
||||
],
|
||||
"generation_products": [
|
||||
234744,
|
||||
71
|
||||
],
|
||||
"generation_withdrawal": [
|
||||
451797,
|
||||
95
|
||||
],
|
||||
"generations": [
|
||||
3665554,
|
||||
47
|
||||
],
|
||||
"initialize": [
|
||||
217827,
|
||||
47
|
||||
],
|
||||
"newborn_batches": [
|
||||
265766,
|
||||
3178
|
||||
],
|
||||
"physical_amounts_build": [
|
||||
623211,
|
||||
428
|
||||
],
|
||||
"production": [
|
||||
1142731,
|
||||
47
|
||||
],
|
||||
"trends": [
|
||||
856364,
|
||||
47
|
||||
]
|
||||
},
|
||||
"elapsed_seconds": 10.114423,
|
||||
"final_adaptive_quality": 3,
|
||||
"final_cadence_hz": 5.0,
|
||||
"frames": 239,
|
||||
"mean_render_fps": 23.87480014844331,
|
||||
"mode": "fixed_5",
|
||||
"published_to_requested": 0.9904200000000001,
|
||||
"published_years": "992039",
|
||||
"render_max_ms": 201.438,
|
||||
"render_p95_ms": 114.211,
|
||||
"requested_years": "1001635"
|
||||
}
|
||||
],
|
||||
"save": "all_objects_speed6.adventure.json",
|
||||
"speed": 6,
|
||||
"wall_seconds_per_run": 10.0
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
extends SceneTree
|
||||
## Paired, read-only throughput benchmark for adaptive 5/4/3/2 Hz cadence.
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const SAVE_PATHS: Array[String] = [
|
||||
"res://tests/artifacts/all_objects_speed6.adventure.json",
|
||||
"res://tests/artifacts/many_objects_fragmented_stars_cycle.adventure.json"
|
||||
]
|
||||
const RESULT_PATH := "res://tests/artifacts/dynamic_cadence_results.json"
|
||||
var cycles: int = 40
|
||||
var repeats: int = 2
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func restore_json_integers(value):
|
||||
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 create_world(initial: Dictionary, use_adaptive_cadence: bool):
|
||||
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 = 6
|
||||
world.adaptive_cadence = use_adaptive_cadence
|
||||
world.frame_step_seconds = 0.2
|
||||
world.cadence_level = 0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
|
||||
func measure(initial: Dictionary, adaptive: bool, save_name: String, repeat_index: int) -> Dictionary:
|
||||
var world = create_world(initial, adaptive)
|
||||
var elapsed_usec: int = 0
|
||||
var requested_seconds: float = 0.0
|
||||
var step_counts: Dictionary = {}
|
||||
var samples: Array[float] = []
|
||||
var initial_clock: Variant = world.state.time.datetime
|
||||
for i in cycles:
|
||||
var step: float = world.frame_step_seconds
|
||||
step_counts[str(step)] = int(step_counts.get(str(step), 0)) + 1
|
||||
requested_seconds += step
|
||||
var started: int = Time.get_ticks_usec()
|
||||
world.evolute(step)
|
||||
var usec: int = Time.get_ticks_usec() - started
|
||||
elapsed_usec += usec
|
||||
samples.append(float(usec) / 1000.0)
|
||||
var applied_years: Variant = E.subtract(world.state.time.datetime, initial_clock)
|
||||
var expected_years: float = requested_seconds * 100000.0
|
||||
assert(absf(E.approx(applied_years) - expected_years) <= maxf(1.0, expected_years * 1e-10), "Clock advance differs from accumulated cadence interval")
|
||||
var validation: String = root.get_node("SaveManager").validate_data({
|
||||
"format_version": 6,
|
||||
"app_version": "0.7.0",
|
||||
"ui": {"current_page": 0},
|
||||
"state": world.state
|
||||
})
|
||||
assert(validation.is_empty(), validation)
|
||||
samples.sort()
|
||||
var result: Dictionary = {
|
||||
"save": save_name,
|
||||
"mode": "adaptive_5_4_3_2" if adaptive else "fixed_5",
|
||||
"repeat": repeat_index,
|
||||
"cycles": cycles,
|
||||
"mean_ms_per_cycle": float(elapsed_usec) / (cycles * 1000.0),
|
||||
"p95_ms": samples[int(ceil(samples.size() * 0.95)) - 1],
|
||||
"max_ms": samples.back(),
|
||||
"cpu_cycles_per_second": float(cycles) * 1000000.0 / elapsed_usec,
|
||||
"simulated_seconds_per_cpu_second": requested_seconds * 1000000.0 / elapsed_usec,
|
||||
"requested_simulation_seconds": requested_seconds,
|
||||
"applied_game_years": E.fixed(applied_years),
|
||||
"cadence_steps": step_counts,
|
||||
"final_cadence_hz": 1.0 / world.frame_step_seconds,
|
||||
"final_adaptive_quality": world.adaptive_quality
|
||||
}
|
||||
print("CADENCE_RESULT ", JSON.stringify(result))
|
||||
dispose(world)
|
||||
return result
|
||||
|
||||
func run() -> void:
|
||||
for arg in OS.get_cmdline_user_args():
|
||||
if arg.begins_with("cycles="): cycles = maxi(1, arg.trim_prefix("cycles=").to_int())
|
||||
if arg.begins_with("repeats="): repeats = maxi(1, arg.trim_prefix("repeats=").to_int())
|
||||
var inputs: Dictionary = {}
|
||||
for path in SAVE_PATHS:
|
||||
var parsed = JSON.parse_string(FileAccess.get_file_as_string(path))
|
||||
assert(parsed is Dictionary and parsed.get("format_version") == 6, "Invalid benchmark save: " + path)
|
||||
var payload: Dictionary = E.decode(restore_json_integers(parsed))
|
||||
var validation: String = root.get_node("SaveManager").validate_data(payload)
|
||||
assert(validation.is_empty(), validation)
|
||||
inputs[path.get_file()] = payload.state
|
||||
var results: Array[Dictionary] = []
|
||||
for save_name in inputs:
|
||||
var initial: Dictionary = inputs[save_name]
|
||||
# Alternate order to limit machine warm-up and thermal bias.
|
||||
for repeat_index in repeats:
|
||||
if repeat_index % 2 == 0:
|
||||
results.append(measure(initial, false, save_name, repeat_index))
|
||||
results.append(measure(initial, true, save_name, repeat_index))
|
||||
else:
|
||||
results.append(measure(initial, true, save_name, repeat_index))
|
||||
results.append(measure(initial, false, save_name, repeat_index))
|
||||
var output := FileAccess.open(RESULT_PATH, FileAccess.WRITE)
|
||||
assert(output != null)
|
||||
output.store_string(JSON.stringify({
|
||||
"machine": OS.get_model_name(),
|
||||
"godot": Engine.get_version_info(),
|
||||
"speed": 6,
|
||||
"cycles_per_run": cycles,
|
||||
"repeats": repeats,
|
||||
"input_saves_unchanged": true,
|
||||
"results": results
|
||||
}, "\t", true, true))
|
||||
output.close()
|
||||
print("Wrote paired cadence results: ", RESULT_PATH)
|
||||
quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://3fogff2lfmy5
|
||||
@@ -0,0 +1,116 @@
|
||||
extends SceneTree
|
||||
## Measures real Stage scheduling on the full-load diagnostic save.
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const RUN_SECONDS: float = 10.0
|
||||
const RESULT_PATH := "res://tests/artifacts/dynamic_cadence_ui_results.json"
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func restore(value):
|
||||
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(value[key])
|
||||
elif value is Array:
|
||||
for i in value.size(): value[i] = restore(value[i])
|
||||
return value
|
||||
|
||||
func prepare(stage, state: Dictionary, adaptive: bool) -> void:
|
||||
var world = stage.owniverse
|
||||
world.state = state.duplicate(true)
|
||||
world.speed = 6
|
||||
world.adaptive_cadence = adaptive
|
||||
world.adaptive_cycle_ema_ms = 0.0
|
||||
world.adaptive_quality = 0
|
||||
world.adaptive_recovery_cycles = 0
|
||||
world.cadence_level = 0
|
||||
world.cadence_recovery_cycles = 0
|
||||
world.frame_step_seconds = 0.2
|
||||
world.frame_elapsed_seconds = 0.0
|
||||
world.frame_clock = world.state.time.duplicate()
|
||||
world.frame_active_clock = {}
|
||||
world._resume_clock = {}
|
||||
world._queued_productions.clear()
|
||||
world.production_queue_busy = false
|
||||
world.frame_busy = false
|
||||
world.frame_commits = 0
|
||||
world.profile.clear()
|
||||
stage.frame_samples.clear()
|
||||
stage.process_samples.clear()
|
||||
stage.previous_frame = 0
|
||||
stage.measuring = false
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
stage.set_owniverse_item_value()
|
||||
|
||||
func run_measurement(stage, initial: Dictionary, adaptive: bool) -> Dictionary:
|
||||
prepare(stage, initial, adaptive)
|
||||
var world = stage.owniverse
|
||||
var start_usec: int = Time.get_ticks_usec()
|
||||
var requested_start: Variant = world.frame_clock.datetime
|
||||
var published_start: Variant = world.state.time.datetime
|
||||
stage.measuring = true
|
||||
while float(Time.get_ticks_usec() - start_usec) / 1000000.0 < RUN_SECONDS:
|
||||
await process_frame
|
||||
stage.measuring = false
|
||||
var measured_frames: int = stage.frame_samples.size()
|
||||
# Let the final atomic transaction publish before starting the paired run.
|
||||
while world.frame_busy or world.production_queue_busy:
|
||||
await process_frame
|
||||
var elapsed_usec: int = Time.get_ticks_usec() - start_usec
|
||||
var requested: Variant = E.subtract(world.frame_clock.datetime, requested_start)
|
||||
var published: Variant = E.subtract(world.state.time.datetime, published_start)
|
||||
var samples: Array = stage.frame_samples.duplicate()
|
||||
var total_frame_ms: float = 0.0
|
||||
for value in samples: total_frame_ms += value
|
||||
samples.sort()
|
||||
var result: Dictionary = {
|
||||
"mode": "adaptive_5_4_3_2" if adaptive else "fixed_5",
|
||||
"frames": measured_frames,
|
||||
"elapsed_seconds": float(elapsed_usec) / 1000000.0,
|
||||
"commits": world.frame_commits,
|
||||
"completed_cycles_per_second": float(world.frame_commits) * 1000000.0 / elapsed_usec,
|
||||
"requested_years": E.fixed(requested),
|
||||
"published_years": E.fixed(published),
|
||||
"published_to_requested": snappedf(E.approx(E.divide(published, requested)), 0.00001),
|
||||
"mean_render_fps": float(samples.size()) * 1000.0 / total_frame_ms if total_frame_ms > 0.0 else 0.0,
|
||||
"render_p95_ms": samples[int(ceil(samples.size() * 0.95)) - 1] if not samples.is_empty() else 0.0,
|
||||
"render_max_ms": samples.back() if not samples.is_empty() else 0.0,
|
||||
"final_cadence_hz": 1.0 / world.frame_step_seconds,
|
||||
"final_adaptive_quality": world.adaptive_quality,
|
||||
"cpu_profile_usec_calls": world.profile.duplicate(true)
|
||||
}
|
||||
print("UI_CADENCE_RESULT ", JSON.stringify(result))
|
||||
return result
|
||||
|
||||
func run() -> void:
|
||||
var raw = JSON.parse_string(FileAccess.get_file_as_string("res://tests/artifacts/all_objects_speed6.adventure.json"))
|
||||
assert(raw is Dictionary and raw.get("format_version") == 6)
|
||||
var payload: Dictionary = E.decode(restore(raw))
|
||||
var validation: String = root.get_node("SaveManager").validate_data(payload)
|
||||
assert(validation.is_empty(), validation)
|
||||
var scene = load("res://world/world.tscn").instantiate()
|
||||
var stage = scene.get_node("Stage")
|
||||
stage.owniverse.free()
|
||||
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
|
||||
root.add_child(scene)
|
||||
await process_frame
|
||||
stage.owniverse.profile_enabled = true
|
||||
var results: Array[Dictionary] = []
|
||||
for repeat_index in 2:
|
||||
if repeat_index % 2 == 0:
|
||||
results.append(await run_measurement(stage, payload.state, false))
|
||||
results.append(await run_measurement(stage, payload.state, true))
|
||||
else:
|
||||
results.append(await run_measurement(stage, payload.state, true))
|
||||
results.append(await run_measurement(stage, payload.state, false))
|
||||
var report: Dictionary = {"machine": OS.get_model_name(), "godot": Engine.get_version_info(), "speed": 6, "wall_seconds_per_run": RUN_SECONDS, "save": "all_objects_speed6.adventure.json", "results": results}
|
||||
var output := FileAccess.open(RESULT_PATH, FileAccess.WRITE)
|
||||
assert(output != null)
|
||||
output.store_string(JSON.stringify(report, "\t", true, true))
|
||||
output.close()
|
||||
print("UI_CADENCE_RESULTS ", JSON.stringify(report))
|
||||
print("Wrote paired UI cadence results: ", RESULT_PATH)
|
||||
for entity in stage.owniverse.catalog.entities.values(): entity.free()
|
||||
stage.owniverse.free()
|
||||
scene.free()
|
||||
quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://olp81bbycovp
|
||||
@@ -14,6 +14,7 @@ func check(condition: bool, message: String) -> void:
|
||||
|
||||
func run() -> void:
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
world.adaptive_cadence = false
|
||||
world.update_adaptive_quality(250000)
|
||||
check(world.adaptive_quality == 2, "A slow cycle selects the medium approximation")
|
||||
world.update_adaptive_quality(250000)
|
||||
@@ -26,6 +27,12 @@ func run() -> void:
|
||||
for i in 30: world.update_adaptive_quality(100000)
|
||||
check(world.adaptive_quality == 0, "The exact mode returns after sustained headroom")
|
||||
|
||||
var cadence_world = load("res://world/owniverse/owniverse.gd").new()
|
||||
for i in 3: cadence_world.update_adaptive_quality(350000)
|
||||
check(cadence_world.cadence_level == 3, "Sustained 350 ms cycles select the 2 Hz cadence")
|
||||
check(is_equal_approx(cadence_world.frame_step_seconds, 0.5), "The lowest cadence uses a 0.5 second frame step")
|
||||
cadence_world.free()
|
||||
|
||||
var rows: Array = []
|
||||
for node in 12:
|
||||
var edges: Dictionary = {}
|
||||
|
||||
@@ -38,6 +38,7 @@ func run():
|
||||
sliced.evolute_frame(0.2, self)
|
||||
check(sliced.frame_busy, "Heavy transaction yields before publishing")
|
||||
check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction")
|
||||
check(E.eq(sliced.display_clock().year, Clock.advance({"datetime": 0, "year": 0}, 0.2, sliced.speed).year), "Display clock advances independently of the heavy transaction")
|
||||
check(sliced.request_production("H", 1, true), "Production requested during a frame is queued")
|
||||
check(sliced.frame_checkpoint().productions.size() == 1, "Queued production remains part of the save checkpoint")
|
||||
check(E.eq(sliced.state.amount.H, initial_h), "Player action waits for transaction commit")
|
||||
@@ -117,15 +118,17 @@ func run():
|
||||
check(E.eq(behind.state.time.datetime, 100000) and E.eq(behind.state.time, behind.frame_clock), "Delayed start consumes all elapsed time in one cycle")
|
||||
behind.evolute_frame(0.0, self)
|
||||
check(behind.frame_commits == 1 and not behind.frame_busy, "Consumed interval cannot be processed twice")
|
||||
behind.evolute_frame(0.23, self)
|
||||
var next_interval: float = behind.frame_step_seconds + 0.03
|
||||
behind.evolute_frame(next_interval, self)
|
||||
var remained_busy: bool = behind.frame_busy
|
||||
if remained_busy: behind.evolute_frame(0.53, self)
|
||||
var pending_clock: Dictionary = behind.frame_clock.duplicate()
|
||||
if remained_busy:
|
||||
check(E.eq(behind.frame_active_clock.datetime, 123000), "Busy transaction reserves its original target")
|
||||
var expected_active: Dictionary = Clock.advance({"datetime": 100000, "year": 100000}, next_interval, behind.speed)
|
||||
check(E.eq(behind.frame_active_clock, expected_active), "Busy transaction reserves its original target")
|
||||
while behind.frame_busy: await process_frame
|
||||
if remained_busy:
|
||||
check(E.eq(behind.state.time.datetime, 123000), "Busy transaction keeps its original target")
|
||||
check(E.eq(behind.state.time, behind.frame_active_clock), "Busy transaction keeps its original target")
|
||||
behind.evolute_frame(0.0, self)
|
||||
while behind.frame_busy: await process_frame
|
||||
check(E.eq(behind.state.time, pending_clock), "Next cycle consumes all time accumulated during computation")
|
||||
|
||||
@@ -69,7 +69,9 @@ func coarsen_cycle_stars(target: Dictionary, id: String) -> void:
|
||||
func temporal_resolution(target: Dictionary) -> Variant:
|
||||
var speed: int = int(target.get("speed", 1))
|
||||
if speed < 4: return 0
|
||||
var width: Variant = E.divide(E.power(10, speed - 1), 10)
|
||||
# Keep the existing 0.1-second resolution at the default 5 Hz cadence,
|
||||
# and scale temporal grouping with the adaptive cycle interval.
|
||||
var width: Variant = E.multiply(E.power(10, speed - 1), world.frame_step_seconds / 2.0)
|
||||
return E.multiply(width, E.power(2, world.adaptive_quality))
|
||||
|
||||
func window_duration(target: Dictionary, lifespan: Variant) -> Variant:
|
||||
|
||||
@@ -23,6 +23,12 @@ var adaptive_quality: int = 0
|
||||
var adaptive_cycle_ema_ms: float = 0.0
|
||||
var adaptive_recovery_cycles: int = 0
|
||||
var adaptive_generation_phase: int = 0
|
||||
## Runtime-only cadence: 5, 4, 3, or 2 full simulation cycles per second.
|
||||
var adaptive_cadence: bool = true
|
||||
var cadence_level: int = 0
|
||||
var cadence_recovery_cycles: int = 0
|
||||
const CADENCE_STEPS: Array[float] = [0.2, 0.25, 1.0 / 3.0, 0.5]
|
||||
var frame_step_seconds: float = 0.2
|
||||
|
||||
func update_adaptive_quality(cycle_usec: int) -> void:
|
||||
var cycle_ms: float = float(cycle_usec) / 1000.0
|
||||
@@ -46,6 +52,20 @@ func update_adaptive_quality(cycle_usec: int) -> void:
|
||||
adaptive_recovery_cycles = 0
|
||||
else:
|
||||
adaptive_recovery_cycles = 0
|
||||
if adaptive_cadence:
|
||||
var step: float = CADENCE_STEPS[cadence_level]
|
||||
if adaptive_cycle_ema_ms > step * 900.0 and cadence_level < CADENCE_STEPS.size() - 1:
|
||||
cadence_level += 1
|
||||
frame_step_seconds = CADENCE_STEPS[cadence_level]
|
||||
cadence_recovery_cycles = 0
|
||||
elif adaptive_cycle_ema_ms < step * 650.0 and cadence_level > 0:
|
||||
cadence_recovery_cycles += 1
|
||||
if cadence_recovery_cycles >= 30:
|
||||
cadence_level -= 1
|
||||
frame_step_seconds = CADENCE_STEPS[cadence_level]
|
||||
cadence_recovery_cycles = 0
|
||||
else:
|
||||
cadence_recovery_cycles = 0
|
||||
if adaptive_quality >= 3:
|
||||
adaptive_generation_phase = 1 - adaptive_generation_phase
|
||||
|
||||
@@ -103,9 +123,12 @@ var _production_queue_cancelled: bool = false
|
||||
var frame_active_clock: Dictionary = {}
|
||||
var _resume_clock: Dictionary = {}
|
||||
var frame_elapsed_seconds: float = 0.0
|
||||
const frame_step_seconds: float = 0.2
|
||||
const production_slice_usec: int = 4000
|
||||
|
||||
## The HUD follows requested time; world state remains at the last committed cycle.
|
||||
func display_clock() -> Dictionary:
|
||||
return frame_clock if not frame_clock.is_empty() else state.time
|
||||
|
||||
## Keep the requested clock advancing while a transaction spans render frames.
|
||||
func evolute_frame(delta: float, tree: SceneTree) -> void:
|
||||
if not is_finite(delta) or delta < 0.0 or not is_finite(delta * pow(10.0, speed - 1)): return
|
||||
|
||||
Reference in New Issue
Block a user