#!/usr/bin/env lua local posix = require 'posix' local invoker = require 'invoker' -- the readlink from posix does not work well posix.readlink = invoker.readlink -- private maps save directory local maps_dir = '/root/memory-leak' local function save_map_file(pid, appinfo, now) local appn = appinfo["appn"] local mapf = string.format('/proc/%d/maps', pid) local mapd = invoker.readfile(mapf, 16384) if not mapd then return false end if mapd == appinfo["lastmap"] then return false end local filn = string.format('%s/%s/%s-%d-%s.maps', maps_dir, appn, appn, pid, os.date('%Y%m%d-%H%M%S', now)) mapf = io.open(filn, "wb") if mapf then mapf:write(mapd) mapf:close() mapf = nil appinfo["lastmap"] = mapd return true end return false end local function dump_map_file(pid, appi, now) local maps = 0 local filp = string.format('/proc/%d/status', pid) local mapf = invoker.readfile(filp) if not mapf or #mapf == 0 then io.stderr:write("Error, failed to open [" .. filp .. "]\n") return maps end local hwm, vmrss = nil, nil local vm_rss = string.match(mapf, 'VmRSS:%s+(%d+)%s+kB') if vm_rss then vmrss = tonumber(vm_rss) * 1024 end local vm_hwm = string.match(mapf, 'VmHWM:%s+(%d+)%s+kB') if vm_hwm then hwm = tonumber(vm_hwm) * 1024 end mapf = nil if vmrss then maps = vmrss end if not appi then return maps end if pid ~= appi.pid then print(string.format('[%s, %d] PID has changed for [%s]: %d => %d', os.date(nil, now), now, appi.appn, appi.pid, pid)) appi.lastmap = "" appi.pid = pid appi.vmhwm = hwm appi.vmrss = vmrss save_map_file(pid, appi, now) return maps end if hwm and appi.vmhwm < hwm then if appi.vmhwm > 0 then print(string.format('[%s, %d] VmHWM changed for [%s]: 0x%x => 0x%x, delta: %d', os.date(nil, now), now, appi.appn, appi.vmhwm, hwm, hwm - appi.vmhwm)) io.stdout:flush() end appi.vmhwm = hwm end if vmrss and appi.vmrss < vmrss then if appi.vmrss > 0 then save_map_file(pid, appi, now) print(string.format('[%s, %d] VmRSS changed for [%s]: 0x%x => 0x%x, delta: %d', os.date(nil, now), now, appi.appn, appi.vmrss, vmrss, vmrss - appi.vmrss)) io.stdout:flush() end appi.vmrss = vmrss end return maps end local function iterate_process(plst, now) local other = 0 local plist = posix.glob('/proc/*/exe') for _, pexe in ipairs(plist) do local pid = string.match(pexe, '^/proc/(%d+)/exe') if pid then pid = tonumber(pid) local rexe = posix.readlink(pexe) if rexe then rexe = posix.basename(rexe) end if rexe then local appi = plst[rexe] if appi then dump_map_file(pid, appi, now) else other = other + dump_map_file(pid, nil, now) end end end end return other end local function memory_list(mlist) local mlst = {} for _, app in ipairs(mlist) do mlst[app] = { pid = 0, vmrss = 0, vmhwm = 0, appn = app, lastmap = "" } os.execute(string.format('exec mkdir -p %s/%s', maps_dir, app)) end return mlst end local function dump_process(plist, now) local sepstr = '---------------------------------------------' print(sepstr) print(string.format('[%s, %d] Application memories:', os.date(nil, now), now)) for keyp, appi in pairs(plist) do print(string.format('\t%s => \n\t\tVmRSS: 0x%x, VmHWM: 0x%x', keyp, appi.vmrss, appi.vmhwm)) end print(sepstr) io.stdout:flush() return true end local function dump_meminfo(now, other) local memi = io.open('/proc/meminfo', 'rb') if not memi then return false end local free, avail, bufsize, cached = nil, nil, nil, nil for line in memi:lines() do if not free then local size_free = string.match(line, '^MemFree:%s+(%d+)%s+kB') if size_free then free = size_free end end if not avail then local size_avail = string.match(line, '^MemAvailable:%s+(%d+)%s+kB') if size_avail then avail = size_avail end end if not bufsize then local size_buf = string.match(line, '^Buffers:%s+(%d+)%s+kB') if size_buf then bufsize = size_buf end end if not cached then local size_cached = string.match(line, '^Cached:%s+(%d+)%s+kB') if size_cached then cached = size_cached end end end memi:close(); memi = nil print(string.format('[%s, %d] MEMINFO => Free %s kB, Available: %s kB, Buffers: %s kB, Cached: %s kB, others: %d kB', os.date(nil, now), now, free or "0", avail or "0", bufsize or "0", cached or "0", math.floor(other / 1024))) io.stdout:flush() return true end local mlst = { 'dy_protocol_parser', 'cloud_conn_dayun', 'cloud_conn_nandu', 'cloud_mqtt', 'dy_common_socket', 'dy_config_manager', 'dy_data_reporter', 'dy_device_monitor', 'dy_modbus_tcp', 'dy_rs485', 'dy_rule_eng', 'dy_upgrade', 'dy_virtual_dev', 'python2.7', 'bmser_drs', 'HMI_BMS', 'ems_ctrl', 'ess_bms', 'grid_meter', 'load_meter', 'pcs_dev_virtual', 'frpc', } local function write_drop_caches(wval) local dropc = "/proc/sys/vm/drop_caches" local filp = io.open(dropc, "wb") if not filp then return false end local typn = type(wval) if typn == "string" then filp:write(wval) elseif typn == "table" then for _, w_val in ipairs(wval) do filp:write(w_val) filp:close(); filp = nil filp = io.open(dropc, "wb") if not filp then break end end end if filp then filp:close() filp = nil end return true end local count = 0 mlst = memory_list(mlst) while true do -- posix.nanosleep({tv_sec = 0, tv_nsec = 990000000}) invoker.waitsec(4) local now = os.time() local other_size = iterate_process(mlst, now) count = count + 1 dump_meminfo(now, other_size) if (count % 60) == 0 then collectgarbage() end if (count % 2) == 0 then write_drop_caches("\3n") end if count >= 300 then dump_process(mlst, now) count = 0 end end