diff --git a/launchlib/config.go b/launchlib/config.go index 302555c2..7d8a7b2a 100644 --- a/launchlib/config.go +++ b/launchlib/config.go @@ -73,6 +73,7 @@ type CustomLauncherConfig struct { type ExperimentalLauncherConfig struct { OverrideActiveProcessorCount bool `yaml:"overrideActiveProcessorCount"` + DynamicRAMPercentage bool `yaml:"dynamicRAMPercentage"` } type PrimaryCustomLauncherConfig struct { diff --git a/launchlib/launcher.go b/launchlib/launcher.go index 6a0feac1..16864d94 100644 --- a/launchlib/launcher.go +++ b/launchlib/launcher.go @@ -283,6 +283,9 @@ func createJvmOpts(combinedJvmOpts []string, customConfig *CustomLauncherConfig, if customConfig.Experimental.OverrideActiveProcessorCount { combinedJvmOpts = ensureActiveProcessorCount(combinedJvmOpts, logger) } + if customConfig.Experimental.DynamicRAMPercentage { + combinedJvmOpts = addDynamicRAMPercentageSystemProps(combinedJvmOpts, logger) + } return combinedJvmOpts } @@ -342,6 +345,16 @@ func ensureActiveProcessorCount(args []string, logger io.Writer) []string { return filtered } +func addDynamicRAMPercentageSystemProps(args []string, logger io.Writer) []string { + ramPercenter := NewScalingRAMPercenter(defaultFS) + ramPercent, err := ramPercenter.RAMPercent() + if err != nil { + _, _ = fmt.Fprintln(logger, "Failed to detect cgroup memory configuration, not setting dynamic RAM percentage system property", err.Error()) + return args + } + return append(args, fmt.Sprintf("-DDynamicRAMPercentage=%.1f", ramPercent)) +} + func hasMaxRAMOverride(args []string) bool { for _, arg := range args { if isMaxRAM(arg) { diff --git a/launchlib/memory.go b/launchlib/memory.go new file mode 100644 index 00000000..aae8479a --- /dev/null +++ b/launchlib/memory.go @@ -0,0 +1,110 @@ +package launchlib + +import ( + "io" + "io/fs" + "math" + "path/filepath" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +const ( + memGroupName = "memory" + memLimitName = "memory.limit_in_bytes" +) + +type RAMPercenter interface { + RAMPercent() (float64, error) +} + +type ChainedRAMPercenter struct { + delegates []RAMPercenter +} + +func NewChainedRAMPercenter(delegates ...RAMPercenter) RAMPercenter { + return ChainedRAMPercenter{ + delegates: delegates, + } +} + +func (c ChainedRAMPercenter) RAMPercent() (float64, error) { + for _, percenter := range c.delegates { + p, err := percenter.RAMPercent() + if err != nil { + // log and move on + } + return p, nil + } + return 0, errors.New("failed to get RAM percentage from all configured delegates") +} + +type StaticRAMPercent struct { + percent float64 +} + +func NewStaticRAMPercent(percent float64) RAMPercenter { + return StaticRAMPercent{ + percent: percent, + } +} + +func (s StaticRAMPercent) RAMPercent() (float64, error) { + return s.percent, nil +} + +const ( + lowerBound = 75 + upperBound = 95 + growthRate = 0.000000001 + // midpoint is 8 GiB in bytes + midpoint = 8589934592 + sharpness = 1 +) + +var ScalingFunc = genlog(lowerBound, upperBound, growthRate, midpoint, sharpness) + +type ScalingRAMPercent struct { + pather CGroupPather + fs fs.FS +} + +func NewScalingRAMPercenter(filesystem fs.FS) RAMPercenter { + return ScalingRAMPercent{ + fs: filesystem, + pather: NewCGroupV1Pather(filesystem), + } +} + +func (s ScalingRAMPercent) RAMPercent() (float64, error) { + // read limit from cgroup + memoryCGroupPath, err := s.pather.Path(memGroupName) + if err != nil { + return 0, errors.Wrap(err, "failed to get memory cgroup path") + } + + memLimitFilepath := filepath.Join(memoryCGroupPath, memLimitName) + memLimitFile, err := s.fs.Open(convertToFSPath(memLimitFilepath)) + if err != nil { + return 0, errors.Wrapf(err, "unable to open memory.limit_in_bytes at expected location: %s", memLimitFilepath) + } + memLimitBytes, err := io.ReadAll(memLimitFile) + if err != nil { + return 0, errors.Wrapf(err, "unable to read memory.limit_in_bytes") + } + memLimit, err := strconv.Atoi(strings.TrimSpace(string(memLimitBytes))) + if err != nil { + return 0, errors.New("unable to convert memory.limit_in_bytes value to expected type") + } + + return ScalingFunc(float64(memLimit)), nil +} + +func genlog(min float64, max float64, growthRate float64, midpoint float64, v float64) func(float64) float64 { + return func(in float64) float64 { + // https://en.wikipedia.org/wiki/Generalised_logistic_function#Definition + return min + (max-min)/(math.Pow(1+math.Pow(math.E, -1*growthRate*(in-midpoint)), 1/v)) + } +} diff --git a/launchlib/memory_test.go b/launchlib/memory_test.go new file mode 100644 index 00000000..fa252646 --- /dev/null +++ b/launchlib/memory_test.go @@ -0,0 +1,131 @@ +// Copyright 2023 Palantir Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package launchlib_test + +import ( + "fmt" + "io/fs" + "testing" + "testing/fstest" + + "github.com/palantir/go-java-launcher/launchlib" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + // We should remain at the default RAM percentage below ~2 GiB + lowMemoryLimitContent = []byte("2147483648\n") + // We should hit the midpoint @ 8 GiB + midpointMemoryLimitContent = []byte("8589934592\n") + // We should hit the upper limit @ 16 GiB + highMemoryLimitContent = []byte("17179869184\n") + badMemoryLimitContent = []byte(``) +) + +func TestRAMPercenter_DefaultCGroupV1RAMPercenter(t *testing.T) { + for _, test := range []struct { + name string + filesystem fs.FS + expectedRAMPercent string + expectedError error + }{ + { + name: "fails when unable to read memory.limit_in_bytes", + filesystem: fstest.MapFS{ + "proc/self/cgroup": &fstest.MapFile{ + Data: CGroupContent, + }, + "proc/self/mountinfo": &fstest.MapFile{ + Data: MountInfoContent, + }, + }, + expectedError: errors.New("unable to open memory.limit_in_bytes at expected location"), + }, + { + name: "fails when unable to parse memory.limit_in_bytes", + filesystem: fstest.MapFS{ + "proc/self/cgroup": &fstest.MapFile{ + Data: CGroupContent, + }, + "proc/self/mountinfo": &fstest.MapFile{ + Data: MountInfoContent, + }, + "sys/fs/cgroup/memory/memory.limit_in_bytes": &fstest.MapFile{ + Data: badMemoryLimitContent, + }, + }, + expectedError: errors.New("unable to convert memory.limit_in_bytes value to expected type"), + }, + { + name: "returns expected RAM percentage when memory.limit_in_bytes under 2 GiB", + filesystem: fstest.MapFS{ + "proc/self/cgroup": &fstest.MapFile{ + Data: CGroupContent, + }, + "proc/self/mountinfo": &fstest.MapFile{ + Data: MountInfoContent, + }, + "sys/fs/cgroup/memory/memory.limit_in_bytes": &fstest.MapFile{ + Data: lowMemoryLimitContent, + }, + }, + expectedRAMPercent: "75.0", + }, + { + name: "returns expected RAM percentage when memory.limit_in_bytes is 8 GiB", + filesystem: fstest.MapFS{ + "proc/self/cgroup": &fstest.MapFile{ + Data: CGroupContent, + }, + "proc/self/mountinfo": &fstest.MapFile{ + Data: MountInfoContent, + }, + "sys/fs/cgroup/memory/memory.limit_in_bytes": &fstest.MapFile{ + Data: midpointMemoryLimitContent, + }, + }, + expectedRAMPercent: "85.0", + }, + { + name: "returns expected RAM percentage when memory.limit_in_bytes over 16 GiB", + filesystem: fstest.MapFS{ + "proc/self/cgroup": &fstest.MapFile{ + Data: CGroupContent, + }, + "proc/self/mountinfo": &fstest.MapFile{ + Data: MountInfoContent, + }, + "sys/fs/cgroup/memory/memory.limit_in_bytes": &fstest.MapFile{ + Data: highMemoryLimitContent, + }, + }, + expectedRAMPercent: "95.0", + }, + } { + t.Run(test.name, func(t *testing.T) { + percenter := launchlib.NewScalingRAMPercenter(test.filesystem) + percent, err := percenter.RAMPercent() + if test.expectedError != nil { + require.Error(t, err) + assert.Contains(t, err.Error(), test.expectedError.Error()) + return + } + assert.NoError(t, err) + assert.Equal(t, test.expectedRAMPercent, fmt.Sprintf("%.1f", percent)) + }) + } +}