diff --git a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs index 7190ac43..34e2bd22 100644 --- a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs +++ b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs @@ -79,11 +79,21 @@ impl Component for ClusterSettings { } .into_element(), ) + .child( + ToggleRow { + cluster_id, + field: BoolField::PerfFlags, + value: profile.perf_flags, + global: global.perf_flags.unwrap_or(true), + } + .into_element(), + ) .child(text_row(cluster_id, TextField::JvmArgs, &profile, &global)) .child(section_header("GAME")) .child( ToggleRow { cluster_id, + field: BoolField::ForceFullscreen, value: profile.force_fullscreen, global: global.force_fullscreen.unwrap_or(false), } @@ -125,6 +135,7 @@ impl Component for ClusterSettings { #[derive(Clone, Copy, PartialEq)] enum Field { ForceFullscreen, + PerfFlags, Resolution, MemMax, JavaPath, @@ -134,6 +145,7 @@ fn clear_update(field: Field) -> ProfileUpdate { let mut u = ProfileUpdate::default(); match field { Field::ForceFullscreen => u.force_fullscreen = Patch::Clear, + Field::PerfFlags => u.perf_flags = Patch::Clear, Field::Resolution => u.resolution = Patch::Clear, Field::MemMax => u.mem_max = Patch::Clear, Field::JavaPath => u.java_path = Patch::Clear, @@ -141,6 +153,45 @@ fn clear_update(field: Field) -> ProfileUpdate { u } +#[derive(Clone, Copy, PartialEq)] +enum BoolField { + ForceFullscreen, + PerfFlags, +} + +impl BoolField { + fn meta(self) -> (IconType, &'static str, &'static str) { + match self { + Self::ForceFullscreen => ( + IconType::Maximize01, + "Force Fullscreen", + "Force Minecraft to start in fullscreen mode.", + ), + Self::PerfFlags => ( + IconType::Rocket02, + "Performance JVM Flags", + "Tune the garbage collector, heap and string deduplication to suit this cluster's Java runtime.", + ), + } + } + + fn patch(self, value: Patch) -> ProfileUpdate { + let mut u = ProfileUpdate::default(); + match self { + Self::ForceFullscreen => u.force_fullscreen = value, + Self::PerfFlags => u.perf_flags = value, + } + u + } + + fn field(self) -> Field { + match self { + Self::ForceFullscreen => Field::ForceFullscreen, + Self::PerfFlags => Field::PerfFlags, + } + } +} + fn runtime_label(runtime: &JavaRuntime) -> String { format!("{} {} ({})", runtime.vendor, runtime.major, runtime.version) } @@ -180,6 +231,7 @@ fn override_cell( #[derive(PartialEq)] struct ToggleRow { cluster_id: i64, + field: BoolField, value: Option, global: bool, } @@ -187,6 +239,7 @@ struct ToggleRow { impl Component for ToggleRow { fn render(&self) -> impl IntoElement { let cluster_id = self.cluster_id; + let field = self.field; let overridden = self.value.is_some(); let global = self.global; let dispatch = use_dispatch(); @@ -202,27 +255,22 @@ impl Component for ToggleRow { return; } last.set(v); - dispatch.update_cluster_profile( - cluster_id, - ProfileUpdate { - force_fullscreen: Patch::Set(v), - ..Default::default() - }, - ); + dispatch.update_cluster_profile(cluster_id, field.patch(Patch::Set(v))); }); } let on_reset: EventHandler<()> = (move |()| { last.set(global); state.set(global); - dispatch.update_cluster_profile(cluster_id, clear_update(Field::ForceFullscreen)); + dispatch.update_cluster_profile(cluster_id, clear_update(field.field())); }) .into(); + let (icon, title, description) = field.meta(); settings_row( - IconType::Maximize01, - "Force Fullscreen", - "Force Minecraft to start in fullscreen mode.", + icon, + title, + description, override_cell(toggle(state), overridden, on_reset), ) } diff --git a/packages/oneclient_app/src/view/app/settings/minecraft.rs b/packages/oneclient_app/src/view/app/settings/minecraft.rs index e4797bdb..5b8f0749 100644 --- a/packages/oneclient_app/src/view/app/settings/minecraft.rs +++ b/packages/oneclient_app/src/view/app/settings/minecraft.rs @@ -38,6 +38,10 @@ impl Component for SettingsMinecraft { let v = profile.mem_max.map(|m| m.to_string()).unwrap_or_default(); move || v }); + let perf_flags = use_state({ + let v = profile.perf_flags.unwrap_or(true); + move || v + }); let jvm_args = use_state({ let v = profile.launch_args.clone().unwrap_or_default(); move || v @@ -62,6 +66,7 @@ impl Component for SettingsMinecraft { &width.read(), &height.read(), &memory.read(), + *perf_flags.read(), &jvm_args.read(), &pre_launch_command.read(), &wrapper_command.read(), @@ -94,6 +99,12 @@ impl Component for SettingsMinecraft { "The amount of memory in megabytes allocated for the game.", memory_field(memory), )) + .child(settings_row( + IconType::Rocket02, + "Performance JVM Flags", + "Tune the garbage collector, heap and string deduplication to suit whichever Java runtime a cluster launches with.", + toggle(perf_flags), + )) .child(settings_row( IconType::Terminal, "JVM Arguments", @@ -137,6 +148,7 @@ fn build_update( width: &str, height: &str, memory: &str, + perf_flags: bool, jvm_args: &str, pre: &str, wrapper: &str, @@ -159,6 +171,7 @@ fn build_update( force_fullscreen: Patch::Set(fullscreen), resolution, mem_max, + perf_flags: Patch::Set(perf_flags), launch_args: command_patch(jvm_args), hook_pre: command_patch(pre), hook_wrapper: command_patch(wrapper), diff --git a/packages/oneclient_cluster/src/profile.rs b/packages/oneclient_cluster/src/profile.rs index ecaa5fa8..4f611019 100644 --- a/packages/oneclient_cluster/src/profile.rs +++ b/packages/oneclient_cluster/src/profile.rs @@ -17,6 +17,7 @@ pub struct GameSettingsProfile { pub hook_wrapper: Option, pub hook_post: Option, pub os_extra: Option, + pub perf_flags: Option, } // `Resolution` moved to oneclient_common: the launch-argument builder needs it @@ -62,6 +63,7 @@ impl GameSettingsProfile { hook_wrapper: None, hook_post: None, os_extra: Some(SettingsOsExtra::default()), + perf_flags: Some(true), } } @@ -100,6 +102,9 @@ impl GameSettingsProfile { if self.os_extra.is_none() { self.os_extra = global.os_extra.clone(); } + if self.perf_flags.is_none() { + self.perf_flags = global.perf_flags; + } } pub fn from_row(row: SettingProfileRow) -> crate::ClusterResult { @@ -121,6 +126,7 @@ impl GameSettingsProfile { .os_extra .map(|json| serde_json::from_str(&json)) .transpose()?, + perf_flags: row.perf_flags.map(|v| v != 0), }) } @@ -144,6 +150,7 @@ impl GameSettingsProfile { .as_ref() .map(serde_json::to_string) .transpose()?, + perf_flags: self.perf_flags.map(i64::from), }) } } diff --git a/packages/oneclient_cluster/src/profiles.rs b/packages/oneclient_cluster/src/profiles.rs index 4e6cbdd2..c56910fe 100644 --- a/packages/oneclient_cluster/src/profiles.rs +++ b/packages/oneclient_cluster/src/profiles.rs @@ -132,6 +132,7 @@ pub struct ProfileUpdate { pub hook_wrapper: Patch, pub hook_post: Patch, pub os_extra: Patch, + pub perf_flags: Patch, } impl ProfileUpdate { @@ -151,6 +152,7 @@ impl ProfileUpdate { self.hook_post .apply_to_command_option(&mut profile.hook_post); self.os_extra.apply_to_option(&mut profile.os_extra); + self.perf_flags.apply_to_option(&mut profile.perf_flags); } } diff --git a/packages/oneclient_core/src/game/launch.rs b/packages/oneclient_core/src/game/launch.rs index 1b4adba7..4289dd10 100644 --- a/packages/oneclient_core/src/game/launch.rs +++ b/packages/oneclient_core/src/game/launch.rs @@ -264,6 +264,8 @@ pub async fn launch_cluster( profile.mem_max.unwrap_or(2048), profile.launch_args.clone().unwrap_or_default(), &java.os_arch, + java.major, + profile.perf_flags.unwrap_or(true), )?; let mut mc_args = arguments::minecraft_arguments( diff --git a/packages/oneclient_db/.sqlx/query-4ab22fa481720e3bae5befee821fe1c0689d666f8bbcde98890e2d4715e9bc80.json b/packages/oneclient_db/.sqlx/query-1408ae02e47afa471a08ae0279997066a17d6048d02e26eeb5ff88ad5275c6b5.json similarity index 87% rename from packages/oneclient_db/.sqlx/query-4ab22fa481720e3bae5befee821fe1c0689d666f8bbcde98890e2d4715e9bc80.json rename to packages/oneclient_db/.sqlx/query-1408ae02e47afa471a08ae0279997066a17d6048d02e26eeb5ff88ad5275c6b5.json index 58a0b670..285ff316 100644 --- a/packages/oneclient_db/.sqlx/query-4ab22fa481720e3bae5befee821fe1c0689d666f8bbcde98890e2d4715e9bc80.json +++ b/packages/oneclient_db/.sqlx/query-1408ae02e47afa471a08ae0279997066a17d6048d02e26eeb5ff88ad5275c6b5.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tSELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n\t\t hook_pre, hook_wrapper, hook_post, os_extra\n\t\tFROM setting_profiles\n\t\tORDER BY name ASC\n\t\t", + "query": "\n\t\tSELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n\t\t hook_pre, hook_wrapper, hook_post, os_extra, perf_flags\n\t\tFROM setting_profiles\n\t\tORDER BY name ASC\n\t\t", "describe": { "columns": [ { @@ -123,6 +123,17 @@ "name": "os_extra" } } + }, + { + "name": "perf_flags", + "ordinal": 11, + "type_info": "Integer", + "origin": { + "Table": { + "table": "setting_profiles", + "name": "perf_flags" + } + } } ], "parameters": { @@ -139,8 +150,9 @@ true, true, true, + true, true ] }, - "hash": "4ab22fa481720e3bae5befee821fe1c0689d666f8bbcde98890e2d4715e9bc80" + "hash": "1408ae02e47afa471a08ae0279997066a17d6048d02e26eeb5ff88ad5275c6b5" } diff --git a/packages/oneclient_db/.sqlx/query-b41126ba17cbc3005b446b23cebc29ffe6fc366294191e546808e9f591b723a9.json b/packages/oneclient_db/.sqlx/query-93745def11c9e2ced3c14a4537caa9910b7f5c3167bafebf80aa7e731c1a2dcf.json similarity index 72% rename from packages/oneclient_db/.sqlx/query-b41126ba17cbc3005b446b23cebc29ffe6fc366294191e546808e9f591b723a9.json rename to packages/oneclient_db/.sqlx/query-93745def11c9e2ced3c14a4537caa9910b7f5c3167bafebf80aa7e731c1a2dcf.json index 9649dd57..61c38c11 100644 --- a/packages/oneclient_db/.sqlx/query-b41126ba17cbc3005b446b23cebc29ffe6fc366294191e546808e9f591b723a9.json +++ b/packages/oneclient_db/.sqlx/query-93745def11c9e2ced3c14a4537caa9910b7f5c3167bafebf80aa7e731c1a2dcf.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tINSERT INTO setting_profiles (\n\t\t\tname, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n\t\t\thook_pre, hook_wrapper, hook_post, os_extra\n\t\t)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\tON CONFLICT(name) DO UPDATE SET\n\t\t\tjava_path = excluded.java_path,\n\t\t\tresolution = excluded.resolution,\n\t\t\tforce_fullscreen = excluded.force_fullscreen,\n\t\t\tmem_max = excluded.mem_max,\n\t\t\tlaunch_args = excluded.launch_args,\n\t\t\tlaunch_env = excluded.launch_env,\n\t\t\thook_pre = excluded.hook_pre,\n\t\t\thook_wrapper = excluded.hook_wrapper,\n\t\t\thook_post = excluded.hook_post,\n\t\t\tos_extra = excluded.os_extra\n\t\tRETURNING name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n hook_pre, hook_wrapper, hook_post, os_extra\n\t\t", + "query": "\n\t\tINSERT INTO setting_profiles (\n\t\t\tname, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n\t\t\thook_pre, hook_wrapper, hook_post, os_extra, perf_flags\n\t\t)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\tON CONFLICT(name) DO UPDATE SET\n\t\t\tjava_path = excluded.java_path,\n\t\t\tresolution = excluded.resolution,\n\t\t\tforce_fullscreen = excluded.force_fullscreen,\n\t\t\tmem_max = excluded.mem_max,\n\t\t\tlaunch_args = excluded.launch_args,\n\t\t\tlaunch_env = excluded.launch_env,\n\t\t\thook_pre = excluded.hook_pre,\n\t\t\thook_wrapper = excluded.hook_wrapper,\n\t\t\thook_post = excluded.hook_post,\n\t\t\tos_extra = excluded.os_extra,\n\t\t\tperf_flags = excluded.perf_flags\n\t\tRETURNING name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n hook_pre, hook_wrapper, hook_post, os_extra, perf_flags\n\t\t", "describe": { "columns": [ { @@ -123,10 +123,21 @@ "name": "os_extra" } } + }, + { + "name": "perf_flags", + "ordinal": 11, + "type_info": "Integer", + "origin": { + "Table": { + "table": "setting_profiles", + "name": "perf_flags" + } + } } ], "parameters": { - "Right": 11 + "Right": 12 }, "nullable": [ false, @@ -139,8 +150,9 @@ true, true, true, + true, true ] }, - "hash": "b41126ba17cbc3005b446b23cebc29ffe6fc366294191e546808e9f591b723a9" + "hash": "93745def11c9e2ced3c14a4537caa9910b7f5c3167bafebf80aa7e731c1a2dcf" } diff --git a/packages/oneclient_db/.sqlx/query-de1b40c927c9819f675b8e3e8e1f583c41450ae9deda20739d82d95e8ca70b27.json b/packages/oneclient_db/.sqlx/query-eea8944fa38835f879435dfa511737722f7a32511b386d34d7e1ead8f2cbc9a4.json similarity index 87% rename from packages/oneclient_db/.sqlx/query-de1b40c927c9819f675b8e3e8e1f583c41450ae9deda20739d82d95e8ca70b27.json rename to packages/oneclient_db/.sqlx/query-eea8944fa38835f879435dfa511737722f7a32511b386d34d7e1ead8f2cbc9a4.json index 93a67f21..727d5411 100644 --- a/packages/oneclient_db/.sqlx/query-de1b40c927c9819f675b8e3e8e1f583c41450ae9deda20739d82d95e8ca70b27.json +++ b/packages/oneclient_db/.sqlx/query-eea8944fa38835f879435dfa511737722f7a32511b386d34d7e1ead8f2cbc9a4.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tSELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n\t\t hook_pre, hook_wrapper, hook_post, os_extra\n\t\tFROM setting_profiles\n\t\tWHERE name = ?\n\t\t", + "query": "\n\t\tSELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env,\n\t\t hook_pre, hook_wrapper, hook_post, os_extra, perf_flags\n\t\tFROM setting_profiles\n\t\tWHERE name = ?\n\t\t", "describe": { "columns": [ { @@ -123,6 +123,17 @@ "name": "os_extra" } } + }, + { + "name": "perf_flags", + "ordinal": 11, + "type_info": "Integer", + "origin": { + "Table": { + "table": "setting_profiles", + "name": "perf_flags" + } + } } ], "parameters": { @@ -139,8 +150,9 @@ true, true, true, + true, true ] }, - "hash": "de1b40c927c9819f675b8e3e8e1f583c41450ae9deda20739d82d95e8ca70b27" + "hash": "eea8944fa38835f879435dfa511737722f7a32511b386d34d7e1ead8f2cbc9a4" } diff --git a/packages/oneclient_db/migrations/20260731120000_perf_jvm_flags.sql b/packages/oneclient_db/migrations/20260731120000_perf_jvm_flags.sql new file mode 100644 index 00000000..9dec507f --- /dev/null +++ b/packages/oneclient_db/migrations/20260731120000_perf_jvm_flags.sql @@ -0,0 +1 @@ +ALTER TABLE setting_profiles ADD COLUMN perf_flags INTEGER; diff --git a/packages/oneclient_db/src/dao/setting_profile.rs b/packages/oneclient_db/src/dao/setting_profile.rs index c82441c2..0453b002 100644 --- a/packages/oneclient_db/src/dao/setting_profile.rs +++ b/packages/oneclient_db/src/dao/setting_profile.rs @@ -15,7 +15,7 @@ pub async fn list_all(pool: &SqlitePool) -> DbResult> { SettingProfileRow, r#" SELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, launch_env, - hook_pre, hook_wrapper, hook_post, os_extra + hook_pre, hook_wrapper, hook_post, os_extra, perf_flags FROM setting_profiles ORDER BY name ASC "#, @@ -31,7 +31,7 @@ pub async fn get_by_name(pool: &SqlitePool, name: &str) -> DbResult, pub hook_post: Option, pub os_extra: Option, + pub perf_flags: Option, } diff --git a/packages/oneclient_mc/src/arguments.rs b/packages/oneclient_mc/src/arguments.rs index f8efd7e6..f52090c7 100644 --- a/packages/oneclient_mc/src/arguments.rs +++ b/packages/oneclient_mc/src/arguments.rs @@ -22,6 +22,8 @@ pub fn java_arguments( mem_max: u32, custom_args: String, java_arch: &str, + java_major: u32, + perf_flags: bool, ) -> McResult> { let mut parsed = Vec::new(); if let Some(args) = arguments { @@ -51,12 +53,52 @@ pub fn java_arguments( parsed.push(classpaths.to_string()); } + if perf_flags { + parsed.extend(performance_flags(java_major, java_arch, mem_max)); + } + parsed.push(format!("-Xmx{mem_max}M")); parsed.extend(split_custom_args(&custom_args)); Ok(parsed) } +#[must_use] +pub fn performance_flags(java_major: u32, java_arch: &str, mem_max: u32) -> Vec { + let mut flags = vec![format!("-Xms{mem_max}M")]; + + if (7..=8).contains(&java_major) { + flags.push("-XX:+UseG1GC".to_string()); + } else if java_major >= 21 && is_64_bit(java_arch) { + flags.push("-XX:+UseZGC".to_string()); + if java_major <= 22 { + flags.push("-XX:+ZGenerational".to_string()); + } + if java_major == 24 { + flags.push("-XX:+UnlockExperimentalVMOptions".to_string()); + } + if java_major >= 24 { + flags.push("-XX:+UseCompactObjectHeaders".to_string()); + } + } + + // Arrived in 8u20, so Java 7 is the one runtime that would reject it. Where + // the selected collector does not support dedup - anything but G1 before 18 + // - the JVM ignores the flag instead of refusing to start, verified on 8. + if java_major >= 8 { + flags.push("-XX:+UseStringDeduplication".to_string()); + } + + flags +} + +fn is_64_bit(java_arch: &str) -> bool { + matches!( + java_arch, + "amd64" | "x86_64" | "x64" | "aarch64" | "arm64" | "ppc64le" | "s390x" | "riscv64" + ) +} + /// Splits a user-entered JVM argument string into individual arguments. /// /// Whitespace-separated, with double quotes for values that contain spaces @@ -453,7 +495,7 @@ where #[cfg(test)] mod tests { - use super::split_custom_args; + use super::{performance_flags, split_custom_args}; #[test] fn blank_input_contributes_nothing() { @@ -481,4 +523,112 @@ mod tests { fn an_empty_quoted_value_survives() { assert_eq!(split_custom_args(r#"-Dempty="""#), vec!["-Dempty="]); } + + #[test] + fn a_java_8_runtime_gets_g1() { + assert_eq!( + performance_flags(8, "amd64", 4096), + vec!["-Xms4096M", "-XX:+UseG1GC", "-XX:+UseStringDeduplication"] + ); + } + + #[test] + fn the_versions_told_to_upgrade_get_no_collector() { + for major in [9, 11, 17, 20] { + let flags = performance_flags(major, "amd64", 4096); + assert_eq!( + flags, + vec!["-Xms4096M", "-XX:+UseStringDeduplication"], + "java {major} should get a fixed heap and no collector flag" + ); + } + } + + #[test] + fn a_32_bit_runtime_never_gets_zgc() { + for arch in ["x86", "arm", "i386", "unknown"] { + let flags = performance_flags(25, arch, 4096); + assert!( + !flags.iter().any(|f| f.contains("UseZGC")), + "{arch} must not select a collector it cannot load" + ); + assert_eq!(flags, vec!["-Xms4096M", "-XX:+UseStringDeduplication"]); + } + } + + #[test] + fn zgc_is_asked_for_a_young_generation_only_before_23() { + assert!(performance_flags(21, "amd64", 4096).contains(&"-XX:+ZGenerational".to_string())); + assert!(performance_flags(22, "aarch64", 4096).contains(&"-XX:+ZGenerational".to_string())); + assert!(!performance_flags(23, "amd64", 4096).contains(&"-XX:+ZGenerational".to_string())); + assert!(!performance_flags(25, "amd64", 4096).contains(&"-XX:+ZGenerational".to_string())); + } + + #[test] + fn compact_headers_are_unlocked_only_on_24() { + for major in [21, 23] { + assert!( + !performance_flags(major, "amd64", 4096) + .contains(&"-XX:+UseCompactObjectHeaders".to_string()), + "java {major} predates compact object headers" + ); + } + + let j24 = performance_flags(24, "amd64", 4096); + let unlock = j24 + .iter() + .position(|f| f == "-XX:+UnlockExperimentalVMOptions") + .expect("24 needs the unlock flag"); + let compact = j24 + .iter() + .position(|f| f == "-XX:+UseCompactObjectHeaders") + .expect("24 supports compact headers"); + assert!( + unlock < compact, + "the unlock flag must precede the option it unlocks" + ); + + let j25 = performance_flags(25, "amd64", 4096); + assert!(j25.contains(&"-XX:+UseCompactObjectHeaders".to_string())); + assert!(!j25.contains(&"-XX:+UnlockExperimentalVMOptions".to_string())); + } + + #[test] + fn only_java_7_is_denied_string_deduplication() { + assert!( + !performance_flags(7, "amd64", 4096) + .contains(&"-XX:+UseStringDeduplication".to_string()) + ); + for major in [8, 17, 21, 24, 25] { + assert!( + performance_flags(major, "amd64", 4096) + .contains(&"-XX:+UseStringDeduplication".to_string()), + "java {major} should deduplicate strings" + ); + } + } + + #[test] + fn a_modern_runtime_gets_the_full_set() { + assert_eq!( + performance_flags(25, "aarch64", 4096), + vec![ + "-Xms4096M", + "-XX:+UseZGC", + "-XX:+UseCompactObjectHeaders", + "-XX:+UseStringDeduplication" + ] + ); + } + + #[test] + fn every_tier_pins_the_heap_to_the_memory_setting() { + for major in [8, 17, 21, 25] { + assert_eq!( + performance_flags(major, "amd64", 8192).first().unwrap(), + "-Xms8192M", + "java {major} should start at the memory setting" + ); + } + } }