feat: lower the default memory on lower memory machines (#783)

This commit is contained in:
Julian Chang 2026-09-04 17:55:57 +02:00 • committed by LynithDev
parent 86145c7180
commit 72a9b63f24
10 changed files with 84 additions and 27 deletions

1
Cargo.lock generated
View file

@ -4557,6 +4557,7 @@ dependencies = [
"interfrost",
"serde",
"strum",
"sysinfo",
"thiserror 2.0.18",
"url",
]

View file

@ -39,7 +39,7 @@ pub fn memory_field(mut memory: State<String>) -> impl IntoElement {
.child(
TextInput::new(memory)
.width(Size::px(90.))
.placeholder("4096")
.placeholder(oneclient_common::default_mem_max().to_string())
.on_validate(validate_memory)
.trailing(
label()

View file

@ -5,7 +5,9 @@ use chrono::{Datelike, NaiveDate};
use oneclient_core::clusters::Cluster;
use oneclient_common::domain::GameLoader;
use oneclient_common::{ParsedMcVersion, VersionKey, format_mc_version, parse_mc_version};
use sysinfo::{MemoryRefreshKind, RefreshKind, System};
use oneclient_common::MEMORY_HEADROOM_GB;
pub use oneclient_common::total_ram_mb;
pub type ClusterGroups = BTreeMap<ReleaseLine, Vec<Cluster>>;
@ -207,19 +209,7 @@ pub fn format_res((w, h): (u32, u32)) -> String {
/// Prevents user from choosing his max amount of ram preset (e.g Someone has 16GB of RAM,
/// so the max preset is 16GB - 2GB = 14GB)
const MEMORY_HEADROOM_GB: u32 = 2;
const MEMORY_PRESETS_GB: [u32; 10] = [2, 4, 6, 8, 12, 16, 24, 32, 48, 64];
pub fn total_ram_mb() -> u32 {
static TOTAL_RAM_MB: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
*TOTAL_RAM_MB.get_or_init(|| {
let system = System::new_with_specifics(
RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_ram()),
);
(system.total_memory() / 1024 / 1024).min(u32::MAX as u64) as u32
})
}
const MEMORY_PRESETS_GB: [u32; 11] = [2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64];
pub fn memory_presets_mb() -> Vec<u32> {
presets_for_total_gb((total_ram_mb() as f32 / 1024.).round() as u32)
@ -451,17 +441,17 @@ mod tests {
fn presets_stop_two_gigabytes_short_of_the_machine() {
assert_eq!(
presets_for_total_gb(16),
vec![2048, 4096, 6144, 8192, 12288, 14336]
vec![2048, 3072, 4096, 6144, 8192, 12288, 14336]
);
assert_eq!(
presets_for_total_gb(32),
vec![2048, 4096, 6144, 8192, 12288, 16384, 24576, 30720]
vec![2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576, 30720]
);
}
#[test]
fn a_ceiling_landing_on_a_round_step_is_not_repeated() {
assert_eq!(presets_for_total_gb(8), vec![2048, 4096, 6144]);
assert_eq!(presets_for_total_gb(8), vec![2048, 3072, 4096, 6144]);
}
#[test]

View file

@ -112,7 +112,9 @@ impl Component for ClusterSettings {
MemoryRow {
cluster_id,
value: profile.mem_max,
global: global.mem_max.unwrap_or(4096),
global: global
.mem_max
.unwrap_or_else(oneclient_common::default_mem_max),
}
.into_element(),
)

View file

@ -68,7 +68,7 @@ impl GameSettingsProfile {
java_path: None,
resolution: None,
force_fullscreen: Some(false),
mem_max: Some(4096),
mem_max: Some(oneclient_common::default_mem_max()),
launch_args: None,
launch_env: None,
hook_pre: None,

View file

@ -20,6 +20,7 @@ interfrost.workspace = true
directories.workspace = true
serde.workspace = true
strum.workspace = true
sysinfo.workspace = true
thiserror.workspace = true
url.workspace = true

View file

@ -4,6 +4,7 @@
pub mod consent;
pub mod constants;
pub mod domain;
pub mod memory;
pub mod os_ext;
pub mod paths;
pub mod patch;
@ -17,6 +18,7 @@ pub use domain::{
ContentType, GameLoader, HashAlgorithm, PackageUpdateMode, ProviderId, Resolution,
};
pub use error::{PathsError, PathsResult};
pub use memory::{MEMORY_HEADROOM_GB, default_mem_max, default_mem_max_for_total, total_ram_mb};
pub use os_ext::OsExt;
pub use patch::Patch;
pub use search::{MatchScore, SearchQuery, normalize_query};

View file

@ -0,0 +1,55 @@
use sysinfo::{MemoryRefreshKind, RefreshKind, System};
pub const MEMORY_HEADROOM_GB: u32 = 2;
const EIGHT_GB_MB: u32 = 7 * 1024;
const TWELVE_GB_MB: u32 = 11 * 1024;
pub fn total_ram_mb() -> u32 {
static TOTAL_RAM_MB: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
*TOTAL_RAM_MB.get_or_init(|| {
let system = System::new_with_specifics(
RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_ram()),
);
(system.total_memory() / 1024 / 1024).min(u32::MAX as u64) as u32
})
}
#[must_use]
pub fn default_mem_max() -> u32 {
default_mem_max_for_total(total_ram_mb())
}
#[must_use]
pub fn default_mem_max_for_total(total_mb: u32) -> u32 {
match total_mb {
0..EIGHT_GB_MB => 2048,
EIGHT_GB_MB..TWELVE_GB_MB => 3072,
_ => 4096,
}
}
#[cfg(test)]
mod tests {
use super::default_mem_max_for_total;
#[test]
fn the_default_heap_ramps_with_total_ram() {
assert_eq!(default_mem_max_for_total(3987), 2048); // 4GB
assert_eq!(default_mem_max_for_total(6060), 2048); // 6GB
assert_eq!(default_mem_max_for_total(7167), 2048);
assert_eq!(default_mem_max_for_total(7168), 3072);
assert_eq!(default_mem_max_for_total(7900), 3072); // 8GB
assert_eq!(default_mem_max_for_total(11263), 3072);
assert_eq!(default_mem_max_for_total(11264), 4096);
assert_eq!(default_mem_max_for_total(11800), 4096); // 12GB
assert_eq!(default_mem_max_for_total(16290), 4096); // 16GB
assert_eq!(default_mem_max_for_total(65229), 4096); // 64GB
}
#[test]
fn a_machine_that_reports_nothing_still_gets_a_heap() {
assert_eq!(default_mem_max_for_total(0), 2048);
}
}

View file

@ -259,7 +259,7 @@ pub async fn launch_cluster(
&libraries,
&classpaths,
&version_name,
profile.mem_max.unwrap_or(2048),
profile.mem_max.unwrap_or_else(oneclient_common::default_mem_max),
profile.launch_args.clone().unwrap_or_default(),
&java.os_arch,
java.major,
@ -349,8 +349,13 @@ pub async fn launch_cluster(
mc_version: cluster.mc_version.clone(),
});
let recorder =
SessionRecorder::start(state, cluster_id, profile.mem_max.unwrap_or(2048), &java).await;
let recorder = SessionRecorder::start(
state,
cluster_id,
profile.mem_max.unwrap_or_else(oneclient_common::default_mem_max),
&java,
)
.await;
// Pinned to the session row so that if the launcher exits first the next
// start can tell whether the game is still playing

View file

@ -13,11 +13,12 @@ async fn cluster_lifecycle_with_settings_profile() {
let global = state.settings.read().global_game_settings.clone();
// Deliberately not the global default of 4096 so the assertions distinguish
// "the cluster's own profile was used" from "fell back to global and matched"
// Deliberately not a value `default_mem_max()` can return so the assertions
// distinguish "the cluster's own profile was used" from "fell back to global
// and matched"
let cluster = state.clusters.create(
&global,
CreateClusterOptions::new("Test Cluster", "1.21.1", GameLoader::Fabric).mem_max(2048),
CreateClusterOptions::new("Test Cluster", "1.21.1", GameLoader::Fabric).mem_max(5120),
)
.await
.unwrap();
@ -31,7 +32,7 @@ async fn cluster_lifecycle_with_settings_profile() {
let resolved = state.clusters.resolve_settings(&global, &cluster)
.await
.unwrap();
assert_eq!(resolved.mem_max, Some(2048));
assert_eq!(resolved.mem_max, Some(5120));
state.clusters.update_profile(cluster.id,
ProfileUpdate {