diff --git a/.gitignore b/.gitignore index 7dfd723..30d0b75 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ # limitations under the License. bin/ +.DS_Store diff --git a/count/count_classes.go b/count/count_classes.go index ac99147..40c3257 100644 --- a/count/count_classes.go +++ b/count/count_classes.go @@ -24,7 +24,7 @@ import ( "strings" ) -var ClassExtensions = []string{".class", ".clj", ".groovy", ".kts"} +var ClassExtensions = []string{".class", ".classdata", ".clj", ".groovy", ".kts"} func Classes(path string) (int, error) { file := filepath.Join(path, "lib", "modules") @@ -114,3 +114,19 @@ func ModuleClasses(file string) (int, error) { return count, nil } + +func JarClassesFrom(paths ...string) (int, int, error) { + var agentClassCount, skippedPaths int + + for _, path := range paths { + if c, err := JarClasses(path); err == nil { + agentClassCount += c + } else if strings.Contains(err.Error(), "no such file or directory") { + skippedPaths++ + continue + } else { + return 0, 0, fmt.Errorf("unable to count classes of jar at %s\n%w", path, err) + } + } + return agentClassCount, skippedPaths, nil +} diff --git a/helper/memory_calculator.go b/helper/memory_calculator.go index b4c8be9..5e17fe4 100644 --- a/helper/memory_calculator.go +++ b/helper/memory_calculator.go @@ -18,6 +18,7 @@ package helper import ( "fmt" + "github.com/mattn/go-shellwords" "io/ioutil" "os" "regexp" @@ -75,6 +76,12 @@ func (m MemoryCalculator) Execute() (map[string]string, error) { } } + var values []string + opts, ok := os.LookupEnv("JAVA_TOOL_OPTIONS") + if ok { + values = append(values, opts) + } + if s, ok := os.LookupEnv("BPL_JVM_LOADED_CLASS_COUNT"); ok { if c.LoadedClassCount, err = strconv.Atoi(s); err != nil { return nil, fmt.Errorf("unable to convert $BPL_JVM_LOADED_CLASS_COUNT=%s to integer\n%w", s, err) @@ -94,6 +101,11 @@ func (m MemoryCalculator) Execute() (map[string]string, error) { } } + agentClassCount, err := m.CountAgentClasses(opts) + if err != nil { + return nil, err + } + staticAdjustment := 0 adjustmentFactor := uint64(100) if adj, ok := os.LookupEnv("BPL_JVM_CLASS_ADJUSTMENT"); ok { @@ -110,12 +122,12 @@ func (m MemoryCalculator) Execute() (map[string]string, error) { appClassCount, err := count.Classes(appPath) - totalClasses := float64(jvmClassCount+appClassCount+staticAdjustment) * (float64(adjustmentFactor) / 100.0) + totalClasses := float64(jvmClassCount+appClassCount+agentClassCount+staticAdjustment) * (float64(adjustmentFactor) / 100.0) if err != nil { return nil, fmt.Errorf("unable to determine class count\n%w", err) } - m.Logger.Debugf("Memory Calculation: (%d%% * (%d + %d + %d)) * %0.2f", adjustmentFactor, jvmClassCount, appClassCount, staticAdjustment, ClassLoadFactor) + m.Logger.Debugf("Memory Calculation: (%d%% * (%d + %d + %d + %d)) * %0.2f", adjustmentFactor, jvmClassCount, appClassCount, agentClassCount, staticAdjustment, ClassLoadFactor) c.LoadedClassCount = int(totalClasses * ClassLoadFactor) } @@ -154,13 +166,7 @@ func (m MemoryCalculator) Execute() (map[string]string, error) { c.TotalMemory = calc.Size{Value: totalMemory} } - var values []string - s, ok := os.LookupEnv("JAVA_TOOL_OPTIONS") - if ok { - values = append(values, s) - } - - r, err := c.Calculate(s) + r, err := c.Calculate(opts) if err != nil { return nil, fmt.Errorf("unable to calculate memory configuration\n%w", err) } @@ -220,3 +226,26 @@ func parseMemInfo(s string) (int64, error) { } return num * unit, nil } + +func (m MemoryCalculator) CountAgentClasses(opts string) (int, error) { + var agentClassCount, skippedAgents int + if p, err := shellwords.Parse(opts); err != nil { + return 0, fmt.Errorf("unable to parse $JAVA_TOOL_OPTIONS\n%w", err) + } else { + var agentPaths []string + for _, s := range p { + if strings.HasPrefix(s, "-javaagent:") { + agentPaths = append(agentPaths, strings.Split(s, ":")[1]) + } + } + if len(agentPaths) > 0 { + agentClassCount, skippedAgents, err = count.JarClassesFrom(agentPaths...) + if err != nil { + return 0, fmt.Errorf("error counting agent jar classes \n%w", err) + } else if skippedAgents > 0 { + m.Logger.Infof(`WARNING: could not count classes from all agent jars (skipped %d), class count and metaspace may not be sized correctly %d`, skippedAgents) + } + } + } + return agentClassCount, nil +} diff --git a/helper/memory_calculator_test.go b/helper/memory_calculator_test.go index 8aba9d9..de02675 100644 --- a/helper/memory_calculator_test.go +++ b/helper/memory_calculator_test.go @@ -17,8 +17,10 @@ package helper_test import ( + "fmt" "io/ioutil" "os" + "path/filepath" "strconv" "testing" @@ -361,6 +363,55 @@ func testMemoryCalculator(t *testing.T, context spec.G, it spec.S) { }) }) + context("$JAVA_TOOL_OPTIONS with agents", func() { + it.Before(func() { + Expect(os.Setenv("JAVA_TOOL_OPTIONS", fmt.Sprintf("-javaagent:%s", filepath.Join("../count/testdata", "stub-dependency.jar")))).To(Succeed()) + }) + + it.After(func() { + Expect(os.Unsetenv("JAVA_TOOL_OPTIONS")).To(Succeed()) + }) + + it("counts classes of agent jars supplied via $JAVA_TOOL_OPTIONS", func() { + c, err := m.CountAgentClasses(os.Getenv("JAVA_TOOL_OPTIONS")) + Expect(err).NotTo(HaveOccurred()) + Expect(c).To(Equal(2)) + Expect(m.Execute()).To(Equal(map[string]string{ + "JAVA_TOOL_OPTIONS": fmt.Sprintf("-javaagent:%s -XX:MaxDirectMemorySize=10M -Xmx522705K -XX:MaxMetaspaceSize=13870K -XX:ReservedCodeCacheSize=240M -Xss1M", filepath.Join("../count/testdata", "stub-dependency.jar")), + })) + }) + + it("skips counting classes if agent jar(s) supplied via $JAVA_TOOL_OPTIONS can't be found", func() { + Expect(os.Setenv("JAVA_TOOL_OPTIONS", fmt.Sprintf("-javaagent:!abc -javaagent:%s", filepath.Join("../count/testdata", "stub-dependency.jar")))).To(Succeed()) + c, err := m.CountAgentClasses(os.Getenv("JAVA_TOOL_OPTIONS")) + Expect(err).NotTo(HaveOccurred()) + Expect(c).To(Equal(2)) + Expect(m.Execute()).To(Equal(map[string]string{ + "JAVA_TOOL_OPTIONS": fmt.Sprintf("-javaagent:!abc -javaagent:%s -XX:MaxDirectMemorySize=10M -Xmx522705K -XX:MaxMetaspaceSize=13870K -XX:ReservedCodeCacheSize=240M -Xss1M", filepath.Join("../count/testdata", "stub-dependency.jar")), + })) + }) + + it("skips counting agent classes if no agent jar(s) are supplied", func() { + Expect(os.Setenv("JAVA_TOOL_OPTIONS", "")).To(Succeed()) + c, err := m.CountAgentClasses(os.Getenv("JAVA_TOOL_OPTIONS")) + Expect(err).NotTo(HaveOccurred()) + Expect(c).To(Equal(0)) + Expect(m.Execute()).To(Equal(map[string]string{ + "JAVA_TOOL_OPTIONS": " -XX:MaxDirectMemorySize=10M -Xmx522705K -XX:MaxMetaspaceSize=13870K -XX:ReservedCodeCacheSize=240M -Xss1M", + })) + }) + + it("does not change Metaspace if it has been user configured", func() { + Expect(os.Setenv("JAVA_TOOL_OPTIONS", fmt.Sprintf("-XX:MaxMetaspaceSize=20000K -javaagent:%s", filepath.Join("../count/testdata", "stub-dependency.jar")))).To(Succeed()) + c, err := m.CountAgentClasses(os.Getenv("JAVA_TOOL_OPTIONS")) + Expect(err).NotTo(HaveOccurred()) + Expect(c).To(Equal(2)) + Expect(m.Execute()).To(Equal(map[string]string{ + "JAVA_TOOL_OPTIONS": "-XX:MaxMetaspaceSize=20000K -javaagent:../count/testdata/stub-dependency.jar -XX:MaxDirectMemorySize=10M -Xmx516576K -XX:ReservedCodeCacheSize=240M -Xss1M", + })) + }) + }) + context("user configured", func() { it.Before(func() { Expect(os.Setenv("JAVA_TOOL_OPTIONS", "-XX:MaxDirectMemorySize=10M -Xmx522705K -XX:MaxMetaspaceSize=13870K -XX:ReservedCodeCacheSize=240M -Xss1M")).To(Succeed())