161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
import type { GcContext, GcStrategyDefinition, GcStrategyId, ResolvedGcStrategyId } from './types'
|
|
|
|
// Official Minecraft launcher G1GC tuning. `-XX:SurvivorRatio=8` (the flag in
|
|
// the leak of the original list was misspelled "SurvialRation", which JVMs
|
|
// would reject).
|
|
function buildG1gcMojangArgs(): string {
|
|
return [
|
|
'-XX:+UseG1GC',
|
|
'-XX:+ParallelRefProcEnabled',
|
|
'-XX:MaxGCPauseMillis=200',
|
|
'-XX:+UnlockExperimentalVMOptions',
|
|
'-XX:+DisableExplicitGC',
|
|
'-XX:+AlwaysPreTouch',
|
|
'-XX:G1NewSizePercent=30',
|
|
'-XX:G1MaxNewSizePercent=40',
|
|
'-XX:G1HeapRegionSize=8M',
|
|
'-XX:G1ReservePercent=15',
|
|
'-XX:G1HeapWastePercent=5',
|
|
'-XX:G1MixedGCCountTarget=4',
|
|
'-XX:InitiatingHeapOccupancyPercent=15',
|
|
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
'-XX:SurvivorRatio=8',
|
|
].join(' ')
|
|
}
|
|
|
|
// PCL-style Shenandoah (adaptive, no large pages — the safe default variant).
|
|
function buildPclShenandoahArgs(): string {
|
|
return [
|
|
'-XX:+UseShenandoahGC',
|
|
'-XX:ShenandoahGCHeuristics=adaptive',
|
|
'-XX:+AlwaysPreTouch',
|
|
'-XX:+DisableExplicitGC',
|
|
].join(' ')
|
|
}
|
|
|
|
// Shenandoah with large pages enabled (may warn on systems without support).
|
|
function buildShenandoahArgs(): string {
|
|
return [
|
|
'-XX:+UseShenandoahGC',
|
|
'-XX:ShenandoahGCHeuristics=adaptive',
|
|
'-XX:+AlwaysPreTouch',
|
|
'-XX:+UseLargePages',
|
|
'-XX:+DisableExplicitGC',
|
|
].join(' ')
|
|
}
|
|
|
|
function buildZgcArgs(javaMajorVersion: number | null): string {
|
|
const args = ['-XX:+UseZGC']
|
|
// Generational ZGC only exists on JDK 21+.
|
|
if (javaMajorVersion !== null && javaMajorVersion >= 21) {
|
|
args.push('-XX:+ZGenerational')
|
|
}
|
|
args.push('-XX:+AlwaysPreTouch', '-XX:-ZUncommit')
|
|
return args.join(' ')
|
|
}
|
|
|
|
// Detection only tags a preset when its *complete* flag set is present in the
|
|
// pasted args — a partial or edited arg list is treated as the user's own raw
|
|
// args, never auto-mislabeled as a preset.
|
|
function tokensOf(argString: string): string[] {
|
|
return argString.split(/\s+/).filter(Boolean)
|
|
}
|
|
|
|
function hasFullArgSet(pastedArgs: string, presetArgString: string): boolean {
|
|
const inputSet = new Set(tokensOf(pastedArgs))
|
|
return tokensOf(presetArgString).every((token) => inputSet.has(token))
|
|
}
|
|
|
|
function detectG1gcMojang(args: string): boolean {
|
|
return hasFullArgSet(args, buildG1gcMojangArgs())
|
|
}
|
|
|
|
// PCL Shenandoah: complete adaptive set, and no large pages.
|
|
function detectPclShenandoah(args: string): boolean {
|
|
return hasFullArgSet(args, buildPclShenandoahArgs()) && !args.includes('-XX:+UseLargePages')
|
|
}
|
|
|
|
// Shenandoah with large pages (its full set already requires `-XX:+UseLargePages`).
|
|
function detectShenandoah(args: string): boolean {
|
|
return hasFullArgSet(args, buildShenandoahArgs())
|
|
}
|
|
|
|
function detectZgc(args: string): boolean {
|
|
return hasFullArgSet(args, buildZgcArgs(null))
|
|
}
|
|
|
|
export const GC_STRATEGY_DEFINITIONS: Record<ResolvedGcStrategyId, GcStrategyDefinition> = {
|
|
'g1gc-mojang': {
|
|
id: 'g1gc-mojang',
|
|
baseArgs: buildG1gcMojangArgs(),
|
|
detect: detectG1gcMojang,
|
|
buildArgs: () => buildG1gcMojangArgs(),
|
|
},
|
|
pcl: {
|
|
id: 'pcl',
|
|
baseArgs: buildPclShenandoahArgs(),
|
|
detect: detectPclShenandoah,
|
|
buildArgs: () => buildPclShenandoahArgs(),
|
|
},
|
|
shenandoah: {
|
|
id: 'shenandoah',
|
|
baseArgs: buildShenandoahArgs(),
|
|
detect: detectShenandoah,
|
|
buildArgs: () => buildShenandoahArgs(),
|
|
},
|
|
zgc: {
|
|
id: 'zgc',
|
|
baseArgs: buildZgcArgs(null),
|
|
detect: detectZgc,
|
|
buildArgs: (context) => buildZgcArgs(context?.javaMajorVersion ?? null),
|
|
},
|
|
}
|
|
|
|
export function detectGcStrategy(args: string): ResolvedGcStrategyId | null {
|
|
for (const [strategyId, definition] of Object.entries(GC_STRATEGY_DEFINITIONS)) {
|
|
if (definition.detect(args)) {
|
|
return strategyId as ResolvedGcStrategyId
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function getStrategyBaseArgs(strategyId: GcStrategyId): string {
|
|
if (strategyId === 'auto') {
|
|
return GC_STRATEGY_DEFINITIONS['g1gc-mojang'].baseArgs
|
|
}
|
|
return GC_STRATEGY_DEFINITIONS[strategyId].baseArgs
|
|
}
|
|
|
|
/**
|
|
* The preferred strategy plus the fallback chain, ordered by preference. The
|
|
* backend verifies each block against the actual JVM and picks the first one
|
|
* that is accepted (pruning unsupported tuning flags along the way).
|
|
*
|
|
* Fallbacks only ever move to less resource-hungry strategies — if the
|
|
* heuristic deliberately avoided ZGC (insufficient resources), we must not
|
|
* silently jump back up to it when Shenandoah is unavailable.
|
|
*/
|
|
const SAFE_TO_DEMANDING: ResolvedGcStrategyId[] = ['g1gc-mojang', 'pcl', 'shenandoah', 'zgc']
|
|
|
|
export function buildGcCandidateChain(
|
|
context: GcContext,
|
|
preferred: ResolvedGcStrategyId,
|
|
): { ids: string[]; args: string[][] } {
|
|
const preferredDemand = SAFE_TO_DEMANDING.indexOf(preferred)
|
|
const ids: string[] = [preferred]
|
|
if (preferredDemand > 0) {
|
|
for (let demand = preferredDemand - 1; demand >= 0; demand -= 1) {
|
|
ids.push(SAFE_TO_DEMANDING[demand])
|
|
}
|
|
}
|
|
// Absolute last resort: just the G1 selector (known to every HotSpot JVM).
|
|
ids.push('minimal-g1')
|
|
const args = ids.map((id) => {
|
|
if (id === 'minimal-g1') return ['-XX:+UseG1GC']
|
|
return GC_STRATEGY_DEFINITIONS[id].buildArgs(context).split(/\s+/).filter(Boolean)
|
|
})
|
|
return { ids, args }
|
|
}
|