#!/usr/bin/luajit
local lines_tailed=100000
local filename="errors.log"
local follow=false

local write=io.write

local shm, size_by_label, count_by_label, weird_log
function init()
  print "initialize"
  shm, size_by_label, count_by_label, weird_log={}, {}, {}, {}
  memmap={}
end

function err(str, ...)
  str = str or "Unknown error occurred"
  io.stderr:write(str:format(...))
  io.stderr:write("\n")
end
function printf(str, ...)
  print(str:format(...))
end


local poolsize=0
local poolstart=0
local memmap={}
local membuckets=1024
local bucketsize=0

local resolve_weird_log; do
  local weirdlog={}
  resolve_weird_log=function(line, is_weird)
    local prev = weirdlog[1]
    if prev and prev.ptr == line.ptr and prev.t == line.t and prev.pid ~= line.pid then
      if prev.action == "free"  and line.action == "alloc" then
        --oh, a free happened in a different worker and at the same time?
        --the log was probably written out-of-sequence
        table.remove(weirdlog, 1)
        alloc_raw(line.ptr, line.size, line.lbl, line.t, line.pid)
        free_raw(prev.ptr, prev.t, prev.pid)
        printf("resolved weird free/alloc at %s", line.ptr)
        return false
      elseif prev.action == "alloc" and line.action == "free" then
        --oh, an alloc happened in a different worker and at the same time?
        --the log was probably written out-of-sequence
        table.remove(weirdlog, 1)
        free_raw(line.ptr, line.t, line.pid)
        alloc_raw(prev.ptr, prev.size, prev.lbl, prev.t, prev.pid)
        printf("resolved weird alloc/free at %s", line.ptr)
        return false
      end
    end
    if is_weird then
      table.insert(weirdlog, 1, line)
      return false
    else
      return true
    end
  end
end

local memmap_add
do
  local bucketsize=0
  memmap_add= function(starthex, size, val)
  if not poolsize or not poolstart then return err("poolsize or poolstart not known") end  
  val = val or 1
  local start=tonumber(starthex, 16)
  if not start then return err("starthex was not a hex number") end
  --start should be relative to pool start
  start=start-poolstart
  
  if not size then
    if shm[starthex] then
      size=shm[starthex].size
    else
      err("shm[%s] is nil", starthex)
    end
  end
  
  local bstart = math.floor(start/poolsize * membuckets)
  local bend =   math.floor((start+size)/poolsize * membuckets)
  
  for i=bstart, bend do
    memmap[i]=(memmap[i] or 0)+val
    if memmap[i]<0 then
      err("negative memmap at bucket %s", i)
      memmap[i]=0
    end
  end
end
end

function alloc(ptr, size, label, time, pid)
  size=tonumber(size)
  local looks_weird=false
  if shm[ptr] ~= nil then
    err("BAD ALLOC AT ptr %s pid %i time %s", ptr, pid, time)
    looks_weird = true
  end
  if resolve_weird_log({action="alloc", ptr=ptr, size=size, lbl=label, t=time, pid=pid}, looks_weird) then
    alloc_raw(ptr, size, label, time, pid)
  end
end
function alloc_raw(ptr, size, label, time, pid)
  shm[ptr]={size=size, label=label, time=time}
  size_by_label[label]=(size_by_label[label] or 0) + size
  count_by_label[label]=(count_by_label[label] or 0) + 1
  memmap_add(ptr, size)
end

function free(ptr, time, pid)
  local looks_weird=false
  local ref=shm[ptr]
  if ref == nil then
    err ("DOUBLE FREE AT ptr %s pid %i time %s", ptr, pid, time)
    looks_weird = true
  end
  if resolve_weird_log({action="free", ptr=ptr, size=nil, lbl=nil, t=time, pid=pid}, looks_weird) then
    free_raw(ptr, time, pid)
  end
end
function free_raw(ptr, time, pid)
  local ref=shm[ptr]
  if ref==nil then
    err("executed double free on ptr %s pid %i time %s", ptr, pid, time)
    return
  end
  memmap_add(ptr, nil, -1)
  size_by_label[ref.label]=size_by_label[ref.label]-ref.size
  count_by_label[ref.label]=(count_by_label[ref.label] or 0) - 1
  shm[ptr]=nil
end

function formatsize(bytes)
  if bytes<1024 then
    return string.format("%ib", bytes)
  elseif bytes > 1024 and bytes < 1048576 then
    return string.format("%.3gKb", bytes/1024)
  else
    return string.format("%.3gMb", bytes/1048576)
  end
end

local alphabet={1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"}
function summary()
  local compare=function(a,b)
    return a[2] > b[2]
  end
  local total=0;
  local resort={}
  for k,v in pairs(size_by_label) do table.insert(resort, {k, v}) end
  table.sort(resort, compare)
  for k,v in ipairs(resort) do
    printf("%-40s %-10s %i", v[1], formatsize(v[2]), count_by_label[v[1]])
    total = total + v[2]
  end
  print ("                                       --------      ")
  printf("%-40s %s", "total", formatsize(total))
  
  --memory map
  local n
  for i=0,membuckets do
    n=memmap[i]
    if n==0 or n == nil then
      write("-")
    elseif n<#alphabet then
      write(alphabet[n] or "?")
    else
      write("#")
    end
  end
  print ""
end


function parse_line(str)
  local out_of_memory
  local time,errlevel, pid, msg=str:match("(%d+/%d+/%d+%s+%d+:%d+:%d+)%s+%[(%w+)%]%s+(%d+)#%d+:%s+(.+)")
  if msg ~= nil then
    local ptr, size, label, start

    if errlevel == "crit" then
      print("CRITICAL:", time, pid, msg)
      if msg == "ngx_slab_alloc() failed: no memory" then
        return true
      end
    end
    ptr, size, label = msg:match("shpool alloc addr (%w+) size (%d+) label (.+)")
    if ptr then
      alloc(ptr, size, label, time, pid)
      return ptr
    end

    ptr = msg:match("shpool free addr (%w+)")
    if ptr then
      free(ptr, time, pid)
      return ptr
    end
    
    start, size = msg:match("ngx_http_push_shpool start (%w+) size (%d+)")
    if size and start then
      poolsize=tonumber(size)
      poolstart = tonumber(start, 16)
      printf("shm start %s size %s", start, formatsize(poolsize))
    end
  end
end



----------------------------------------------------------------
--NO FUNCTIONS DEFINED BELOW THIS LINE PLEASE (except lambdas)--

--handle arguments
if arg[1] == "tail" or arg[1] == "follow" then
  follow=true
elseif arg[1] ~= nil then
  filename=arg[1]
end


init()


if follow then
  local lasttime, now=os.time(), nil
  print "follow errors.log"
  local tailin = io.popen(string.format('tail --lines=%s -F %s 2>&1', lines_tailed, filename), 'r')
  for line in tailin:lines() do
    now=os.time()
    if line:match('truncated') or
       line:match(("‘%s’ has become inaccessible: No such file or directory"):format(filename)) then
      --reset
      init()
    elseif parse_line(line) == true then
      summary()
      lasttime=now
    elseif now - lasttime > 1 then
      summary()
      lasttime=now
    end
  end
else
  printf("open %s", filename)
  local f = io.open(filename, "r")
  for line in f:lines() do
    if parse_line(line) == true then
      summary()
    end
  end
  f:close()
end


summary()
