Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion RUBY_VS_GO_BUILDPACK_COMPARISON.md
Original file line number Diff line number Diff line change
Expand Up @@ -2191,12 +2191,40 @@ dependencies:
| **APM Agents** | ✅ 15 agents | ✅ 14 agents | Missing: Google Stackdriver Debugger (deprecated) |
| **Security Providers** | ✅ 6 | ✅ 6 | Identical |
| **Database JDBC Injection** | ✅ | ✅ | Identical |
| **Memory Calculator** | ✅ | ✅ | Identical |
| **Memory Calculator** | ✅ v3.13.0 | ✅ v4.2.0 | **Behaviour change** — see below |
| **JVMKill Agent** | ✅ | ✅ | Identical |
| **Custom JRE Repositories** | ✅ Runtime config | ❌ Requires fork | Breaking change |
| **Multi-buildpack** | ⚠️ Via framework | ✅ Native V3 | Go improvement |
| **Configuration Overrides** | ✅ | ✅ | Identical (JBP_CONFIG_*) |

#### Memory Calculator Behaviour Change (v3 → v4)

The memory calculator was upgraded from **v3.13.0 to v4.2.0**. The difference only affects apps with an **explicit `-Xmx`** in `JAVA_OPTS` (setting `-Xmx` explicitly in containerised environments is generally considered bad practice — the calculator sizes heap better automatically). How a pinned `-Xmx` is handled:

| | v3.13.0 (Ruby) | v4.2.0 (Go) |
|--|----------------|-------------|
| Memory check | `non-heap > total` | `non-heap + heap > total` |
| Non-heap when `-Xmx` set | Squeezed to `total − Xmx` | Calculated independently |
| `-Xmx512M`, container=750M | ✅ passes | ❌ fails |

When `-Xmx` is not set, both v3 and v4 size heap and non-heap to fit within the container — no difference. When `-Xmx` is pinned, v4 requires the container to fit both heap and non-heap (thread stacks + metaspace + code cache). v3 squeezed non-heap into whatever remained after `-Xmx`, claiming less total memory — at the cost of potentially undersized thread stacks and metaspace at runtime. Apps that fit in smaller containers with v3 may fail at startup with v4:

```
required memory 1269289K is greater than 750M available for allocation
```

**Migration options**:

1. **Lower `stack_threads`** *(only if your app uses fewer than 250 threads)*: 250 threads × ~1M = ~250M native memory. Reducing this alone is often enough to fit within the container:
```yaml
env:
JBP_CONFIG_OPEN_JDK_JRE: '{ memory_calculator: { stack_threads: 50 } }'
```

2. **Remove `-Xmx` from `JAVA_OPTS`** — let the calculator size heap automatically. Note: removing `-Xmx` avoids the fixed-heap check but does not reduce total memory need. You will likely still need to increase `memory:` in `manifest.yml` so the calculator has enough room to allocate adequate heap.

3. **Increase manifest memory** — raise `memory:` to fit `Xmx + non-heap`. Based on the error above (`1269289K ≈ 1240M`), set at least **1300M** for a `-Xmx512M` app with default settings.

### 10.3 Adoption Recommendations

**✅ RECOMMENDED for**:
Expand Down
109 changes: 94 additions & 15 deletions src/java/jres/memory_calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ type MemoryCalculator struct {
javaMajorVersion int
calculatorPath string
version string
classCount int
stackThreads int
headroom int
classCount int
stackThreads int
headroom int
configLoaded bool
classCountUserSet bool
stackThreadsUserSet bool
headroomUserSet bool
}

// NewMemoryCalculator creates a new memory calculator
Expand Down Expand Up @@ -54,6 +58,8 @@ func (m *MemoryCalculator) Supply() error {
m.version = dep.Version
m.ctx.Log.Info("Installing Memory Calculator (%s)", m.version)

m.LoadConfig()

// Create bin directory
binDir := filepath.Join(m.jreDir, "bin")
if err := os.MkdirAll(binDir, 0755); err != nil {
Expand Down Expand Up @@ -107,14 +113,15 @@ func (m *MemoryCalculator) Supply() error {

m.calculatorPath = finalPath

// Count classes in the application
if err := m.countClasses(); err != nil {
m.ctx.Log.Warning("Failed to count classes: %s (using default)", err.Error())
m.classCount = 0 // Will be calculated as 35% of actual later
// Count classes in the application, unless overridden by config
if m.classCount == 0 {
if err := m.countClasses(); err != nil {
m.ctx.Log.Warning("Failed to count classes: %s (using default)", err.Error())
}
}

m.ctx.Log.Info("Memory Calculator installed: Loaded Classes: %d, Threads: %d",
m.classCount, m.stackThreads)
m.ctx.Log.Info("Memory Calculator installed: Loaded Classes: %s, Threads: %s, Headroom: %s",
m.classCountDisplay(), m.stackThreadsDisplay(), m.headroomDisplay())

// Clean up temp directory
os.RemoveAll(tempDir)
Expand Down Expand Up @@ -152,6 +159,8 @@ func (m *MemoryCalculator) detectInstalledCalculator() {

// Finalize configures the memory calculator in the startup command
func (m *MemoryCalculator) Finalize() error {
m.LoadConfig()

// If calculatorPath not set, try to detect it from previous installation
if m.calculatorPath == "" {
m.detectInstalledCalculator()
Expand Down Expand Up @@ -364,24 +373,73 @@ func (m *MemoryCalculator) convertToRuntimePath(stagingPath string) string {
return fmt.Sprintf("/home/vcap/deps/%s/jre/bin/%s", depsIdx, filename)
}

// LoadConfig loads memory calculator configuration from environment/config
// openJDKJREConfig mirrors the memory_calculator section of JBP_CONFIG_OPEN_JDK_JRE.
type openJDKJREConfig struct {
MemoryCalculator memoryCalculatorConfig `yaml:"memory_calculator"`
}

type memoryCalculatorConfig struct {
StackThreads int `yaml:"stack_threads"`
ClassCount int `yaml:"class_count"`
Headroom int `yaml:"headroom"`
}

// LoadConfig loads memory calculator configuration from JBP_CONFIG_OPEN_JDK_JRE
// (standard CF format) and falls back to MEMORY_CALCULATOR_* env vars.
// Must be called at the start of Supply(), before countClasses().
func (m *MemoryCalculator) LoadConfig() {
// Check for environment overrides
// JBP_CONFIG_OPEN_JDK_JRE='{memory_calculator: {stack_threads: 300}}'
if m.configLoaded {
return
}
m.configLoaded = true
if config := os.Getenv("JBP_CONFIG_OPEN_JDK_JRE"); config != "" {
yamlHandler := common.YamlHandler{}

// Extract raw memory_calculator sub-section to validate its fields separately,
// so unknown top-level keys (e.g. jre:) are silently ignored while typos
// inside memory_calculator: are warned about.
rawCfg := struct {
MC interface{} `yaml:"memory_calculator"`
}{}
if err := yamlHandler.Unmarshal([]byte(config), &rawCfg); err == nil && rawCfg.MC != nil {
if mcBytes, err := yamlHandler.Marshal(rawCfg.MC); err == nil {
if err := yamlHandler.ValidateFields(mcBytes, &memoryCalculatorConfig{}); err != nil {
m.ctx.Log.Warning("Unknown fields in JBP_CONFIG_OPEN_JDK_JRE memory_calculator: %s", err.Error())
}
}
}

// For now, using defaults
// In production, we'd parse JSON from environment variables
cfg := openJDKJREConfig{}
if err := yamlHandler.Unmarshal([]byte(config), &cfg); err != nil {
m.ctx.Log.Warning("Failed to parse JBP_CONFIG_OPEN_JDK_JRE: %s", err.Error())
} else {
mc := cfg.MemoryCalculator
if mc.StackThreads > 0 {
m.stackThreads = mc.StackThreads
m.stackThreadsUserSet = true
}
if mc.ClassCount > 0 {
m.classCount = mc.ClassCount
m.classCountUserSet = true
}
if mc.Headroom > 0 {
m.headroom = mc.Headroom
m.headroomUserSet = true
}
}
}

// Check specific environment variables
if val := os.Getenv("MEMORY_CALCULATOR_STACK_THREADS"); val != "" {
if threads, err := strconv.Atoi(val); err == nil {
m.stackThreads = threads
m.stackThreadsUserSet = true
}
}

if val := os.Getenv("MEMORY_CALCULATOR_HEADROOM"); val != "" {
if headroom, err := strconv.Atoi(val); err == nil {
m.headroom = headroom
m.headroomUserSet = true
}
}
}
Expand All @@ -395,6 +453,27 @@ func copyFile(src, dst string) error {
return os.WriteFile(dst, data, 0755)
}

func (m *MemoryCalculator) classCountDisplay() string {
if m.classCountUserSet {
return fmt.Sprintf("%d", m.classCount)
}
return fmt.Sprintf("%d (auto-detected)", m.classCount)
}

func (m *MemoryCalculator) stackThreadsDisplay() string {
if m.stackThreadsUserSet {
return fmt.Sprintf("%d", m.stackThreads)
}
return fmt.Sprintf("%d (default)", m.stackThreads)
}

func (m *MemoryCalculator) headroomDisplay() string {
if m.headroomUserSet {
return fmt.Sprintf("%d%%", m.headroom)
}
return fmt.Sprintf("%d%% (default)", m.headroom)
}

// RunMemoryCalculator runs the memory calculator and returns the calculated JAVA_OPTS
// This is primarily for testing
func (m *MemoryCalculator) RunMemoryCalculator(memoryLimit string) (string, error) {
Expand Down
Loading