mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 04:30:44 +01:00
## About The Pull Request ### Summary This PR changes internal structure of `/datum/gas_mixture`: `gases[gas_id][MOLES]` refactored into `moles[gas_id]`, `gases[gas_id][ARCHIVE]` into `moles_archive[gas_id]` and `gases[gas_id][GAS_META]` into `gas_meta` static variable. This allows us to use BYOND 516 vector functions for calculating total moles and heat capacity. Also it simplifies some parts of the code, allowing us to get rid of macros `ADD_GAS` and `ASSERT_GAS`. According to the profiler `/turf/open/process_cell` time is reduced by ~20%. ### Details `gas_mixture.gases` was a nested 2d-list with MOLES, ARCHIVE and GAS_META for each gas_id. For example, to get gas moles you had to do `gases[gas_id][MOLES]`. I've changed this structure to be as follows: `moles[gas_id]` - moles for the gas, `moles_archive[gas_id]` - archived version of moles, `gas_meta[KEY][gas_id]` - static var with meta information for the gas. Since I removed key GAS_META from the gases, `gas_meta` was moved to the static variable and the order of keys in the array was changed from `gas_meta[gas_id][META_KEY]' to 'gas_meta[META_KEY][gas_id]`. This was done to allow using it in vector calculations (for example heat capacity or fusion power). Static variable access is very fast and it is considered as accessing a global in the bytecode. Byond 516 introduced new vector functions: `values_sum`, `values_dot` and others. These functions are very fast, but operate only on associative lists. This allows us to change the way we calculate total_moles and heat_capacity - very hot and heavily used functions. `total_moles()` became just `values_sum(moles)`, and `heat_capacity` is just a dot product: `values_dot(moles, gas_meta[META_GAS_SPECIFIC_HEAT])`. As a side bonus, since `moles` is just an associative list, you don't really need old macros `ADD_GAS` and `ASSERT_GAS` - all they did was to make a copy of a list[3] with default value [0, 0, gas_meta] for specific gas. Now when you're adding gas you can just use `moles[gas_id] += amount` and when you query amount of gas you can just query the key (for example `moles[/datum/gas/oxygen]`) if the key does not exist, it returns null and works as 0 for all arithmetic and logic operations. For example, old code would be `if (!air.gases[/datum/gas/oxygen] || air.gases[/datum/gas/oxygen][MOLES] < 1)` and now it is `if (moles[/datum/gas/oxygen] < 1)`. This simplifies some parts of the code and also speeds things up. For the performance comparison I used Tracy profiler. I've done many different tests, and they all show slightly different numbers, but overall speedup for `process_cell` is about 20%. (-20% to average time per call from ). My testing setup was as follows: Load Icebox, drop 30/60/90 radius bomb in the middle of the bridge, set code to blue, wait 10 minutes until the round ends. Also I fixed random seed in the master controller and in the planetary gas randomization so generated maps are the same between tests. Althought it's not very realistic, it generates a lot of samples for the `process_cell` (around ~3.5M per 10 minutes). Another test I did was a plasmafire in an 8x8 space, on runtime station, it showed (-24% time on process_cell). Another test was a emagged holodeck burn test, it showed (-13% time) As for other functions of gas_mixture: `total_moles`: -50%(2x speedup), `heat_capacity`: -65%(3x speedup), `share`: -30%, `react`: -20%. Timings of all those functions is in microseconds range and they are very hot (call count is in the same order as process_cell) <details><summary>Some pictures from profiler</summary> <img width="569" height="642" alt="process_cell" src="https://github.com/user-attachments/assets/76fa0c27-719d-485d-9bfc-859fef788999" /> <img width="572" height="315" alt="image" src="https://github.com/user-attachments/assets/f68497e9-4db8-4a9c-b43f-ad04e6dc5cac" /> <img width="569" height="317" alt="image" src="https://github.com/user-attachments/assets/d7249e7b-f344-47a6-8b39-1bab0521182d" /> <img width="541" height="316" alt="image" src="https://github.com/user-attachments/assets/1bef71fd-533d-40fa-a85e-7a803ad322f7" /> <img width="519" height="409" alt="image" src="https://github.com/user-attachments/assets/51165289-174e-403d-a09c-787dd9af136a" /> <img width="523" height="318" alt="image" src="https://github.com/user-attachments/assets/fe4b1db0-17d8-47f9-8fd0-e2ecef2ee66a" /> </details> <details><summary>Setting up a profiler</summary> If you wanna to reproduce my results here is a list of steps 1. download: https://github.com/goonstation/byond-tracy-writer (this one has offsets for my version 1677) 2. build the dll, drop in the tgstation/ folder 3. download rtracy https://github.com/Dimach/rtracy 4. download Tracy profiler (0.13.1) https://github.com/wolfpld/tracy 5. uncomment `#define USE_BYOND_TRACY` in `_compile_options.dm` 6. build tgstation 7. open dream daemon, run the desired test, after round end dream daemon closes 8. navigate to tgstation/data/profiler, find the `123412341234.utracy` file 9. run `rtracty 123412341234.utracy` 10. open tracy-profiler.exe, press Connect, save the profiler data 11. repeat steps 5-10 with another branch, save another profiler data 12. open tracy-profiler, open first data, press compare, open second data </details> ## Why It's Good For The Game ## Changelog 🆑 refactor: Atmos refactor & speedup by utilizing BYOND 516 vector functions /🆑 --------- Co-authored-by: san7890 <the@san7890.com>
203 lines
12 KiB
Plaintext
203 lines
12 KiB
Plaintext
#define TEST_CHECK_BREATH_MESSAGE(lungs_organ, message) "[lungs_organ.type]/check_breath() [message]"
|
|
#define TEST_ALERT_THROW_MESSAGE(lungs_organ, alert_name) TEST_CHECK_BREATH_MESSAGE(lungs_organ, "failed to throw alert [alert_name] when expected.")
|
|
#define TEST_ALERT_INHIBIT_MESSAGE(lungs_organ, alert_name) TEST_CHECK_BREATH_MESSAGE(lungs_organ, "threw alert [alert_name] when it wasn't expected.")
|
|
#define GET_MOLES(gas_mixture, gas_type) (gas_mixture.moles[gas_type] || 0)
|
|
|
|
/// Tests the standard, plasmaman, and lavaland lungs organ to ensure breathing and suffocation behave as expected.
|
|
/// Performs a check on each main (can be life-sustaining) gas, and ensures gas alerts are only thrown when expected.
|
|
/datum/unit_test/lungs
|
|
abstract_type = /datum/unit_test/lungs
|
|
|
|
/datum/unit_test/lungs/lungs_sanity/Run()
|
|
// "Standard" form of breathing.
|
|
// 2500 Litres of O2/N2 gas mix, ideal for life.
|
|
var/datum/gas_mixture/test_mix = create_standard_mix()
|
|
var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent)
|
|
var/obj/item/organ/lungs/test_lungs = allocate(/obj/item/organ/lungs)
|
|
// Test one breath of O2/N2 mix.
|
|
lungs_test_check_breath("standard gas mixture", lab_rat, test_lungs, test_mix)
|
|
|
|
// Suffocation with an empty gas mix.
|
|
var/datum/gas_mixture/empty_test_mix = allocate(/datum/gas_mixture)
|
|
lab_rat = allocate(/mob/living/carbon/human/consistent)
|
|
test_lungs = allocate(/obj/item/organ/lungs)
|
|
// Test one breath of nothing. Suffocate due to the breath being empty.
|
|
lungs_test_check_breath("empty gas mixture", lab_rat, test_lungs, empty_test_mix, expect_failure = TRUE)
|
|
|
|
// Suffocation with null. This does indeed happen normally.
|
|
lab_rat = allocate(/mob/living/carbon/human/consistent)
|
|
test_lungs = allocate(/obj/item/organ/lungs)
|
|
// Test one breath of nothing. Suffocate due to the breath being null.
|
|
lungs_test_check_breath("null", lab_rat, test_lungs, null, expect_failure = TRUE)
|
|
|
|
// Suffocation with Nitrogen.
|
|
var/datum/gas_mixture/nitro_test_mix = create_nitrogen_mix()
|
|
lab_rat = allocate(/mob/living/carbon/human/consistent)
|
|
test_lungs = allocate(/obj/item/organ/lungs)
|
|
// Test one breath of Nitrogen. Suffocate due to the breath being 100% N2.
|
|
lungs_test_check_breath("pure Nitrogen", lab_rat, test_lungs, nitro_test_mix, expect_failure = TRUE)
|
|
|
|
/// Tests the Plasmaman lungs organ to ensure Plasma breathing and suffocation behave as expected.
|
|
/datum/unit_test/lungs/lungs_sanity_plasmaman
|
|
|
|
/datum/unit_test/lungs/lungs_sanity_plasmaman/Run()
|
|
// 2500 Litres of pure Plasma.
|
|
var/datum/gas_mixture/plasma_test_mix = create_plasma_mix()
|
|
var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent)
|
|
var/obj/item/organ/lungs/plasmaman/test_lungs = allocate(/obj/item/organ/lungs/plasmaman)
|
|
// Test one breath of Plasma on Plasmaman lungs.
|
|
lungs_test_check_breath("pure Plasma", lab_rat, test_lungs, plasma_test_mix)
|
|
|
|
// Tests suffocation with Nitrogen.
|
|
var/datum/gas_mixture/nitro_test_mix = create_nitrogen_mix()
|
|
lab_rat = allocate(/mob/living/carbon/human/consistent)
|
|
test_lungs = allocate(/obj/item/organ/lungs/plasmaman)
|
|
// Test one breath of Nitrogen on Plasmaman lungs.
|
|
lungs_test_check_breath("pure Nitrogen", lab_rat, test_lungs, nitro_test_mix, expect_failure = TRUE)
|
|
|
|
/// Tests the lavaland/Ashwalker lungs organ.
|
|
/// Ensures they can breathe from the lavaland air mixture properly, and suffocate on inadequate mixture.
|
|
/datum/unit_test/lungs/lungs_sanity_ashwalker
|
|
|
|
/datum/unit_test/lungs/lungs_sanity_ashwalker/Run()
|
|
// Gas mix resembling one cell of lavaland's atmosphere.
|
|
var/datum/gas_mixture/lavaland_test_mix = create_lavaland_mix()
|
|
var/obj/item/organ/lungs/lavaland/test_lungs = allocate(/obj/item/organ/lungs/lavaland)
|
|
var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent)
|
|
// Test one breath of Lavaland gas mix on Ashwalker lungs.
|
|
lungs_test_check_breath("Lavaland air mixture", lab_rat, test_lungs, lavaland_test_mix)
|
|
|
|
/// Comprehensive unit test for [/obj/item/organ/lungs/proc/check_breath()]
|
|
/// If "expect_failure" is set to TRUE, the test ensures the given Human suffocated.
|
|
/// A "test_name" string is required to contextualize test logs. Describe the gas you're testing.
|
|
/datum/unit_test/lungs/proc/lungs_test_check_breath(test_name, mob/living/carbon/human/lab_rat, obj/item/organ/lungs/test_lungs, datum/gas_mixture/test_mix, expect_failure = FALSE)
|
|
// Setup a small volume of gas which represents one "breath" from test_mix.
|
|
var/datum/gas_mixture/test_breath
|
|
|
|
if(!isnull(test_mix))
|
|
var/total_moles = test_mix.total_moles()
|
|
if(total_moles > 0)
|
|
test_breath = test_mix.remove(total_moles * BREATH_PERCENTAGE)
|
|
|
|
if(isnull(test_breath))
|
|
test_breath = allocate(/datum/gas_mixture, BREATH_VOLUME)
|
|
|
|
// Backup of the breath mixture, to compare against after testing check_breath().
|
|
var/datum/gas_mixture/test_breath_backup = test_breath.copy()
|
|
|
|
// Get partial pressures for each "main" gas.
|
|
var/oxygen_pp = 0
|
|
var/nitro_pp = 0
|
|
var/co2_pp = 0
|
|
var/plasma_pp = 0
|
|
if(test_breath.total_moles() > 0)
|
|
oxygen_pp = test_breath.get_breath_partial_pressure(GET_MOLES(test_breath, /datum/gas/oxygen))
|
|
nitro_pp = test_breath.get_breath_partial_pressure(GET_MOLES(test_breath, /datum/gas/nitrogen))
|
|
co2_pp = test_breath.get_breath_partial_pressure(GET_MOLES(test_breath, /datum/gas/carbon_dioxide))
|
|
plasma_pp = test_breath.get_breath_partial_pressure(GET_MOLES(test_breath, /datum/gas/plasma))
|
|
|
|
// Minimum and maximum gas tolerances for the 4 main life-sustaining gases.
|
|
var/min_oxygen = test_lungs.safe_oxygen_min
|
|
var/min_nitro = test_lungs.safe_nitro_min
|
|
var/min_plasma = test_lungs.safe_plasma_min
|
|
var/max_oxygen = test_lungs.safe_oxygen_max
|
|
var/max_co2 = test_lungs.safe_co2_max
|
|
var/max_plasma = test_lungs.safe_plasma_max
|
|
|
|
// Test a single "breath" of air.
|
|
var/status_code = test_lungs.check_breath(test_breath, lab_rat)
|
|
|
|
// Ensures failed_last_breath is set as we expect, and that check_breath returns a corollary status code.
|
|
if(expect_failure)
|
|
TEST_ASSERT(!status_code, TEST_CHECK_BREATH_MESSAGE(test_lungs, "returned truthy / status code 1 (success) when it wasn't expected."))
|
|
TEST_ASSERT(lab_rat.failed_last_breath, TEST_CHECK_BREATH_MESSAGE(test_lungs, "should suffocate from [test_name]."))
|
|
else
|
|
TEST_ASSERT(status_code, TEST_CHECK_BREATH_MESSAGE(test_lungs, "returned falsy / status code 0 (failure) when it wasn't expected."))
|
|
TEST_ASSERT(!lab_rat.failed_last_breath, TEST_CHECK_BREATH_MESSAGE(test_lungs, "can't get a full breath from [test_name]."))
|
|
|
|
// Checks each "main" gas to ensure gas alerts are thrown/inhibited when expected.
|
|
lungs_test_alert_min(lab_rat, test_lungs, ALERT_NOT_ENOUGH_OXYGEN, min_oxygen, oxygen_pp)
|
|
lungs_test_alert_max(lab_rat, test_lungs, ALERT_TOO_MUCH_OXYGEN, max_oxygen, oxygen_pp)
|
|
|
|
lungs_test_alert_min(lab_rat, test_lungs, ALERT_NOT_ENOUGH_NITRO, min_nitro, nitro_pp)
|
|
|
|
lungs_test_alert_max(lab_rat, test_lungs, ALERT_TOO_MUCH_CO2, max_co2, co2_pp)
|
|
|
|
lungs_test_alert_min(lab_rat, test_lungs, ALERT_NOT_ENOUGH_PLASMA, min_plasma, plasma_pp)
|
|
lungs_test_alert_max(lab_rat, test_lungs, ALERT_TOO_MUCH_PLASMA, max_plasma, plasma_pp)
|
|
|
|
// Track the volumes of O2 and CO2 which are expected to be exhaled.
|
|
var/expected_oxygen = GET_MOLES(test_breath_backup, /datum/gas/oxygen)
|
|
var/expected_nitro = GET_MOLES(test_breath_backup, /datum/gas/nitrogen)
|
|
var/expected_co2 = GET_MOLES(test_breath_backup, /datum/gas/carbon_dioxide)
|
|
var/expected_plasma = GET_MOLES(test_breath_backup, /datum/gas/plasma)
|
|
|
|
// Setup expectations for main gas exchange tests.
|
|
if(min_oxygen)
|
|
expected_co2 += expected_oxygen
|
|
expected_oxygen = 0
|
|
if(min_nitro)
|
|
expected_co2 += GET_MOLES(test_breath_backup, /datum/gas/nitrogen)
|
|
expected_nitro = 0
|
|
if(min_plasma)
|
|
expected_co2 += GET_MOLES(test_breath_backup, /datum/gas/plasma)
|
|
expected_plasma = 0
|
|
|
|
// Validate conversion of inhaled gas to exhaled gas.
|
|
if(min_oxygen)
|
|
TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/oxygen), expected_oxygen), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should consume all Oxygen initially present in the breath."))
|
|
TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/carbon_dioxide), expected_co2), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should convert Oxygen into an equivalent volume of CO2."))
|
|
if(min_nitro)
|
|
TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/nitrogen), expected_nitro), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should consume all Nitrogen initially present in the breath."))
|
|
TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/carbon_dioxide), expected_co2), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should convert Nitrogen into an equivalent volume of CO2."))
|
|
if(min_plasma)
|
|
TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/plasma), expected_plasma), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should consume all Plasma initially present in the breath."))
|
|
TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/carbon_dioxide), expected_co2), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should convert Plasma into an equivalent volume of CO2."))
|
|
|
|
/// Tests minimum gas alerts by comparing gas pressure.
|
|
/datum/unit_test/lungs/proc/lungs_test_alert_min(mob/living/carbon/human/lab_rat, obj/item/organ/lungs/test_lungs, alert_name, min_pressure, pressure)
|
|
var/alert_thrown = lab_rat.has_alert(alert_name)
|
|
var/pressure_safe = (pressure >= min_pressure) || (min_pressure == 0)
|
|
TEST_ASSERT(!pressure_safe && alert_thrown || pressure_safe, TEST_ALERT_THROW_MESSAGE(test_lungs, alert_name))
|
|
TEST_ASSERT(pressure_safe && !alert_thrown || !pressure_safe, TEST_ALERT_INHIBIT_MESSAGE(test_lungs, alert_name))
|
|
|
|
/// Tests maximum gas alerts by comparing gas pressure.
|
|
/datum/unit_test/lungs/proc/lungs_test_alert_max(mob/living/carbon/human/lab_rat, obj/item/organ/lungs/test_lungs, alert_name, max_pressure, pressure)
|
|
var/alert_thrown = lab_rat.has_alert(alert_name)
|
|
var/pressure_safe = (pressure <= max_pressure) || (max_pressure == 0)
|
|
TEST_ASSERT(!pressure_safe && alert_thrown || pressure_safe, TEST_ALERT_THROW_MESSAGE(test_lungs, alert_name))
|
|
TEST_ASSERT(pressure_safe && !alert_thrown || !pressure_safe, TEST_ALERT_INHIBIT_MESSAGE(test_lungs, alert_name))
|
|
|
|
/// Set up a 2500-Litre gas mixture with the given gases and percentages.
|
|
/datum/unit_test/lungs/proc/create_gas_mix(list/gas_to_percent)
|
|
var/datum/gas_mixture/test_mix = allocate(/datum/gas_mixture, 2500)
|
|
test_mix.temperature = T20C
|
|
for(var/datum/gas/gas_type as anything in gas_to_percent)
|
|
test_mix.add_gas(gas_type)
|
|
test_mix.moles[gas_type] = (ONE_ATMOSPHERE * 2500 / (R_IDEAL_GAS_EQUATION * T20C) * gas_to_percent[gas_type])
|
|
return test_mix
|
|
|
|
/// Set up an O2/N2 gas mix which is "ideal" for organic life.
|
|
/datum/unit_test/lungs/proc/create_standard_mix()
|
|
return create_gas_mix(list(/datum/gas/oxygen = O2STANDARD, /datum/gas/nitrogen = N2STANDARD))
|
|
|
|
/// Set up a pure Nitrogen gas mix.
|
|
/datum/unit_test/lungs/proc/create_nitrogen_mix()
|
|
return create_gas_mix(list(/datum/gas/nitrogen = 1))
|
|
|
|
/// Set up an O2/N2 gas mix which is "ideal" for plasmamen.
|
|
/datum/unit_test/lungs/proc/create_plasma_mix()
|
|
return create_gas_mix(list(/datum/gas/plasma = 1))
|
|
|
|
/// Set up an Lavaland gas mix which is "ideal" for Ashwalker life.
|
|
/datum/unit_test/lungs/proc/create_lavaland_mix()
|
|
var/datum/gas_mixture/immutable/planetary/lavaland_mix = SSair.parse_gas_string(LAVALAND_DEFAULT_ATMOS, /datum/gas_mixture/immutable/planetary)
|
|
var/datum/gas_mixture/test_mix = allocate(/datum/gas_mixture, 2500)
|
|
test_mix.copy_from(lavaland_mix)
|
|
return test_mix
|
|
|
|
#undef TEST_CHECK_BREATH_MESSAGE
|
|
#undef TEST_ALERT_THROW_MESSAGE
|
|
#undef TEST_ALERT_INHIBIT_MESSAGE
|
|
#undef GET_MOLES
|