add better default JVM args

This commit is contained in:
Julian Chang 2026-07-31 04:29:02 +07:00
parent 087365e877
commit fb4c16c027
No known key found for this signature in database
12 changed files with 287 additions and 25 deletions

View file

@ -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<bool>) -> 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<bool>,
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),
)
}

View file

@ -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),

View file

@ -17,6 +17,7 @@ pub struct GameSettingsProfile {
pub hook_wrapper: Option<String>,
pub hook_post: Option<String>,
pub os_extra: Option<SettingsOsExtra>,
pub perf_flags: Option<bool>,
}
// `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<Self> {
@ -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),
})
}
}

View file

@ -132,6 +132,7 @@ pub struct ProfileUpdate {
pub hook_wrapper: Patch<String>,
pub hook_post: Patch<String>,
pub os_extra: Patch<SettingsOsExtra>,
pub perf_flags: Patch<bool>,
}
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);
}
}

View file

@ -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(

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -0,0 +1 @@
ALTER TABLE setting_profiles ADD COLUMN perf_flags INTEGER;

View file

@ -15,7 +15,7 @@ pub async fn list_all(pool: &SqlitePool) -> DbResult<Vec<SettingProfileRow>> {
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<Option<Setti
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
WHERE name = ?
"#,
@ -59,9 +59,9 @@ pub async fn upsert(
r#"
INSERT INTO setting_profiles (
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
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
java_path = excluded.java_path,
resolution = excluded.resolution,
@ -72,9 +72,10 @@ pub async fn upsert(
hook_pre = excluded.hook_pre,
hook_wrapper = excluded.hook_wrapper,
hook_post = excluded.hook_post,
os_extra = excluded.os_extra
os_extra = excluded.os_extra,
perf_flags = excluded.perf_flags
RETURNING 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
"#,
&row.name,
&row.java_path,
@ -87,6 +88,7 @@ pub async fn upsert(
&row.hook_wrapper,
&row.hook_post,
&row.os_extra,
row.perf_flags,
)
.fetch_one(pool)
.await

View file

@ -13,4 +13,5 @@ pub struct SettingProfileRow {
pub hook_wrapper: Option<String>,
pub hook_post: Option<String>,
pub os_extra: Option<String>,
pub perf_flags: Option<i64>,
}

View file

@ -22,6 +22,8 @@ pub fn java_arguments(
mem_max: u32,
custom_args: String,
java_arch: &str,
java_major: u32,
perf_flags: bool,
) -> McResult<Vec<String>> {
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<String> {
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"
);
}
}
}