Skip to content

Memory leak in jruby within openhab #9070

@ninjaltd

Description

@ninjaltd

Hi team,

I am running some very simple jruby automation scripts within openhab 4.1.3 on RPi 4 and am experiencing what appears to be some memory leaks within jruby as time goes on.

Linux hapi 5.10.103-v7l+ #1529 SMP Tue Mar 8 12:24:00 GMT 2022 armv7l GNU/Linux

jruby version is 9.4.9.0

I am not using any gems other than openhab-jrubyscripting to interface with openhab

If left unchecked, the JVM will run out of memory within a week in my RPi setup. Inspecting heap dumps, several possible leaks are identified which appear to be within jruby. Details below;

Problem Suspect 1
29,102 instances of org.jruby.RubyClass, loaded by org.openhab.automation.jrubyscripting occupy 195,511,120 (19.70%) bytes.
org.jruby.RubyClass
org.openhab.automation.jrubyscripting

Problem Suspect 2
71,150 instances of org.jruby.ir.IRMethod, loaded by org.openhab.automation.jrubyscripting occupy 148,220,520 (14.93%) bytes.
org.jruby.ir.IRMethod
org.openhab.automation.jrubyscripting

Problem Suspect 3
33 instances of org.jruby.Ruby, loaded by org.openhab.automation.jrubyscripting occupy 118,031,520 (11.89%) bytes.
org.jruby.Ruby
org.openhab.automation.jrubyscripting

I am happy to provide heap dumps privately for any jruby devs that would like to analyse.

Here is the script which I'm using:

# automation of HVAC based on astro, temp & solar forecasts
#

ITEM_DEFAULT_VALUES = {
  Hvac_Solar_Power_Threshold_Dusk    => 2000 | 'W',
  Hvac_Solar_Power_Threshold_Dawn    => 3000 | 'W',
  Hvac_Solar_Override_Trigger_Cool   => 9 | '°C',
  Hvac_Solar_Override_Trigger_Heat   => 34 | '°C',
  Hvac_Solar_Override_Normal_Cool    => 13 | '°C',
  Hvac_Solar_Override_Normal_Heat    => 28 | '°C'
}

DAWN_AVG_MINS = 10
DUSK_AVG_MINS = 10

shared_cache.compute_if_absent(:hvacstate) { OFF }
shared_cache.compute_if_absent(:hvacsolaroverride) { OFF }

def hvac_log(msg)
  logger.info "HVAC: UN(#{Airtouch_Unit_Fujitsu_Power.state}) AS(#{shared_cache[:hvacstate]}): #{msg}"
  Airtouch_Unit_Fujitsu_Automation_Message.update(msg)
end

def deploy_scenes(power_scene, setpoint_scene)
  hvac_log sprintf("deploy_scenes(#{power_scene}, #{setpoint_scene})")
  active_scenes = [power_scene, setpoint_scene]
  gAirtouchZones.members.each do |zone_group|
    zone_group.tags.sort.each do |tag|
      if tag =~ /Fujitsu_Scene_(\S+?)\[(\S+)\]\[(\S+)\]$/
        scene, tag, state = $1, $2, $3
        if active_scenes.include? scene
          zone_group.members.flatten.sort_by(&:name).tagged(tag).each do |item| 
            hvac_log sprintf("[Scene_Item_State] %-40s | %-10s | %-10s --> %s", item.name, scene, tag, state)
            item.ensure << state
          end
        end
      end
    end
  end
end

def determine_automation_scenes(mode)
  sunrise_time   = Sunrise_Time.state
  sunset_time    = Sunset_Time.state
  tnow           = Time.now

  # Determine POWER scene
  case tnow.wday
  when 5 # fri
    power_scene = "AllOn"
  when 6, 0 # sat, sun
    power_scene = "Weekend"
  else
    power_scene = "Workday"
  end
  power_scene = 'Night' if  Time.now < (sunrise_time - '1 hours') || Time.now > (sunset_time + '5 hours')

  # Determine SETPOINT scene
  case mode
  when 'HEAT'
    setpoint_scene = 'Winter'
  when 'COOL'
    setpoint_scene = 'Summer'
  end
  #case Season_Name.state
  #when 'WINTER','AUTUMN'
  #  setpoint_scene = 'Winter'
  #when 'SPRING','SUMMER'
  #  setpoint_scene = 'Summer'
  #end

  [power_scene, setpoint_scene]
end

rule "hvac_startup" do
  on_start at_level: 80
  run do
    for item, value in ITEM_DEFAULT_VALUES
      item.state = value if item.state.nil?
    end
  end
end

rule "hvac_scene_changed" do
  changed Airtouch_Unit_Fujitsu_Scene_Power
  changed Airtouch_Unit_Fujitsu_Scene_Setpoint
  run do
    deploy_scenes(Airtouch_Unit_Fujitsu_Scene_Power.state, Airtouch_Unit_Fujitsu_Scene_Setpoint.state)
  end
end

rule "hvac_event_off" do
  changed Airtouch_Unit_Fujitsu_Power, to: OFF
  run do
    shared_cache[:hvacstate] = OFF
    shared_cache[:hvacsolaroverride] = OFF
    logger.info "HVAC: was turned OFF"
  end
end

# Force the HVAC off at sunrise, assuming it was manually turned on beforehand.
rule "hvac_sunrise_force_off" do
	channel "astro:sun:home:rise#event", attach: "sunrise"   
  run do
    hvac_log "Turning unit off due to SUNRISE event"
    Airtouch_Unit_Fujitsu_Power.ensure.off
  end
end
#rule "hvac_morning_force_off" do
#  every :day, at: '07:00'
#  run do
#    hvac_log "Turning unit off due to reached 7am"
#    Airtouch_Unit_Fujitsu_Power.ensure.off
#  end
#end

rule "hvac_automation" do
  every 1.minutes
  run do |event|
    solar_remain   = ForecastSolar_9Hens_Remaining.state
    inlet_temp     = Airtouch_Unit_Fujitsu_Inlet_Temp.state
    max_temp_today = BOM_Day1_MaximumTemperature.state
    cur_temp       = BOM_Day1_AirTemperature.state
    apparent_temp  = BOM_Day1_ApparentTemperature.state

    # Use forecasts and inlet temp to determine if we should HEAT or COOL
    if max_temp_today <= 20 || apparent_temp <= 7 || (inlet_temp <= 12 | '°C')
      mode = 'HEAT'
    elsif max_temp_today >= 27 || apparent_temp >= 28 || (inlet_temp >= 28 | '°C')
      mode = 'COOL'
    else
      mode = nil
    end

    # Overrides for normal solar automation.  Use inlet temp as gauge of general comfort level
    if (inlet_temp <= Hvac_Solar_Override_Trigger_Cool.state | '°C') || (inlet_temp >= Hvac_Solar_Override_Trigger_Heat.state | '°C')
      shared_cache[:hvacsolaroverride] = ON
    end

    if Airtouch_Unit_Fujitsu_Automation_Override.state == ON
      hvac_log "Manual override active"
    else
      if Airtouch_Unit_Fujitsu_Power.state == ON
        if mode.nil?
          hvac_log "Environmentals outside of automation bounds"
          if shared_cache[:hvacstate] == ON
            hvac_log "Temperatures (cur:#{cur_temp} inlet:#{inlet_temp} max:#{max_temp_today} app:#{apparent_temp}) outside of automation bounds"
            hvac_log "turning unit OFF due to automation state being ON"
            Airtouch_Unit_Fujitsu_Power.ensure.off
          end
        else
          hvac_log "Sunset = #{Sunset_Time.state}, Season = #{Season_Name.state}, Mode = #{mode}, SolarRemain = #{solar_remain}"
          if shared_cache[:hvacstate] == ON
            if shared_cache[:hvacsolaroverride] == ON
              if ((mode == 'COOL' && inlet_temp <= Hvac_Solar_Override_Normal_Heat.state | '°C') || 
                  (mode == 'HEAT' && inlet_temp >= Hvac_Solar_Override_Normal_Cool.state | '°C'))
                hvac_log "turn unit off NOW:  SOLAR OVERRIDE IS ACTIVE AND INLET TEMP #{inlet_temp} IS NOW WITHIN COMFORTABLE RANGE"
                Airtouch_Unit_Fujitsu_Power.ensure.off
              end
            else
              sunset_avg_solar_power = Solar_Total_Gen_Power.average_since(DUSK_AVG_MINS.minutes.ago)
              if (solar_remain && (solar_remain < 3 | 'kWh')) || ((sunset_avg_solar_power < Hvac_Solar_Power_Threshold_Dusk.state | 'W') || (Time.now > Sunset_Time.state))
                hvac_log "Solar energy (#{sunset_avg_solar_power} / 10min) too low OR we're past sunset time:  TURN HVAC UNIT OFF NOW"
                Airtouch_Unit_Fujitsu_Power.ensure.off
              end
            end
          end
        end
      else
        if mode.nil?
          hvac_log "Temperatures (cur:#{cur_temp} inlet:#{inlet_temp} max:#{max_temp_today} app:#{apparent_temp}) outside of automation bounds"
        elsif shared_cache[:hvacsolaroverride] == ON || Time.now > Sunrise_Time.state
          sunrise_avg_solar_power = Solar_Total_Gen_Power.average_since(DAWN_AVG_MINS.minutes.ago)
          hvac_log "Sunrise = #{Sunrise_Time.state}, Season = #{Season_Name.state}, Mode = #{mode}, SolarGen(30mAVG) = #{sunrise_avg_solar_power}, SolarRemain = #{solar_remain}, Temp = cur:#{cur_temp} inlet:#{inlet_temp} max:#{max_temp_today} app:#{apparent_temp}"
          if shared_cache[:hvacsolaroverride] == ON || (sunrise_avg_solar_power > Hvac_Solar_Power_Threshold_Dawn.state | 'W') || ((sunrise_avg_solar_power > Hvac_Solar_Power_Threshold_Dawn.state | 'W') && solar_remain && (solar_remain > 20 | 'kWh'))
            if shared_cache[:hvacsolaroverride] == ON
              hvac_log "turn unit on NOW:  SOLAR OVERRIDE IS ACTIVE BECAUSE INLET TEMP #{inlet_temp} IS OUT OF COMFORTABLE RANGE !!!"
            else
              hvac_log "turn unit on NOW:  Average solar generation / solar forecast / temp forecast exceeded thresholds"
            end
            power_scene, setpoint_scene = determine_automation_scenes(mode)
            Airtouch_Unit_Fujitsu_Scene_Power.update(power_scene)
            Airtouch_Unit_Fujitsu_Scene_Setpoint.update(setpoint_scene)
            Airtouch_Unit_Fujitsu_Mode.ensure.command(mode)
            Airtouch_Unit_Fujitsu_Power.ensure.on
            hvac_log "Starting timer to set automation state" 
            sleep 10
            shared_cache[:hvacstate] = ON
            hvac_log "Mode is #{Airtouch_Unit_Fujitsu_Mode.state}, :hvacstate is #{shared_cache[:hvacstate]}, hvacsolaroverride is #{shared_cache[:hvacsolaroverride]}"
          end
        end
      end
    end
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions