All signs so far seem to be that CPU is cheap for this application, but memory is expensive. It's easy to get sluggish if the GC comes on.
After some experiments using https://github.com/yaukeywang/LuaMemorySnapshotDump, one source of memory leaks is rendered fragments (https://love2d.org/wiki/Text objects). I need to render text in approximately word-sized fragments to mostly break lines more intelligently at word boundaries.
I've attached the files I used for my experiments (suffixed with a '.')
There's definitely still a leak in fragments. The longer I edit, the more memory goes to them.
OGUV4HSA7XGSQLUVWBAE3AE263Z7Z6G3BZOB4CN2AOYD2DEJMOZAC
LS55YKGWKICTQTAHR5KLMNDOL6CDI4ATT3NT5Z2YL5IM3CRQOONQC
NP7PIUBTR4K6SWJS46YZG3H2RYYNRGNEJMPV4I24TQXT5O3YT27QC
HMODUNJEQLZ3W46GKYIDL55F6COVXHTIC6UW4AK3SXOOKOPE6NNAC
XNFTJHC4QSHNSIWNN7K6QZEZ37GTQYKHS4EPNSVPQCUSWREROGIQC
BULPIBEGL7TMK6CVIE7IS7WGAHGOSUJBGJSFQK542MOWGHP2ADQQC
KOYAJWE4NJ2J4X3SHEAVMRXYZPZGOMTI7OX3PTUQIDIZ2GQI6UKAC
3OKKTUT4Q7W44JHILOFV5BVUA7ZOBIHBCEXGZ65CPXV4PRLI2W4QC
242L3OQXTU2TCAINRJXQEEDSXQXM7Y7USUPBK37ZNM3A7V5TUDSAC
HOSPP2ANSW654DYRTC6CQUQA2GUKV6T2FI7QBKXD2DZS3R32IMGAC
R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC
Z4XRNDTRTGSZHNB65WNHOVUBFW4QWQABLVSK4RM3QJHGK33DMRJAC
OTIBCAUJ3KDQJLVDN3A536DLZGNRYMGJLORZVR3WLCGXGO6UGO6AC
NQWWTGXRLSBASOSP75FPOSVYP664VYRFQH7MY5LALLIP2VEBQMCQC
local utf8 = require 'utf8'
require 'app'
require 'test'
require 'keychord'
require 'file'
require 'button'
local Text = require 'text'
local Drawing = require 'drawing'
local geom = require 'geom'
require 'help'
require 'icons'
local mri = require 'MemoryReferenceInfo'
-- run in both tests and a real run
function App.initialize_globals()
-- a line is either text or a drawing
-- a text is a table with:
-- mode = 'text',
-- string data,
-- a (y) coord in pixels (updated while painting screen),
-- some cached data that's blown away and recomputed when data changes:
-- fragments: snippets of rendered love.graphics.Text, guaranteed to not wrap
-- screen_line_starting_pos: optional array of grapheme indices if it wraps over more than one screen line
-- a drawing is a table with:
-- mode = 'drawing'
-- a (y) coord in pixels (updated while painting screen),
-- a (h)eight,
-- an array of points, and
-- an array of shapes
-- a shape is a table containing:
-- a mode
-- an array points for mode 'freehand' (raw x,y coords; freehand drawings don't pollute the points array of a drawing)
-- an array vertices for mode 'polygon', 'rectangle', 'square'
-- p1, p2 for mode 'line'
-- p1, p2, arrow-mode for mode 'arrow-line'
-- center, radius for mode 'circle'
-- center, radius, start_angle, end_angle for mode 'arc'
-- Unless otherwise specified, coord fields are normalized; a drawing is always 256 units wide
-- The field names are carefully chosen so that switching modes in midstream
-- remembers previously entered points where that makes sense.
Lines = {{mode='text', data=''}}
-- Lines can be too long to fit on screen, in which case they _wrap_ into
-- multiple _screen lines_.
--
-- Therefore, any potential location for the cursor can be described in two ways:
-- * schema 1: As a combination of line index and position within a line (in utf8 codepoint units)
-- * schema 2: As a combination of line index, screen line index within the line, and a position within the screen line.
--
-- Most of the time we'll only persist positions in schema 1, translating to
-- schema 2 when that's convenient.
Screen_top1 = {line=1, pos=1} -- position of start of screen line at top of screen
Cursor1 = {line=1, pos=1} -- position of cursor
Screen_bottom1 = {line=1, pos=1} -- position of start of screen line at bottom of screen
Selection1 = {}
Old_cursor1, Old_selection1, Mousepress_shift = nil -- some extra state to compute selection between mousepress and mouserelease
Recent_mouse = {} -- when selecting text, avoid recomputing some state on every single frame
Cursor_x, Cursor_y = 0, 0 -- in pixels
Current_drawing_mode = 'line'
Previous_drawing_mode = nil
-- values for tests
Font_height = 14
Line_height = 15
Margin_top = 15
Filename = love.filesystem.getUserDirectory()..'/lines.txt'
-- undo
History = {}
Next_history = 1
-- search
Search_term = nil
Search_text = nil
Search_backup = nil -- stuff to restore when cancelling search
-- resize
Last_resize_time = nil
-- blinking cursor
Cursor_time = 0
Initialize_done = false
Before_done = false
mri.m_cConfig.m_bAllMemoryRefFileAddTime = false
end -- App.initialize_globals
function App.initialize(arg)
love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
love.keyboard.setKeyRepeat(true)
if arg[1] == '-geometry' then
initialize_window_geometry(arg[2])
table.remove(arg, 2)
table.remove(arg, 1)
else
initialize_window_geometry()
end
initialize_font_settings(20)
if #arg > 0 then
Filename = arg[1]
end
print('init', collectgarbage('count'))
Lines = load_from_disk(Filename)
print('load_from_disk', collectgarbage('count'))
for i,line in ipairs(Lines) do
if line.mode == 'text' then
Cursor1.line = i
break
end
end
love.window.setTitle('lines.love - '..Filename)
if #arg > 1 then
print('ignoring commandline args after '..arg[1])
end
Initialize_done = true
--? if rawget(_G, 'jit') then
--? jit.off()
--? jit.flush()
--? end
end -- App.initialize
function initialize_window_geometry(geometry_spec)
local geometry_initialized
if geometry_spec then
geometry_initialized = parse_geometry_spec(geometry_spec)
end
if not geometry_initialized then
-- maximize window
love.window.setMode(0, 0) -- maximize
App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
-- shrink slightly to account for window decoration
App.screen.width = App.screen.width-100
App.screen.height = App.screen.height-100
end
App.screen.flags.resizable = true
App.screen.flags.minwidth = math.min(App.screen.width, 200)
App.screen.flags.minheight = math.min(App.screen.width, 200)
love.window.updateMode(App.screen.width, App.screen.height, App.screen.flags)
end
function parse_geometry_spec(geometry_spec)
local width, height, x, y = geometry_spec:match('(%d+)x(%d+)%+(%d+)%+(%d+)')
if width == nil then
print('invalid geometry spec: '..geometry_spec)
print('expected format: {width}x{height}+{x}+{y}')
return false
end
App.screen.width = math.floor(tonumber(width))
App.screen.height = math.floor(tonumber(height))
App.screen.flags = {x=math.floor(tonumber(x)), y=math.floor(tonumber(y))}
return true
end
function love.resize(w, h)
--? print(("Window resized to width: %d and height: %d."):format(w, h))
App.screen.width, App.screen.height = w, h
Line_width = math.min(40*App.width(Em), App.screen.width-50)
Text.redraw_all()
Last_resize_time = love.timer.getTime()
end
function initialize_font_settings(font_height)
Font_height = font_height
love.graphics.setFont(love.graphics.newFont(Font_height))
Line_height = math.floor(font_height*1.3)
-- maximum width available to either text or drawings, in pixels
Em = App.newText(love.graphics.getFont(), 'm')
-- readable text width is 50-75 chars
Line_width = math.min(40*App.width(Em), App.screen.width-50)
end
function App.filedropped(file)
App.initialize_globals() -- in particular, forget all undo history
Filename = file:getFilename()
file:open('r')
Lines = load_from_file(file)
file:close()
for i,line in ipairs(Lines) do
if line.mode == 'text' then
Cursor1.line = i
break
end
end
love.window.setTitle('Text with Lines - '..Filename)
end
frame_index = 0
function App.draw()
frame_index = frame_index+1
if frame_index % 10 == 0 then
print(frame_index)
end
Button_handlers = {}
love.graphics.setColor(1, 1, 1)
love.graphics.rectangle('fill', 0, 0, App.screen.width-1, App.screen.height-1)
love.graphics.setColor(0, 0, 0)
-- some hysteresis while resizing
if Last_resize_time then
if love.timer.getTime() - Last_resize_time < 0.1 then
return
else
Last_resize_time = nil
end
end
assert(Text.le1(Screen_top1, Cursor1))
local y = Margin_top
--? print('== draw')
for line_index,line in ipairs(Lines) do
--? print('draw:', y, line_index, line)
if y + Line_height > App.screen.height then break end
--? print('a')
if line_index >= Screen_top1.line then
Screen_bottom1.line = line_index
if line.mode == 'text' and line.data == '' then
line.y = y
button('draw', {x=4,y=y+4, w=12,h=12, color={1,1,0},
icon = icon.insert_drawing,
onpress1 = function()
Drawing.before = snapshot()
table.insert(Lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}})
if Cursor1.line >= line_index then
Cursor1.line = Cursor1.line+1
end
end})
if Search_term == nil then
if line_index == Cursor1.line then
Text.draw_cursor(25, y)
end
end
Screen_bottom1.pos = Screen_top1.pos
y = y + Line_height
elseif line.mode == 'drawing' then
y = y+10 -- padding
line.y = y
Drawing.draw(line)
y = y + Drawing.pixels(line.h) + 10 -- padding
else
--? print('text')
line.y = y
y, Screen_bottom1.pos = Text.draw(line, Line_width, line_index)
y = y + Line_height
--? print('=> y', y)
end
end
end
--? print('screen bottom: '..tostring(Screen_bottom1.pos)..' in '..tostring(Lines[Screen_bottom1.line].data))
if Search_term then
Text.draw_search_bar()
end
if Initialize_done then
if not Before_done then
Before_done = true
print('before', collectgarbage('count'))
collectgarbage('collect')
mri.m_cMethods.DumpMemorySnapshot('./', '0', -1)
frame_index = 0
elseif frame_index == 1000 then
print('after', collectgarbage('count'))
collectgarbage('collect')
mri.m_cMethods.DumpMemorySnapshot('./', '1', -1)
mri.m_cMethods.DumpMemorySnapshotComparedFile("./", "Compared", -1,
"./LuaMemRefInfo-All-[0].txt",
"./LuaMemRefInfo-All-[1].txt")
os.exit(1)
end
end
end
function App.update(dt)
Cursor_time = Cursor_time + dt
-- some hysteresis while resizing
if Last_resize_time then
if love.timer.getTime() - Last_resize_time < 0.1 then
return
else
Last_resize_time = nil
end
end
Drawing.update(dt)
end
function App.mousepressed(x,y, mouse_button)
if Search_term then return end
propagate_to_button_handlers(x,y, mouse_button)
for line_index,line in ipairs(Lines) do
if line.mode == 'text' then
if Text.in_line(line, x,y) then
-- delicate dance between cursor, selection and old cursor
-- manual tests:
-- regular press+release: sets cursor, clears selection
-- shift press+release:
-- sets selection to old cursor if not set otherwise leaves it untouched
-- sets cursor
-- press and hold to start a selection: sets selection on press, cursor on release
-- press and hold, then press shift: ignore shift
-- i.e. mousereleased should never look at shift state
Old_cursor1 = Cursor1
Old_selection1 = Selection1
Mousepress_shift = App.shift_down()
Selection1 = {line=line_index, pos=Text.to_pos_on_line(line, x, y)}
end
elseif line.mode == 'drawing' then
if Drawing.in_drawing(line, x, y) then
Drawing.mouse_pressed(line, x,y, button)
end
end
end
end
function App.mousereleased(x,y, button)
if Search_term then return end
if Lines.current_drawing then
Drawing.mouse_released(x,y, button)
else
for line_index,line in ipairs(Lines) do
if line.mode == 'text' then
if Text.in_line(line, x,y) then
Cursor1 = {line=line_index, pos=Text.to_pos_on_line(line, x, y)}
if Mousepress_shift then
if Old_selection1.line == nil then
Selection1 = Old_cursor1
else
Selection1 = Old_selection1
end
end
Old_cursor1, Old_selection1, Mousepress_shift = nil
end
end
end
--? print('select:', Selection1.line, Selection1.pos)
end
end
function App.textinput(t)
for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
if Search_term then
Search_term = Search_term..t
Search_text = nil
Text.search_next()
elseif Current_drawing_mode == 'name' then
local drawing = Lines.current_drawing
local p = drawing.points[drawing.pending.target_point]
p.name = p.name..t
else
Text.textinput(t)
end
save_to_disk(Lines, Filename)
end
function App.keychord_pressed(chord)
if Search_term then
if chord == 'escape' then
Search_term = nil
Search_text = nil
Cursor1 = Search_backup.cursor
Screen_top1 = Search_backup.screen_top
Search_backup = nil
Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
elseif chord == 'return' then
Search_term = nil
Search_text = nil
Search_backup = nil
elseif chord == 'backspace' then
local len = utf8.len(Search_term)
local byte_offset = utf8.offset(Search_term, len)
Search_term = string.sub(Search_term, 1, byte_offset-1)
Search_text = nil
elseif chord == 'down' then
Cursor1.pos = Cursor1.pos+1
Text.search_next()
elseif chord == 'up' then
Text.search_previous()
end
return
elseif chord == 'C-f' then
Search_term = ''
Search_backup = {cursor={line=Cursor1.line, pos=Cursor1.pos}, screen_top={line=Screen_top1.line, pos=Screen_top1.pos}}
assert(Search_text == nil)
elseif chord == 'C-=' then
initialize_font_settings(Font_height+2)
Text.redraw_all()
elseif chord == 'C--' then
initialize_font_settings(Font_height-2)
Text.redraw_all()
elseif chord == 'C-0' then
initialize_font_settings(20)
Text.redraw_all()
elseif chord == 'C-z' then
for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
local event = undo_event()
if event then
local src = event.before
Screen_top1 = deepcopy(src.screen_top)
Cursor1 = deepcopy(src.cursor)
Selection1 = deepcopy(src.selection)
patch(Lines, event.after, event.before)
Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
end
elseif chord == 'C-y' then
for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
local event = redo_event()
if event then
local src = event.after
Screen_top1 = deepcopy(src.screen_top)
Cursor1 = deepcopy(src.cursor)
Selection1 = deepcopy(src.selection)
patch(Lines, event.before, event.after)
Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
end
-- clipboard
elseif chord == 'C-c' then
print('C-c', collectgarbage('count'))
for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
local s = Text.selection()
if s then
App.setClipboardText(s)
end
elseif chord == 'C-x' then
for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
local s = Text.cut_selection()
if s then
App.setClipboardText(s)
end
save_to_disk(Lines, Filename)
elseif chord == 'C-v' then
for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
-- We don't have a good sense of when to scroll, so we'll be conservative
-- and sometimes scroll when we didn't quite need to.
local before_line = Cursor1.line
local before = snapshot(before_line)
local clipboard_data = App.getClipboardText()
local num_newlines = 0 -- hack 1
for _,code in utf8.codes(clipboard_data) do
local c = utf8.char(code)
if c == '\n' then
Text.insert_return()
num_newlines = num_newlines+1
else
Text.insert_at_cursor(c)
end
end
-- hack 1: if we have too many newlines we definitely need to scroll
for i=before_line,Cursor1.line do
Lines[i].screen_line_starting_pos = nil
Text.populate_screen_line_starting_pos(i)
end
if Cursor1.line-Screen_top1.line+1 + num_newlines > App.screen.height/Line_height then
Text.snap_cursor_to_bottom_of_screen()
end
-- hack 2: if we have too much text wrapping we definitely need to scroll
local clipboard_text = App.newText(love.graphics.getFont(), clipboard_data)
local clipboard_width = App.width(clipboard_text)
--? print(Cursor_y, Cursor_y*Line_width, Cursor_y*Line_width+Cursor_x, Cursor_y*Line_width+Cursor_x+clipboard_width, Line_width*App.screen.height/Line_height)
if Cursor_y*Line_width+Cursor_x + clipboard_width > Line_width*App.screen.height/Line_height then
Text.snap_cursor_to_bottom_of_screen()
end
save_to_disk(Lines, Filename)
record_undo_event({before=before, after=snapshot(before_line, Cursor1.line)})
-- dispatch to drawing or text
elseif love.mouse.isDown('1') or chord:sub(1,2) == 'C-' then
-- DON'T reset line.y here
Drawing.keychord_pressed(chord)
elseif chord == 'escape' and love.mouse.isDown('1') then
local drawing = Drawing.current_drawing()
if drawing then
drawing.pending = {}
end
elseif chord == 'escape' and not love.mouse.isDown('1') then
for _,line in ipairs(Lines) do
if line.mode == 'drawing' then
line.show_help = false
end
end
elseif Current_drawing_mode == 'name' then
if chord == 'return' then
Current_drawing_mode = Previous_drawing_mode
Previous_drawing_mode = nil
else
local drawing = Lines.current_drawing
local p = drawing.points[drawing.pending.target_point]
if chord == 'escape' then
p.name = nil
elseif chord == 'backspace' then
local len = utf8.len(p.name)
local byte_offset = utf8.offset(p.name, len-1)
p.name = string.sub(p.name, 1, byte_offset)
end
end
save_to_disk(Lines, Filename)
else
for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
Text.keychord_pressed(chord)
end
end
function App.keyreleased(key, scancode)
end
--
-- Collect memory reference info.
-- https://github.com/yaukeywang/LuaMemorySnapshotDump
--
-- @filename MemoryReferenceInfo.lua
-- @author WangYaoqi
-- @date 2016-02-03
-- The global config of the mri.
local cConfig =
{
m_bAllMemoryRefFileAddTime = true,
m_bSingleMemoryRefFileAddTime = true,
m_bComparedMemoryRefFileAddTime = true
}
-- Get the format string of date time.
local function FormatDateTimeNow()
local cDateTime = os.date("*t")
local strDateTime = string.format("%04d%02d%02d-%02d%02d%02d", tostring(cDateTime.year), tostring(cDateTime.month), tostring(cDateTime.day),
tostring(cDateTime.hour), tostring(cDateTime.min), tostring(cDateTime.sec))
return strDateTime
end
-- Get the string result without overrided __tostring.
local function GetOriginalToStringResult(cObject)
if not cObject then
return ""
end
local cMt = getmetatable(cObject)
if not cMt then
return tostring(cObject)
end
-- Check tostring override.
local strName = ""
local cToString = rawget(cMt, "__tostring")
if cToString then
rawset(cMt, "__tostring", nil)
strName = tostring(cObject)
rawset(cMt, "__tostring", cToString)
else
strName = tostring(cObject)
end
return strName
end
-- Create a container to collect the mem ref info results.
local function CreateObjectReferenceInfoContainer()
-- Create new container.
local cContainer = {}
-- Contain [table/function] - [reference count] info.
local cObjectReferenceCount = {}
setmetatable(cObjectReferenceCount, {__mode = "k"})
-- Contain [table/function] - [name] info.
local cObjectAddressToName = {}
setmetatable(cObjectAddressToName, {__mode = "k"})
-- Set members.
cContainer.m_cObjectReferenceCount = cObjectReferenceCount
cContainer.m_cObjectAddressToName = cObjectAddressToName
-- For stack info.
cContainer.m_nStackLevel = -1
cContainer.m_strShortSrc = "None"
cContainer.m_nCurrentLine = -1
return cContainer
end
-- Create a container to collect the mem ref info results from a dumped file.
-- strFilePath - The file path.
local function CreateObjectReferenceInfoContainerFromFile(strFilePath)
-- Create a empty container.
local cContainer = CreateObjectReferenceInfoContainer()
cContainer.m_strShortSrc = strFilePath
-- Cache ref info.
local cRefInfo = cContainer.m_cObjectReferenceCount
local cNameInfo = cContainer.m_cObjectAddressToName
-- Read each line from file.
local cFile = assert(io.open(strFilePath, "rb"))
for strLine in cFile:lines() do
local strHeader = string.sub(strLine, 1, 2)
if "--" ~= strHeader then
local _, _, strAddr, strName, strRefCount= string.find(strLine, "(.+)\t(.*)\t(%d+)")
if strAddr then
cRefInfo[strAddr] = strRefCount
cNameInfo[strAddr] = strName
end
end
end
-- Close and clear file handler.
io.close(cFile)
cFile = nil
return cContainer
end
-- Create a container to collect the mem ref info results from a dumped file.
-- strObjectName - The object name you need to collect info.
-- cObject - The object you need to collect info.
local function CreateSingleObjectReferenceInfoContainer(strObjectName, cObject)
-- Create new container.
local cContainer = {}
-- Contain [address] - [true] info.
local cObjectExistTag = {}
setmetatable(cObjectExistTag, {__mode = "k"})
-- Contain [name] - [true] info.
local cObjectAliasName = {}
-- Contain [access] - [true] info.
local cObjectAccessTag = {}
setmetatable(cObjectAccessTag, {__mode = "k"})
-- Set members.
cContainer.m_cObjectExistTag = cObjectExistTag
cContainer.m_cObjectAliasName = cObjectAliasName
cContainer.m_cObjectAccessTag = cObjectAccessTag
-- For stack info.
cContainer.m_nStackLevel = -1
cContainer.m_strShortSrc = "None"
cContainer.m_nCurrentLine = -1
-- Init with object values.
cContainer.m_strObjectName = strObjectName
cContainer.m_strAddressName = (("string" == type(cObject)) and ("\"" .. tostring(cObject) .. "\"")) or GetOriginalToStringResult(cObject)
cContainer.m_cObjectExistTag[cObject] = true
return cContainer
end
-- Collect memory reference info from a root table or function.
-- strName - The root object name that start to search, default is "_G" if leave this to nil.
-- cObject - The root object that start to search, default is _G if leave this to nil.
-- cDumpInfoContainer - The container of the dump result info.
local function CollectObjectReferenceInMemory(strName, cObject, cDumpInfoContainer)
if not cObject then
return
end
if not strName then
strName = ""
end
-- Check container.
if (not cDumpInfoContainer) then
cDumpInfoContainer = CreateObjectReferenceInfoContainer()
end
-- Check stack.
if cDumpInfoContainer.m_nStackLevel > 0 then
local cStackInfo = debug.getinfo(cDumpInfoContainer.m_nStackLevel, "Sl")
if cStackInfo then
cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src
cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline
end
cDumpInfoContainer.m_nStackLevel = -1
end
-- Get ref and name info.
local cRefInfoContainer = cDumpInfoContainer.m_cObjectReferenceCount
local cNameInfoContainer = cDumpInfoContainer.m_cObjectAddressToName
local strType = type(cObject)
if "table" == strType then
-- Check table with class name.
if rawget(cObject, "__cname") then
if "string" == type(cObject.__cname) then
strName = strName .. "[class:" .. cObject.__cname .. "]"
end
elseif rawget(cObject, "class") then
if "string" == type(cObject.class) then
strName = strName .. "[class:" .. cObject.class .. "]"
end
elseif rawget(cObject, "_className") then
if "string" == type(cObject._className) then
strName = strName .. "[class:" .. cObject._className .. "]"
end
end
-- Check if table is _G.
if cObject == _G then
strName = strName .. "[_G]"
end
-- Get metatable.
local bWeakK = false
local bWeakV = false
local cMt = getmetatable(cObject)
if cMt then
-- Check mode.
local strMode = rawget(cMt, "__mode")
if strMode then
if "k" == strMode then
bWeakK = true
elseif "v" == strMode then
bWeakV = true
elseif "kv" == strMode then
bWeakK = true
bWeakV = true
end
end
end
-- Add reference and name.
cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
if cNameInfoContainer[cObject] then
return
end
-- Set name.
cNameInfoContainer[cObject] = strName
-- Dump table key and value.
for k, v in pairs(cObject) do
-- Check key type.
local strKeyType = type(k)
if "table" == strKeyType then
if not bWeakK then
CollectObjectReferenceInMemory(strName .. ".[table:key.table]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "function" == strKeyType then
if not bWeakK then
CollectObjectReferenceInMemory(strName .. ".[table:key.function]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "thread" == strKeyType then
if not bWeakK then
CollectObjectReferenceInMemory(strName .. ".[table:key.thread]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "userdata" == strKeyType then
if not bWeakK then
CollectObjectReferenceInMemory(strName .. ".[table:key.userdata]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
else
CollectObjectReferenceInMemory(strName .. "." .. k, v, cDumpInfoContainer)
end
end
-- Dump metatable.
if cMt then
CollectObjectReferenceInMemory(strName ..".[metatable]", cMt, cDumpInfoContainer)
end
elseif "function" == strType then
-- Get function info.
local cDInfo = debug.getinfo(cObject, "Su")
-- Write this info.
cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
if cNameInfoContainer[cObject] then
return
end
-- Set name.
cNameInfoContainer[cObject] = strName .. "[line:" .. tostring(cDInfo.linedefined) .. "@file:" .. cDInfo.short_src .. "]"
-- Get upvalues.
local nUpsNum = cDInfo.nups
for i = 1, nUpsNum do
local strUpName, cUpValue = debug.getupvalue(cObject, i)
local strUpValueType = type(cUpValue)
--print(strUpName, cUpValue)
if "table" == strUpValueType then
CollectObjectReferenceInMemory(strName .. ".[ups:table:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "function" == strUpValueType then
CollectObjectReferenceInMemory(strName .. ".[ups:function:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "thread" == strUpValueType then
CollectObjectReferenceInMemory(strName .. ".[ups:thread:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "userdata" == strUpValueType then
CollectObjectReferenceInMemory(strName .. ".[ups:userdata:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
end
end
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectObjectReferenceInMemory(strName ..".[function:environment]", cEnv, cDumpInfoContainer)
end
end
elseif "thread" == strType then
-- Add reference and name.
cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
if cNameInfoContainer[cObject] then
return
end
-- Set name.
cNameInfoContainer[cObject] = strName
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectObjectReferenceInMemory(strName ..".[thread:environment]", cEnv, cDumpInfoContainer)
end
end
-- Dump metatable.
local cMt = getmetatable(cObject)
if cMt then
CollectObjectReferenceInMemory(strName ..".[thread:metatable]", cMt, cDumpInfoContainer)
end
elseif "userdata" == strType then
-- Add reference and name.
cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
if cNameInfoContainer[cObject] then
return
end
-- Set name.
cNameInfoContainer[cObject] = strName
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectObjectReferenceInMemory(strName ..".[userdata:environment]", cEnv, cDumpInfoContainer)
end
end
-- Dump metatable.
local cMt = getmetatable(cObject)
if cMt then
CollectObjectReferenceInMemory(strName ..".[userdata:metatable]", cMt, cDumpInfoContainer)
end
elseif "string" == strType then
-- Add reference and name.
cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
if cNameInfoContainer[cObject] then
return
end
-- Set name.
cNameInfoContainer[cObject] = strName .. "[" .. strType .. "]"
else
-- For "number" and "boolean". (If you want to dump them, uncomment the followed lines.)
-- -- Add reference and name.
-- cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
-- if cNameInfoContainer[cObject] then
-- return
-- end
-- -- Set name.
-- cNameInfoContainer[cObject] = strName .. "[" .. strType .. ":" .. tostring(cObject) .. "]"
end
end
-- Collect memory reference info of a single object from a root table or function.
-- strName - The root object name that start to search, can not be nil.
-- cObject - The root object that start to search, can not be nil.
-- cDumpInfoContainer - The container of the dump result info.
local function CollectSingleObjectReferenceInMemory(strName, cObject, cDumpInfoContainer)
if not cObject then
return
end
if not strName then
strName = ""
end
-- Check container.
if (not cDumpInfoContainer) then
cDumpInfoContainer = CreateObjectReferenceInfoContainer()
end
-- Check stack.
if cDumpInfoContainer.m_nStackLevel > 0 then
local cStackInfo = debug.getinfo(cDumpInfoContainer.m_nStackLevel, "Sl")
if cStackInfo then
cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src
cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline
end
cDumpInfoContainer.m_nStackLevel = -1
end
local cExistTag = cDumpInfoContainer.m_cObjectExistTag
local cNameAllAlias = cDumpInfoContainer.m_cObjectAliasName
local cAccessTag = cDumpInfoContainer.m_cObjectAccessTag
local strType = type(cObject)
if "table" == strType then
-- Check table with class name.
if rawget(cObject, "__cname") then
if "string" == type(cObject.__cname) then
strName = strName .. "[class:" .. cObject.__cname .. "]"
end
elseif rawget(cObject, "class") then
if "string" == type(cObject.class) then
strName = strName .. "[class:" .. cObject.class .. "]"
end
elseif rawget(cObject, "_className") then
if "string" == type(cObject._className) then
strName = strName .. "[class:" .. cObject._className .. "]"
end
end
-- Check if table is _G.
if cObject == _G then
strName = strName .. "[_G]"
end
-- Get metatable.
local bWeakK = false
local bWeakV = false
local cMt = getmetatable(cObject)
if cMt then
-- Check mode.
local strMode = rawget(cMt, "__mode")
if strMode then
if "k" == strMode then
bWeakK = true
elseif "v" == strMode then
bWeakV = true
elseif "kv" == strMode then
bWeakK = true
bWeakV = true
end
end
end
-- Check if the specified object.
if cExistTag[cObject] and (not cNameAllAlias[strName]) then
cNameAllAlias[strName] = true
end
-- Add reference and name.
if cAccessTag[cObject] then
return
end
-- Get this name.
cAccessTag[cObject] = true
-- Dump table key and value.
for k, v in pairs(cObject) do
-- Check key type.
local strKeyType = type(k)
if "table" == strKeyType then
if not bWeakK then
CollectSingleObjectReferenceInMemory(strName .. ".[table:key.table]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "function" == strKeyType then
if not bWeakK then
CollectSingleObjectReferenceInMemory(strName .. ".[table:key.function]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "thread" == strKeyType then
if not bWeakK then
CollectSingleObjectReferenceInMemory(strName .. ".[table:key.thread]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "userdata" == strKeyType then
if not bWeakK then
CollectSingleObjectReferenceInMemory(strName .. ".[table:key.userdata]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
else
CollectSingleObjectReferenceInMemory(strName .. "." .. k, v, cDumpInfoContainer)
end
end
-- Dump metatable.
if cMt then
CollectSingleObjectReferenceInMemory(strName ..".[metatable]", cMt, cDumpInfoContainer)
end
elseif "function" == strType then
-- Get function info.
local cDInfo = debug.getinfo(cObject, "Su")
local cCombinedName = strName .. "[line:" .. tostring(cDInfo.linedefined) .. "@file:" .. cDInfo.short_src .. "]"
-- Check if the specified object.
if cExistTag[cObject] and (not cNameAllAlias[cCombinedName]) then
cNameAllAlias[cCombinedName] = true
end
-- Write this info.
if cAccessTag[cObject] then
return
end
-- Set name.
cAccessTag[cObject] = true
-- Get upvalues.
local nUpsNum = cDInfo.nups
for i = 1, nUpsNum do
local strUpName, cUpValue = debug.getupvalue(cObject, i)
local strUpValueType = type(cUpValue)
--print(strUpName, cUpValue)
if "table" == strUpValueType then
CollectSingleObjectReferenceInMemory(strName .. ".[ups:table:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "function" == strUpValueType then
CollectSingleObjectReferenceInMemory(strName .. ".[ups:function:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "thread" == strUpValueType then
CollectSingleObjectReferenceInMemory(strName .. ".[ups:thread:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "userdata" == strUpValueType then
CollectSingleObjectReferenceInMemory(strName .. ".[ups:userdata:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
end
end
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectSingleObjectReferenceInMemory(strName ..".[function:environment]", cEnv, cDumpInfoContainer)
end
end
elseif "thread" == strType then
-- Check if the specified object.
if cExistTag[cObject] and (not cNameAllAlias[strName]) then
cNameAllAlias[strName] = true
end
-- Add reference and name.
if cAccessTag[cObject] then
return
end
-- Get this name.
cAccessTag[cObject] = true
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectSingleObjectReferenceInMemory(strName ..".[thread:environment]", cEnv, cDumpInfoContainer)
end
end
-- Dump metatable.
local cMt = getmetatable(cObject)
if cMt then
CollectSingleObjectReferenceInMemory(strName ..".[thread:metatable]", cMt, cDumpInfoContainer)
end
elseif "userdata" == strType then
-- Check if the specified object.
if cExistTag[cObject] and (not cNameAllAlias[strName]) then
cNameAllAlias[strName] = true
end
-- Add reference and name.
if cAccessTag[cObject] then
return
end
-- Get this name.
cAccessTag[cObject] = true
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectSingleObjectReferenceInMemory(strName ..".[userdata:environment]", cEnv, cDumpInfoContainer)
end
end
-- Dump metatable.
local cMt = getmetatable(cObject)
if cMt then
CollectSingleObjectReferenceInMemory(strName ..".[userdata:metatable]", cMt, cDumpInfoContainer)
end
elseif "string" == strType then
-- Check if the specified object.
if cExistTag[cObject] and (not cNameAllAlias[strName]) then
cNameAllAlias[strName] = true
end
-- Add reference and name.
if cAccessTag[cObject] then
return
end
-- Get this name.
cAccessTag[cObject] = true
else
-- For "number" and "boolean" type, they are not object type, skip.
end
end
-- The base method to dump a mem ref info result into a file.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- strRootObjectName - The header info to show the root object name, can be nil.
-- cRootObject - The header info to show the root object address, can be nil.
-- cDumpInfoResultsBase - The base dumped mem info result, nil means no compare and only output cDumpInfoResults, otherwise to compare with cDumpInfoResults.
-- cDumpInfoResults - The compared dumped mem info result, dump itself only if cDumpInfoResultsBase is nil, otherwise dump compared results with cDumpInfoResultsBase.
local function OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, strRootObjectName, cRootObject, cDumpInfoResultsBase, cDumpInfoResults)
-- Check results.
if not cDumpInfoResults then
return
end
-- Get time format string.
local strDateTime = FormatDateTimeNow()
-- Collect memory info.
local cRefInfoBase = (cDumpInfoResultsBase and cDumpInfoResultsBase.m_cObjectReferenceCount) or nil
local cNameInfoBase = (cDumpInfoResultsBase and cDumpInfoResultsBase.m_cObjectAddressToName) or nil
local cRefInfo = cDumpInfoResults.m_cObjectReferenceCount
local cNameInfo = cDumpInfoResults.m_cObjectAddressToName
-- Create a cache result to sort by ref count.
local cRes = {}
local nIdx = 0
for k in pairs(cRefInfo) do
nIdx = nIdx + 1
cRes[nIdx] = k
end
-- Sort result.
table.sort(cRes, function (l, r)
return cRefInfo[l] > cRefInfo[r]
end)
-- Save result to file.
local bOutputFile = strSavePath and (string.len(strSavePath) > 0)
local cOutputHandle = nil
local cOutputEntry = print
if bOutputFile then
-- Check save path affix.
local strAffix = string.sub(strSavePath, -1)
if ("/" ~= strAffix) and ("\\" ~= strAffix) then
strSavePath = strSavePath .. "/"
end
-- Combine file name.
local strFileName = strSavePath .. "LuaMemRefInfo-All"
if (not strExtraFileName) or (0 == string.len(strExtraFileName)) then
if cDumpInfoResultsBase then
if cConfig.m_bComparedMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "].txt"
else
strFileName = strFileName .. ".txt"
end
else
if cConfig.m_bAllMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "].txt"
else
strFileName = strFileName .. ".txt"
end
end
else
if cDumpInfoResultsBase then
if cConfig.m_bComparedMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"
else
strFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"
end
else
if cConfig.m_bAllMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"
else
strFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"
end
end
end
local cFile = assert(io.open(strFileName, "w"))
cOutputHandle = cFile
cOutputEntry = cFile.write
end
local cOutputer = function (strContent)
if cOutputHandle then
cOutputEntry(cOutputHandle, strContent)
else
cOutputEntry(strContent)
end
end
-- Write table header.
if cDumpInfoResultsBase then
cOutputer("--------------------------------------------------------\n")
cOutputer("-- This is compared memory information.\n")
cOutputer("--------------------------------------------------------\n")
cOutputer("-- Collect base memory reference at line:" .. tostring(cDumpInfoResultsBase.m_nCurrentLine) .. "@file:" .. cDumpInfoResultsBase.m_strShortSrc .. "\n")
cOutputer("-- Collect compared memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n")
else
cOutputer("--------------------------------------------------------\n")
cOutputer("-- Collect memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n")
end
cOutputer("--------------------------------------------------------\n")
cOutputer("-- [Table/Function/String Address/Name]\t[Reference Path]\t[Reference Count]\n")
cOutputer("--------------------------------------------------------\n")
if strRootObjectName and cRootObject then
if "string" == type(cRootObject) then
cOutputer("-- From Root Object: \"" .. tostring(cRootObject) .. "\" (" .. strRootObjectName .. ")\n")
else
cOutputer("-- From Root Object: " .. GetOriginalToStringResult(cRootObject) .. " (" .. strRootObjectName .. ")\n")
end
end
-- Save each info.
for i, v in ipairs(cRes) do
if (not cDumpInfoResultsBase) or (not cRefInfoBase[v]) then
if (nMaxRescords > 0) then
if (i <= nMaxRescords) then
if "string" == type(v) then
local strOrgString = tostring(v)
local nPattenBegin, nPattenEnd = string.find(strOrgString, "string: \".*\"")
if ((not cDumpInfoResultsBase) and ((nil == nPattenBegin) or (nil == nPattenEnd))) then
local strRepString = string.gsub(strOrgString, "([\n\r])", "\\n")
cOutputer("string: \"" .. strRepString .. "\"\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
else
cOutputer(tostring(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
end
else
cOutputer(GetOriginalToStringResult(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
end
end
else
if "string" == type(v) then
local strOrgString = tostring(v)
local nPattenBegin, nPattenEnd = string.find(strOrgString, "string: \".*\"")
if ((not cDumpInfoResultsBase) and ((nil == nPattenBegin) or (nil == nPattenEnd))) then
local strRepString = string.gsub(strOrgString, "([\n\r])", "\\n")
cOutputer("string: \"" .. strRepString .. "\"\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
else
cOutputer(tostring(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
end
else
cOutputer(GetOriginalToStringResult(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
end
end
end
end
if bOutputFile then
io.close(cOutputHandle)
cOutputHandle = nil
end
end
-- The base method to dump a mem ref info result of a single object into a file.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- cDumpInfoResults - The dumped results.
local function OutputMemorySnapshotSingleObject(strSavePath, strExtraFileName, nMaxRescords, cDumpInfoResults)
-- Check results.
if not cDumpInfoResults then
return
end
-- Get time format string.
local strDateTime = FormatDateTimeNow()
-- Collect memory info.
local cObjectAliasName = cDumpInfoResults.m_cObjectAliasName
-- Save result to file.
local bOutputFile = strSavePath and (string.len(strSavePath) > 0)
local cOutputHandle = nil
local cOutputEntry = print
if bOutputFile then
-- Check save path affix.
local strAffix = string.sub(strSavePath, -1)
if ("/" ~= strAffix) and ("\\" ~= strAffix) then
strSavePath = strSavePath .. "/"
end
-- Combine file name.
local strFileName = strSavePath .. "LuaMemRefInfo-Single"
if (not strExtraFileName) or (0 == string.len(strExtraFileName)) then
if cConfig.m_bSingleMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "].txt"
else
strFileName = strFileName .. ".txt"
end
else
if cConfig.m_bSingleMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"
else
strFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"
end
end
local cFile = assert(io.open(strFileName, "w"))
cOutputHandle = cFile
cOutputEntry = cFile.write
end
local cOutputer = function (strContent)
if cOutputHandle then
cOutputEntry(cOutputHandle, strContent)
else
cOutputEntry(strContent)
end
end
-- Write table header.
cOutputer("--------------------------------------------------------\n")
cOutputer("-- Collect single object memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n")
cOutputer("--------------------------------------------------------\n")
-- Calculate reference count.
local nCount = 0
for k in pairs(cObjectAliasName) do
nCount = nCount + 1
end
-- Output reference count.
cOutputer("-- For Object: " .. cDumpInfoResults.m_strAddressName .. " (" .. cDumpInfoResults.m_strObjectName .. "), have " .. tostring(nCount) .. " reference in total.\n")
cOutputer("--------------------------------------------------------\n")
-- Save each info.
for k in pairs(cObjectAliasName) do
if (nMaxRescords > 0) then
if (i <= nMaxRescords) then
cOutputer(k .. "\n")
end
else
cOutputer(k .. "\n")
end
end
if bOutputFile then
io.close(cOutputHandle)
cOutputHandle = nil
end
end
-- Fileter an existing result file and output it.
-- strFilePath - The existing result file.
-- strFilter - The filter string.
-- bIncludeFilter - Include(true) or exclude(false) the filter.
-- bOutputFile - Output to file(true) or console(false).
local function OutputFilteredResult(strFilePath, strFilter, bIncludeFilter, bOutputFile)
if (not strFilePath) or (0 == string.len(strFilePath)) then
print("You need to specify a file path.")
return
end
if (not strFilter) or (0 == string.len(strFilter)) then
print("You need to specify a filter string.")
return
end
-- Read file.
local cFilteredResult = {}
local cReadFile = assert(io.open(strFilePath, "rb"))
for strLine in cReadFile:lines() do
local nBegin, nEnd = string.find(strLine, strFilter)
if nBegin and nEnd then
if bIncludeFilter then
nBegin, nEnd = string.find(strLine, "[\r\n]")
if nBegin and nEnd and (string.len(strLine) == nEnd) then
table.insert(cFilteredResult, string.sub(strLine, 1, nBegin - 1))
else
table.insert(cFilteredResult, strLine)
end
end
else
if not bIncludeFilter then
nBegin, nEnd = string.find(strLine, "[\r\n]")
if nBegin and nEnd and (string.len(strLine) == nEnd) then
table.insert(cFilteredResult, string.sub(strLine, 1, nBegin - 1))
else
table.insert(cFilteredResult, strLine)
end
end
end
end
-- Close and clear read file handle.
io.close(cReadFile)
cReadFile = nil
-- Write filtered result.
local cOutputHandle = nil
local cOutputEntry = print
if bOutputFile then
-- Combine file name.
local _, _, strResFileName = string.find(strFilePath, "(.*)%.txt")
strResFileName = strResFileName .. "-Filter-" .. ((bIncludeFilter and "I") or "E") .. "-[" .. strFilter .. "].txt"
local cFile = assert(io.open(strResFileName, "w"))
cOutputHandle = cFile
cOutputEntry = cFile.write
end
local cOutputer = function (strContent)
if cOutputHandle then
cOutputEntry(cOutputHandle, strContent)
else
cOutputEntry(strContent)
end
end
-- Output result.
for i, v in ipairs(cFilteredResult) do
cOutputer(v .. "\n")
end
if bOutputFile then
io.close(cOutputHandle)
cOutputHandle = nil
end
end
-- Dump memory reference at current time.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- strRootObjectName - The root object name that start to search, default is "_G" if leave this to nil.
-- cRootObject - The root object that start to search, default is _G if leave this to nil.
local function DumpMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, strRootObjectName, cRootObject)
-- Get time format string.
local strDateTime = FormatDateTimeNow()
-- Check root object.
if cRootObject then
if (not strRootObjectName) or (0 == string.len(strRootObjectName)) then
strRootObjectName = tostring(cRootObject)
end
else
cRootObject = debug.getregistry()
strRootObjectName = "registry"
end
-- Create container.
local cDumpInfoContainer = CreateObjectReferenceInfoContainer()
local cStackInfo = debug.getinfo(2, "Sl")
if cStackInfo then
cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src
cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline
end
-- Collect memory info.
CollectObjectReferenceInMemory(strRootObjectName, cRootObject, cDumpInfoContainer)
-- Dump the result.
OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, strRootObjectName, cRootObject, nil, cDumpInfoContainer)
end
-- Dump compared memory reference results generated by DumpMemorySnapshot.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- cResultBefore - The base dumped results.
-- cResultAfter - The compared dumped results.
local function DumpMemorySnapshotCompared(strSavePath, strExtraFileName, nMaxRescords, cResultBefore, cResultAfter)
-- Dump the result.
OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, nil, nil, cResultBefore, cResultAfter)
end
-- Dump compared memory reference file results generated by DumpMemorySnapshot.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- strResultFilePathBefore - The base dumped results file.
-- strResultFilePathAfter - The compared dumped results file.
local function DumpMemorySnapshotComparedFile(strSavePath, strExtraFileName, nMaxRescords, strResultFilePathBefore, strResultFilePathAfter)
-- Read results from file.
local cResultBefore = CreateObjectReferenceInfoContainerFromFile(strResultFilePathBefore)
local cResultAfter = CreateObjectReferenceInfoContainerFromFile(strResultFilePathAfter)
-- Dump the result.
OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, nil, nil, cResultBefore, cResultAfter)
end
-- Dump memory reference of a single object at current time.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- strObjectName - The object name reference you want to dump.
-- cObject - The object reference you want to dump.
local function DumpMemorySnapshotSingleObject(strSavePath, strExtraFileName, nMaxRescords, strObjectName, cObject)
-- Check object.
if not cObject then
return
end
if (not strObjectName) or (0 == string.len(strObjectName)) then
strObjectName = GetOriginalToStringResult(cObject)
end
-- Get time format string.
local strDateTime = FormatDateTimeNow()
-- Create container.
local cDumpInfoContainer = CreateSingleObjectReferenceInfoContainer(strObjectName, cObject)
local cStackInfo = debug.getinfo(2, "Sl")
if cStackInfo then
cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src
cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline
end
-- Collect memory info.
CollectSingleObjectReferenceInMemory("registry", debug.getregistry(), cDumpInfoContainer)
-- Dump the result.
OutputMemorySnapshotSingleObject(strSavePath, strExtraFileName, nMaxRescords, cDumpInfoContainer)
end
-- Return methods.
local cPublications = {m_cConfig = nil, m_cMethods = {}, m_cHelpers = {}, m_cBases = {}}
cPublications.m_cConfig = cConfig
cPublications.m_cMethods.DumpMemorySnapshot = DumpMemorySnapshot
cPublications.m_cMethods.DumpMemorySnapshotCompared = DumpMemorySnapshotCompared
cPublications.m_cMethods.DumpMemorySnapshotComparedFile = DumpMemorySnapshotComparedFile
cPublications.m_cMethods.DumpMemorySnapshotSingleObject = DumpMemorySnapshotSingleObject
cPublications.m_cHelpers.FormatDateTimeNow = FormatDateTimeNow
cPublications.m_cHelpers.GetOriginalToStringResult = GetOriginalToStringResult
cPublications.m_cBases.CreateObjectReferenceInfoContainer = CreateObjectReferenceInfoContainer
cPublications.m_cBases.CreateObjectReferenceInfoContainerFromFile = CreateObjectReferenceInfoContainerFromFile
cPublications.m_cBases.CreateSingleObjectReferenceInfoContainer = CreateSingleObjectReferenceInfoContainer
cPublications.m_cBases.CollectObjectReferenceInMemory = CollectObjectReferenceInMemory
cPublications.m_cBases.CollectSingleObjectReferenceInMemory = CollectSingleObjectReferenceInMemory
cPublications.m_cBases.OutputMemorySnapshot = OutputMemorySnapshot
cPublications.m_cBases.OutputMemorySnapshotSingleObject = OutputMemorySnapshotSingleObject
cPublications.m_cBases.OutputFilteredResult = OutputFilteredResult
return cPublications
--
-- Collect memory reference info.
-- https://github.com/yaukeywang/LuaMemorySnapshotDump
--
-- @filename MemoryReferenceInfo.lua
-- @author WangYaoqi
-- @date 2016-02-03
-- The global config of the mri.
local cConfig =
{
m_bAllMemoryRefFileAddTime = true,
m_bSingleMemoryRefFileAddTime = true,
m_bComparedMemoryRefFileAddTime = true
}
-- Get the format string of date time.
local function FormatDateTimeNow()
local cDateTime = os.date("*t")
local strDateTime = string.format("%04d%02d%02d-%02d%02d%02d", tostring(cDateTime.year), tostring(cDateTime.month), tostring(cDateTime.day),
tostring(cDateTime.hour), tostring(cDateTime.min), tostring(cDateTime.sec))
return strDateTime
end
-- Get the string result without overrided __tostring.
local function GetOriginalToStringResult(cObject)
if not cObject then
return ""
end
local cMt = getmetatable(cObject)
if not cMt then
return tostring(cObject)
end
-- Check tostring override.
local strName = ""
local cToString = rawget(cMt, "__tostring")
if cToString then
print('tostring overridden:', tostring(cObject))
--? rawset(cMt, "__tostring", nil)
--? strName = tostring(cObject)
--? rawset(cMt, "__tostring", cToString)
--? else
--? strName = tostring(cObject)
end
strName = tostring(cObject)
return strName
end
-- Create a container to collect the mem ref info results.
local function CreateObjectReferenceInfoContainer()
-- Create new container.
local cContainer = {}
-- Contain [table/function] - [reference count] info.
local cObjectReferenceCount = {}
setmetatable(cObjectReferenceCount, {__mode = "k"})
-- Contain [table/function] - [name] info.
local cObjectAddressToName = {}
setmetatable(cObjectAddressToName, {__mode = "k"})
-- Set members.
cContainer.m_cObjectReferenceCount = cObjectReferenceCount
cContainer.m_cObjectAddressToName = cObjectAddressToName
-- For stack info.
cContainer.m_nStackLevel = -1
cContainer.m_strShortSrc = "None"
cContainer.m_nCurrentLine = -1
return cContainer
end
-- Create a container to collect the mem ref info results from a dumped file.
-- strFilePath - The file path.
local function CreateObjectReferenceInfoContainerFromFile(strFilePath)
-- Create a empty container.
local cContainer = CreateObjectReferenceInfoContainer()
cContainer.m_strShortSrc = strFilePath
-- Cache ref info.
local cRefInfo = cContainer.m_cObjectReferenceCount
local cNameInfo = cContainer.m_cObjectAddressToName
-- Read each line from file.
local cFile = assert(io.open(strFilePath, "rb"))
for strLine in cFile:lines() do
local strHeader = string.sub(strLine, 1, 2)
if "--" ~= strHeader then
local _, _, strAddr, strName, strRefCount= string.find(strLine, "(.+)\t(.*)\t(%d+)")
if strAddr then
cRefInfo[strAddr] = strRefCount
cNameInfo[strAddr] = strName
end
end
end
-- Close and clear file handler.
io.close(cFile)
cFile = nil
return cContainer
end
-- Create a container to collect the mem ref info results from a dumped file.
-- strObjectName - The object name you need to collect info.
-- cObject - The object you need to collect info.
local function CreateSingleObjectReferenceInfoContainer(strObjectName, cObject)
-- Create new container.
local cContainer = {}
-- Contain [address] - [true] info.
local cObjectExistTag = {}
setmetatable(cObjectExistTag, {__mode = "k"})
-- Contain [name] - [true] info.
local cObjectAliasName = {}
-- Contain [access] - [true] info.
local cObjectAccessTag = {}
setmetatable(cObjectAccessTag, {__mode = "k"})
-- Set members.
cContainer.m_cObjectExistTag = cObjectExistTag
cContainer.m_cObjectAliasName = cObjectAliasName
cContainer.m_cObjectAccessTag = cObjectAccessTag
-- For stack info.
cContainer.m_nStackLevel = -1
cContainer.m_strShortSrc = "None"
cContainer.m_nCurrentLine = -1
-- Init with object values.
cContainer.m_strObjectName = strObjectName
cContainer.m_strAddressName = (("string" == type(cObject)) and ("\"" .. tostring(cObject) .. "\"")) or GetOriginalToStringResult(cObject)
cContainer.m_cObjectExistTag[cObject] = true
return cContainer
end
-- Collect memory reference info from a root table or function.
-- strName - The root object name that start to search, default is "_G" if leave this to nil.
-- cObject - The root object that start to search, default is _G if leave this to nil.
-- cDumpInfoContainer - The container of the dump result info.
local function CollectObjectReferenceInMemory(strName, cObject, cDumpInfoContainer)
if not cObject then
return
end
if not strName then
strName = ""
end
-- Check container.
if (not cDumpInfoContainer) then
cDumpInfoContainer = CreateObjectReferenceInfoContainer()
end
-- Check stack.
if cDumpInfoContainer.m_nStackLevel > 0 then
local cStackInfo = debug.getinfo(cDumpInfoContainer.m_nStackLevel, "Sl")
if cStackInfo then
cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src
cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline
end
cDumpInfoContainer.m_nStackLevel = -1
end
-- Get ref and name info.
local cRefInfoContainer = cDumpInfoContainer.m_cObjectReferenceCount
local cNameInfoContainer = cDumpInfoContainer.m_cObjectAddressToName
local strType = type(cObject)
if "table" == strType then
-- Check table with class name.
if rawget(cObject, "__cname") then
if "string" == type(cObject.__cname) then
strName = strName .. "[class:" .. cObject.__cname .. "]"
end
elseif rawget(cObject, "class") then
if "string" == type(cObject.class) then
strName = strName .. "[class:" .. cObject.class .. "]"
end
elseif rawget(cObject, "_className") then
if "string" == type(cObject._className) then
strName = strName .. "[class:" .. cObject._className .. "]"
end
end
-- Check if table is _G.
if cObject == _G then
strName = strName .. "[_G]"
end
-- Get metatable.
local bWeakK = false
local bWeakV = false
local cMt = getmetatable(cObject)
if cMt then
-- Check mode.
local strMode = rawget(cMt, "__mode")
if strMode then
if "k" == strMode then
bWeakK = true
elseif "v" == strMode then
bWeakV = true
elseif "kv" == strMode then
bWeakK = true
bWeakV = true
end
end
end
-- Add reference and name.
cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
if cNameInfoContainer[cObject] then
return
end
-- Set name.
cNameInfoContainer[cObject] = strName
-- Dump table key and value.
for k, v in pairs(cObject) do
-- Check key type.
local strKeyType = type(k)
if "table" == strKeyType then
if not bWeakK then
CollectObjectReferenceInMemory(strName .. ".[table:key.table]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "function" == strKeyType then
if not bWeakK then
CollectObjectReferenceInMemory(strName .. ".[table:key.function]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "thread" == strKeyType then
if not bWeakK then
CollectObjectReferenceInMemory(strName .. ".[table:key.thread]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "userdata" == strKeyType then
if not bWeakK then
CollectObjectReferenceInMemory(strName .. ".[table:key.userdata]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
else
CollectObjectReferenceInMemory(strName .. "." .. tostring(k), v, cDumpInfoContainer)
end
end
-- Dump metatable.
if cMt then
CollectObjectReferenceInMemory(strName ..".[metatable]", cMt, cDumpInfoContainer)
end
elseif "function" == strType then
-- Get function info.
local cDInfo = debug.getinfo(cObject, "Su")
-- Write this info.
cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
if cNameInfoContainer[cObject] then
return
end
-- Set name.
cNameInfoContainer[cObject] = strName .. "[line:" .. tostring(cDInfo.linedefined) .. "@file:" .. cDInfo.short_src .. "]"
-- Get upvalues.
local nUpsNum = cDInfo.nups
for i = 1, nUpsNum do
local strUpName, cUpValue = debug.getupvalue(cObject, i)
local strUpValueType = type(cUpValue)
--print(strUpName, cUpValue)
if "table" == strUpValueType then
CollectObjectReferenceInMemory(strName .. ".[ups:table:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "function" == strUpValueType then
CollectObjectReferenceInMemory(strName .. ".[ups:function:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "thread" == strUpValueType then
CollectObjectReferenceInMemory(strName .. ".[ups:thread:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "userdata" == strUpValueType then
CollectObjectReferenceInMemory(strName .. ".[ups:userdata:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
end
end
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectObjectReferenceInMemory(strName ..".[function:environment]", cEnv, cDumpInfoContainer)
end
end
elseif "thread" == strType then
-- Add reference and name.
cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
if cNameInfoContainer[cObject] then
return
end
-- Set name.
cNameInfoContainer[cObject] = strName
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectObjectReferenceInMemory(strName ..".[thread:environment]", cEnv, cDumpInfoContainer)
end
end
-- Dump metatable.
local cMt = getmetatable(cObject)
if cMt then
CollectObjectReferenceInMemory(strName ..".[thread:metatable]", cMt, cDumpInfoContainer)
end
elseif "userdata" == strType then
-- Add reference and name.
cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
if cNameInfoContainer[cObject] then
return
end
-- Set name.
cNameInfoContainer[cObject] = strName
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectObjectReferenceInMemory(strName ..".[userdata:environment]", cEnv, cDumpInfoContainer)
end
end
-- Dump metatable.
local cMt = getmetatable(cObject)
if cMt then
CollectObjectReferenceInMemory(strName ..".[userdata:metatable]", cMt, cDumpInfoContainer)
end
elseif "string" == strType then
-- Add reference and name.
cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
if cNameInfoContainer[cObject] then
return
end
-- Set name.
cNameInfoContainer[cObject] = strName .. "[" .. strType .. "]"
else
-- For "number" and "boolean". (If you want to dump them, uncomment the followed lines.)
-- -- Add reference and name.
-- cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1
-- if cNameInfoContainer[cObject] then
-- return
-- end
-- -- Set name.
-- cNameInfoContainer[cObject] = strName .. "[" .. strType .. ":" .. tostring(cObject) .. "]"
end
end
-- Collect memory reference info of a single object from a root table or function.
-- strName - The root object name that start to search, can not be nil.
-- cObject - The root object that start to search, can not be nil.
-- cDumpInfoContainer - The container of the dump result info.
local function CollectSingleObjectReferenceInMemory(strName, cObject, cDumpInfoContainer)
if not cObject then
return
end
if not strName then
strName = ""
end
-- Check container.
if (not cDumpInfoContainer) then
cDumpInfoContainer = CreateObjectReferenceInfoContainer()
end
-- Check stack.
if cDumpInfoContainer.m_nStackLevel > 0 then
local cStackInfo = debug.getinfo(cDumpInfoContainer.m_nStackLevel, "Sl")
if cStackInfo then
cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src
cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline
end
cDumpInfoContainer.m_nStackLevel = -1
end
local cExistTag = cDumpInfoContainer.m_cObjectExistTag
local cNameAllAlias = cDumpInfoContainer.m_cObjectAliasName
local cAccessTag = cDumpInfoContainer.m_cObjectAccessTag
local strType = type(cObject)
if "table" == strType then
-- Check table with class name.
if rawget(cObject, "__cname") then
if "string" == type(cObject.__cname) then
strName = strName .. "[class:" .. cObject.__cname .. "]"
end
elseif rawget(cObject, "class") then
if "string" == type(cObject.class) then
strName = strName .. "[class:" .. cObject.class .. "]"
end
elseif rawget(cObject, "_className") then
if "string" == type(cObject._className) then
strName = strName .. "[class:" .. cObject._className .. "]"
end
end
-- Check if table is _G.
if cObject == _G then
strName = strName .. "[_G]"
end
-- Get metatable.
local bWeakK = false
local bWeakV = false
local cMt = getmetatable(cObject)
if cMt then
-- Check mode.
local strMode = rawget(cMt, "__mode")
if strMode then
if "k" == strMode then
bWeakK = true
elseif "v" == strMode then
bWeakV = true
elseif "kv" == strMode then
bWeakK = true
bWeakV = true
end
end
end
-- Check if the specified object.
if cExistTag[cObject] and (not cNameAllAlias[strName]) then
cNameAllAlias[strName] = true
end
-- Add reference and name.
if cAccessTag[cObject] then
return
end
-- Get this name.
cAccessTag[cObject] = true
-- Dump table key and value.
for k, v in pairs(cObject) do
-- Check key type.
local strKeyType = type(k)
if "table" == strKeyType then
if not bWeakK then
CollectSingleObjectReferenceInMemory(strName .. ".[table:key.table]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "function" == strKeyType then
if not bWeakK then
CollectSingleObjectReferenceInMemory(strName .. ".[table:key.function]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "thread" == strKeyType then
if not bWeakK then
CollectSingleObjectReferenceInMemory(strName .. ".[table:key.thread]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
elseif "userdata" == strKeyType then
if not bWeakK then
CollectSingleObjectReferenceInMemory(strName .. ".[table:key.userdata]", k, cDumpInfoContainer)
end
if not bWeakV then
CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)
end
else
CollectSingleObjectReferenceInMemory(strName .. "." .. k, v, cDumpInfoContainer)
end
end
-- Dump metatable.
if cMt then
CollectSingleObjectReferenceInMemory(strName ..".[metatable]", cMt, cDumpInfoContainer)
end
elseif "function" == strType then
-- Get function info.
local cDInfo = debug.getinfo(cObject, "Su")
local cCombinedName = strName .. "[line:" .. tostring(cDInfo.linedefined) .. "@file:" .. cDInfo.short_src .. "]"
-- Check if the specified object.
if cExistTag[cObject] and (not cNameAllAlias[cCombinedName]) then
cNameAllAlias[cCombinedName] = true
end
-- Write this info.
if cAccessTag[cObject] then
return
end
-- Set name.
cAccessTag[cObject] = true
-- Get upvalues.
local nUpsNum = cDInfo.nups
for i = 1, nUpsNum do
local strUpName, cUpValue = debug.getupvalue(cObject, i)
local strUpValueType = type(cUpValue)
--print(strUpName, cUpValue)
if "table" == strUpValueType then
CollectSingleObjectReferenceInMemory(strName .. ".[ups:table:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "function" == strUpValueType then
CollectSingleObjectReferenceInMemory(strName .. ".[ups:function:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "thread" == strUpValueType then
CollectSingleObjectReferenceInMemory(strName .. ".[ups:thread:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
elseif "userdata" == strUpValueType then
CollectSingleObjectReferenceInMemory(strName .. ".[ups:userdata:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)
end
end
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectSingleObjectReferenceInMemory(strName ..".[function:environment]", cEnv, cDumpInfoContainer)
end
end
elseif "thread" == strType then
-- Check if the specified object.
if cExistTag[cObject] and (not cNameAllAlias[strName]) then
cNameAllAlias[strName] = true
end
-- Add reference and name.
if cAccessTag[cObject] then
return
end
-- Get this name.
cAccessTag[cObject] = true
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectSingleObjectReferenceInMemory(strName ..".[thread:environment]", cEnv, cDumpInfoContainer)
end
end
-- Dump metatable.
local cMt = getmetatable(cObject)
if cMt then
CollectSingleObjectReferenceInMemory(strName ..".[thread:metatable]", cMt, cDumpInfoContainer)
end
elseif "userdata" == strType then
-- Check if the specified object.
if cExistTag[cObject] and (not cNameAllAlias[strName]) then
cNameAllAlias[strName] = true
end
-- Add reference and name.
if cAccessTag[cObject] then
return
end
-- Get this name.
cAccessTag[cObject] = true
-- Dump environment table.
local getfenv = debug.getfenv
if getfenv then
local cEnv = getfenv(cObject)
if cEnv then
CollectSingleObjectReferenceInMemory(strName ..".[userdata:environment]", cEnv, cDumpInfoContainer)
end
end
-- Dump metatable.
local cMt = getmetatable(cObject)
if cMt then
CollectSingleObjectReferenceInMemory(strName ..".[userdata:metatable]", cMt, cDumpInfoContainer)
end
elseif "string" == strType then
-- Check if the specified object.
if cExistTag[cObject] and (not cNameAllAlias[strName]) then
cNameAllAlias[strName] = true
end
-- Add reference and name.
if cAccessTag[cObject] then
return
end
-- Get this name.
cAccessTag[cObject] = true
else
-- For "number" and "boolean" type, they are not object type, skip.
end
end
-- The base method to dump a mem ref info result into a file.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- strRootObjectName - The header info to show the root object name, can be nil.
-- cRootObject - The header info to show the root object address, can be nil.
-- cDumpInfoResultsBase - The base dumped mem info result, nil means no compare and only output cDumpInfoResults, otherwise to compare with cDumpInfoResults.
-- cDumpInfoResults - The compared dumped mem info result, dump itself only if cDumpInfoResultsBase is nil, otherwise dump compared results with cDumpInfoResultsBase.
local function OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, strRootObjectName, cRootObject, cDumpInfoResultsBase, cDumpInfoResults)
-- Check results.
if not cDumpInfoResults then
return
end
-- Get time format string.
local strDateTime = FormatDateTimeNow()
-- Collect memory info.
local cRefInfoBase = (cDumpInfoResultsBase and cDumpInfoResultsBase.m_cObjectReferenceCount) or nil
local cNameInfoBase = (cDumpInfoResultsBase and cDumpInfoResultsBase.m_cObjectAddressToName) or nil
local cRefInfo = cDumpInfoResults.m_cObjectReferenceCount
local cNameInfo = cDumpInfoResults.m_cObjectAddressToName
-- Create a cache result to sort by ref count.
local cRes = {}
local nIdx = 0
for k in pairs(cRefInfo) do
nIdx = nIdx + 1
cRes[nIdx] = k
end
-- Sort result.
table.sort(cRes, function (l, r)
return cRefInfo[l] > cRefInfo[r]
end)
-- Save result to file.
local bOutputFile = strSavePath and (string.len(strSavePath) > 0)
local cOutputHandle = nil
local cOutputEntry = print
if bOutputFile then
-- Check save path affix.
local strAffix = string.sub(strSavePath, -1)
if ("/" ~= strAffix) and ("\\" ~= strAffix) then
strSavePath = strSavePath .. "/"
end
-- Combine file name.
local strFileName = strSavePath .. "LuaMemRefInfo-All"
if (not strExtraFileName) or (0 == string.len(strExtraFileName)) then
if cDumpInfoResultsBase then
if cConfig.m_bComparedMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "].txt"
else
strFileName = strFileName .. ".txt"
end
else
if cConfig.m_bAllMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "].txt"
else
strFileName = strFileName .. ".txt"
end
end
else
if cDumpInfoResultsBase then
if cConfig.m_bComparedMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"
else
strFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"
end
else
if cConfig.m_bAllMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"
else
strFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"
end
end
end
local cFile = assert(io.open(strFileName, "w"))
cOutputHandle = cFile
cOutputEntry = cFile.write
end
local cOutputer = function (strContent)
if cOutputHandle then
cOutputEntry(cOutputHandle, strContent)
else
cOutputEntry(strContent)
end
end
-- Write table header.
if cDumpInfoResultsBase then
cOutputer("--------------------------------------------------------\n")
cOutputer("-- This is compared memory information.\n")
cOutputer("--------------------------------------------------------\n")
cOutputer("-- Collect base memory reference at line:" .. tostring(cDumpInfoResultsBase.m_nCurrentLine) .. "@file:" .. cDumpInfoResultsBase.m_strShortSrc .. "\n")
cOutputer("-- Collect compared memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n")
else
cOutputer("--------------------------------------------------------\n")
cOutputer("-- Collect memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n")
end
cOutputer("--------------------------------------------------------\n")
cOutputer("-- [Table/Function/String Address/Name]\t[Reference Path]\t[Reference Count]\n")
cOutputer("--------------------------------------------------------\n")
if strRootObjectName and cRootObject then
if "string" == type(cRootObject) then
cOutputer("-- From Root Object: \"" .. tostring(cRootObject) .. "\" (" .. strRootObjectName .. ")\n")
else
cOutputer("-- From Root Object: " .. GetOriginalToStringResult(cRootObject) .. " (" .. strRootObjectName .. ")\n")
end
end
-- Save each info.
for i, v in ipairs(cRes) do
if (not cDumpInfoResultsBase) or (not cRefInfoBase[v]) then
if (nMaxRescords > 0) then
if (i <= nMaxRescords) then
if "string" == type(v) then
local strOrgString = tostring(v)
local nPattenBegin, nPattenEnd = string.find(strOrgString, "string: \".*\"")
if ((not cDumpInfoResultsBase) and ((nil == nPattenBegin) or (nil == nPattenEnd))) then
local strRepString = string.gsub(strOrgString, "([\n\r])", "\\n")
cOutputer("string: \"" .. strRepString .. "\"\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
else
cOutputer(tostring(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
end
else
cOutputer(GetOriginalToStringResult(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
end
end
else
if "string" == type(v) then
local strOrgString = tostring(v)
local nPattenBegin, nPattenEnd = string.find(strOrgString, "string: \".*\"")
if ((not cDumpInfoResultsBase) and ((nil == nPattenBegin) or (nil == nPattenEnd))) then
local strRepString = string.gsub(strOrgString, "([\n\r])", "\\n")
cOutputer("string: \"" .. strRepString .. "\"\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
else
cOutputer(tostring(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
end
else
cOutputer(GetOriginalToStringResult(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")
end
end
end
end
if bOutputFile then
io.close(cOutputHandle)
cOutputHandle = nil
end
end
-- The base method to dump a mem ref info result of a single object into a file.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- cDumpInfoResults - The dumped results.
local function OutputMemorySnapshotSingleObject(strSavePath, strExtraFileName, nMaxRescords, cDumpInfoResults)
-- Check results.
if not cDumpInfoResults then
return
end
-- Get time format string.
local strDateTime = FormatDateTimeNow()
-- Collect memory info.
local cObjectAliasName = cDumpInfoResults.m_cObjectAliasName
-- Save result to file.
local bOutputFile = strSavePath and (string.len(strSavePath) > 0)
local cOutputHandle = nil
local cOutputEntry = print
if bOutputFile then
-- Check save path affix.
local strAffix = string.sub(strSavePath, -1)
if ("/" ~= strAffix) and ("\\" ~= strAffix) then
strSavePath = strSavePath .. "/"
end
-- Combine file name.
local strFileName = strSavePath .. "LuaMemRefInfo-Single"
if (not strExtraFileName) or (0 == string.len(strExtraFileName)) then
if cConfig.m_bSingleMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "].txt"
else
strFileName = strFileName .. ".txt"
end
else
if cConfig.m_bSingleMemoryRefFileAddTime then
strFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"
else
strFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"
end
end
local cFile = assert(io.open(strFileName, "w"))
cOutputHandle = cFile
cOutputEntry = cFile.write
end
local cOutputer = function (strContent)
if cOutputHandle then
cOutputEntry(cOutputHandle, strContent)
else
cOutputEntry(strContent)
end
end
-- Write table header.
cOutputer("--------------------------------------------------------\n")
cOutputer("-- Collect single object memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n")
cOutputer("--------------------------------------------------------\n")
-- Calculate reference count.
local nCount = 0
for k in pairs(cObjectAliasName) do
nCount = nCount + 1
end
-- Output reference count.
cOutputer("-- For Object: " .. cDumpInfoResults.m_strAddressName .. " (" .. cDumpInfoResults.m_strObjectName .. "), have " .. tostring(nCount) .. " reference in total.\n")
cOutputer("--------------------------------------------------------\n")
-- Save each info.
for k in pairs(cObjectAliasName) do
if (nMaxRescords > 0) then
if (i <= nMaxRescords) then
cOutputer(k .. "\n")
end
else
cOutputer(k .. "\n")
end
end
if bOutputFile then
io.close(cOutputHandle)
cOutputHandle = nil
end
end
-- Fileter an existing result file and output it.
-- strFilePath - The existing result file.
-- strFilter - The filter string.
-- bIncludeFilter - Include(true) or exclude(false) the filter.
-- bOutputFile - Output to file(true) or console(false).
local function OutputFilteredResult(strFilePath, strFilter, bIncludeFilter, bOutputFile)
if (not strFilePath) or (0 == string.len(strFilePath)) then
print("You need to specify a file path.")
return
end
if (not strFilter) or (0 == string.len(strFilter)) then
print("You need to specify a filter string.")
return
end
-- Read file.
local cFilteredResult = {}
local cReadFile = assert(io.open(strFilePath, "rb"))
for strLine in cReadFile:lines() do
local nBegin, nEnd = string.find(strLine, strFilter)
if nBegin and nEnd then
if bIncludeFilter then
nBegin, nEnd = string.find(strLine, "[\r\n]")
if nBegin and nEnd and (string.len(strLine) == nEnd) then
table.insert(cFilteredResult, string.sub(strLine, 1, nBegin - 1))
else
table.insert(cFilteredResult, strLine)
end
end
else
if not bIncludeFilter then
nBegin, nEnd = string.find(strLine, "[\r\n]")
if nBegin and nEnd and (string.len(strLine) == nEnd) then
table.insert(cFilteredResult, string.sub(strLine, 1, nBegin - 1))
else
table.insert(cFilteredResult, strLine)
end
end
end
end
-- Close and clear read file handle.
io.close(cReadFile)
cReadFile = nil
-- Write filtered result.
local cOutputHandle = nil
local cOutputEntry = print
if bOutputFile then
-- Combine file name.
local _, _, strResFileName = string.find(strFilePath, "(.*)%.txt")
strResFileName = strResFileName .. "-Filter-" .. ((bIncludeFilter and "I") or "E") .. "-[" .. strFilter .. "].txt"
local cFile = assert(io.open(strResFileName, "w"))
cOutputHandle = cFile
cOutputEntry = cFile.write
end
local cOutputer = function (strContent)
if cOutputHandle then
cOutputEntry(cOutputHandle, strContent)
else
cOutputEntry(strContent)
end
end
-- Output result.
for i, v in ipairs(cFilteredResult) do
cOutputer(v .. "\n")
end
if bOutputFile then
io.close(cOutputHandle)
cOutputHandle = nil
end
end
-- Dump memory reference at current time.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- strRootObjectName - The root object name that start to search, default is "_G" if leave this to nil.
-- cRootObject - The root object that start to search, default is _G if leave this to nil.
local function DumpMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, strRootObjectName, cRootObject)
-- Get time format string.
local strDateTime = FormatDateTimeNow()
-- Check root object.
if cRootObject then
if (not strRootObjectName) or (0 == string.len(strRootObjectName)) then
strRootObjectName = tostring(cRootObject)
end
else
cRootObject = debug.getregistry()
strRootObjectName = "registry"
end
-- Create container.
local cDumpInfoContainer = CreateObjectReferenceInfoContainer()
local cStackInfo = debug.getinfo(2, "Sl")
if cStackInfo then
cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src
cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline
end
-- Collect memory info.
CollectObjectReferenceInMemory(strRootObjectName, cRootObject, cDumpInfoContainer)
-- Dump the result.
OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, strRootObjectName, cRootObject, nil, cDumpInfoContainer)
end
-- Dump compared memory reference results generated by DumpMemorySnapshot.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- cResultBefore - The base dumped results.
-- cResultAfter - The compared dumped results.
local function DumpMemorySnapshotCompared(strSavePath, strExtraFileName, nMaxRescords, cResultBefore, cResultAfter)
-- Dump the result.
OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, nil, nil, cResultBefore, cResultAfter)
end
-- Dump compared memory reference file results generated by DumpMemorySnapshot.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- strResultFilePathBefore - The base dumped results file.
-- strResultFilePathAfter - The compared dumped results file.
local function DumpMemorySnapshotComparedFile(strSavePath, strExtraFileName, nMaxRescords, strResultFilePathBefore, strResultFilePathAfter)
-- Read results from file.
local cResultBefore = CreateObjectReferenceInfoContainerFromFile(strResultFilePathBefore)
local cResultAfter = CreateObjectReferenceInfoContainerFromFile(strResultFilePathAfter)
-- Dump the result.
OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, nil, nil, cResultBefore, cResultAfter)
end
-- Dump memory reference of a single object at current time.
-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does.
-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "".
-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result.
-- strObjectName - The object name reference you want to dump.
-- cObject - The object reference you want to dump.
local function DumpMemorySnapshotSingleObject(strSavePath, strExtraFileName, nMaxRescords, strObjectName, cObject)
-- Check object.
if not cObject then
return
end
if (not strObjectName) or (0 == string.len(strObjectName)) then
strObjectName = GetOriginalToStringResult(cObject)
end
-- Get time format string.
local strDateTime = FormatDateTimeNow()
-- Create container.
local cDumpInfoContainer = CreateSingleObjectReferenceInfoContainer(strObjectName, cObject)
local cStackInfo = debug.getinfo(2, "Sl")
if cStackInfo then
cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src
cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline
end
-- Collect memory info.
CollectSingleObjectReferenceInMemory("registry", debug.getregistry(), cDumpInfoContainer)
-- Dump the result.
OutputMemorySnapshotSingleObject(strSavePath, strExtraFileName, nMaxRescords, cDumpInfoContainer)
end
-- Return methods.
local cPublications = {m_cConfig = nil, m_cMethods = {}, m_cHelpers = {}, m_cBases = {}}
cPublications.m_cConfig = cConfig
cPublications.m_cMethods.DumpMemorySnapshot = DumpMemorySnapshot
cPublications.m_cMethods.DumpMemorySnapshotCompared = DumpMemorySnapshotCompared
cPublications.m_cMethods.DumpMemorySnapshotComparedFile = DumpMemorySnapshotComparedFile
cPublications.m_cMethods.DumpMemorySnapshotSingleObject = DumpMemorySnapshotSingleObject
cPublications.m_cHelpers.FormatDateTimeNow = FormatDateTimeNow
cPublications.m_cHelpers.GetOriginalToStringResult = GetOriginalToStringResult
cPublications.m_cBases.CreateObjectReferenceInfoContainer = CreateObjectReferenceInfoContainer
cPublications.m_cBases.CreateObjectReferenceInfoContainerFromFile = CreateObjectReferenceInfoContainerFromFile
cPublications.m_cBases.CreateSingleObjectReferenceInfoContainer = CreateSingleObjectReferenceInfoContainer
cPublications.m_cBases.CollectObjectReferenceInMemory = CollectObjectReferenceInMemory
cPublications.m_cBases.CollectSingleObjectReferenceInMemory = CollectSingleObjectReferenceInMemory
cPublications.m_cBases.OutputMemorySnapshot = OutputMemorySnapshot
cPublications.m_cBases.OutputMemorySnapshotSingleObject = OutputMemorySnapshotSingleObject
cPublications.m_cBases.OutputFilteredResult = OutputFilteredResult
return cPublications