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 runfunction 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 screenCursor1 = {line=1, pos=1} -- position of cursorScreen_bottom1 = {line=1, pos=1} -- position of start of screen line at bottom of screenSelection1 = {}Old_cursor1, Old_selection1, Mousepress_shift = nil -- some extra state to compute selection between mousepress and mousereleaseRecent_mouse = {} -- when selecting text, avoid recomputing some state on every single frameCursor_x, Cursor_y = 0, 0 -- in pixelsCurrent_drawing_mode = 'line'Previous_drawing_mode = nil-- values for testsFont_height = 14Line_height = 15Margin_top = 15Filename = love.filesystem.getUserDirectory()..'/lines.txt'-- undoHistory = {}Next_history = 1-- searchSearch_term = nilSearch_text = nilSearch_backup = nil -- stuff to restore when cancelling search-- resizeLast_resize_time = nil-- blinking cursorCursor_time = 0Initialize_done = falseBefore_done = falsemri.m_cConfig.m_bAllMemoryRefFileAddTime = falseend -- App.initialize_globalsfunction App.initialize(arg)love.keyboard.setTextInput(true) -- bring up keyboard on touch screenlove.keyboard.setKeyRepeat(true)if arg[1] == '-geometry' theninitialize_window_geometry(arg[2])table.remove(arg, 2)table.remove(arg, 1)elseinitialize_window_geometry()endinitialize_font_settings(20)if #arg > 0 thenFilename = arg[1]endprint('init', collectgarbage('count'))Lines = load_from_disk(Filename)print('load_from_disk', collectgarbage('count'))for i,line in ipairs(Lines) doif line.mode == 'text' thenCursor1.line = ibreakendendlove.window.setTitle('lines.love - '..Filename)if #arg > 1 thenprint('ignoring commandline args after '..arg[1])endInitialize_done = true--? if rawget(_G, 'jit') then--? jit.off()--? jit.flush()--? endend -- App.initializefunction initialize_window_geometry(geometry_spec)local geometry_initializedif geometry_spec thengeometry_initialized = parse_geometry_spec(geometry_spec)endif not geometry_initialized then-- maximize windowlove.window.setMode(0, 0) -- maximizeApp.screen.width, App.screen.height, App.screen.flags = love.window.getMode()-- shrink slightly to account for window decorationApp.screen.width = App.screen.width-100App.screen.height = App.screen.height-100endApp.screen.flags.resizable = trueApp.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)endfunction parse_geometry_spec(geometry_spec)local width, height, x, y = geometry_spec:match('(%d+)x(%d+)%+(%d+)%+(%d+)')if width == nil thenprint('invalid geometry spec: '..geometry_spec)print('expected format: {width}x{height}+{x}+{y}')return falseendApp.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 trueendfunction love.resize(w, h)--? print(("Window resized to width: %d and height: %d."):format(w, h))App.screen.width, App.screen.height = w, hLine_width = math.min(40*App.width(Em), App.screen.width-50)Text.redraw_all()Last_resize_time = love.timer.getTime()endfunction initialize_font_settings(font_height)Font_height = font_heightlove.graphics.setFont(love.graphics.newFont(Font_height))Line_height = math.floor(font_height*1.3)-- maximum width available to either text or drawings, in pixelsEm = App.newText(love.graphics.getFont(), 'm')-- readable text width is 50-75 charsLine_width = math.min(40*App.width(Em), App.screen.width-50)endfunction App.filedropped(file)App.initialize_globals() -- in particular, forget all undo historyFilename = file:getFilename()file:open('r')Lines = load_from_file(file)file:close()for i,line in ipairs(Lines) doif line.mode == 'text' thenCursor1.line = ibreakendendlove.window.setTitle('Text with Lines - '..Filename)endframe_index = 0function App.draw()frame_index = frame_index+1if frame_index % 10 == 0 thenprint(frame_index)endButton_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 resizingif Last_resize_time thenif love.timer.getTime() - Last_resize_time < 0.1 thenreturnelseLast_resize_time = nilendendassert(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 thenScreen_bottom1.line = line_indexif line.mode == 'text' and line.data == '' thenline.y = ybutton('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 thenCursor1.line = Cursor1.line+1endend})if Search_term == nil thenif line_index == Cursor1.line thenText.draw_cursor(25, y)endendScreen_bottom1.pos = Screen_top1.posy = y + Line_heightelseif line.mode == 'drawing' theny = y+10 -- paddingline.y = yDrawing.draw(line)y = y + Drawing.pixels(line.h) + 10 -- paddingelse--? print('text')line.y = yy, Screen_bottom1.pos = Text.draw(line, Line_width, line_index)y = y + Line_height--? print('=> y', y)endendend--? print('screen bottom: '..tostring(Screen_bottom1.pos)..' in '..tostring(Lines[Screen_bottom1.line].data))if Search_term thenText.draw_search_bar()endif Initialize_done thenif not Before_done thenBefore_done = trueprint('before', collectgarbage('count'))collectgarbage('collect')mri.m_cMethods.DumpMemorySnapshot('./', '0', -1)frame_index = 0elseif frame_index == 1000 thenprint('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)endendendfunction App.update(dt)Cursor_time = Cursor_time + dt-- some hysteresis while resizingif Last_resize_time thenif love.timer.getTime() - Last_resize_time < 0.1 thenreturnelseLast_resize_time = nilendendDrawing.update(dt)endfunction App.mousepressed(x,y, mouse_button)if Search_term then return endpropagate_to_button_handlers(x,y, mouse_button)for line_index,line in ipairs(Lines) doif line.mode == 'text' thenif 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 stateOld_cursor1 = Cursor1Old_selection1 = Selection1Mousepress_shift = App.shift_down()Selection1 = {line=line_index, pos=Text.to_pos_on_line(line, x, y)}endelseif line.mode == 'drawing' thenif Drawing.in_drawing(line, x, y) thenDrawing.mouse_pressed(line, x,y, button)endendendendfunction App.mousereleased(x,y, button)if Search_term then return endif Lines.current_drawing thenDrawing.mouse_released(x,y, button)elsefor line_index,line in ipairs(Lines) doif line.mode == 'text' thenif Text.in_line(line, x,y) thenCursor1 = {line=line_index, pos=Text.to_pos_on_line(line, x, y)}if Mousepress_shift thenif Old_selection1.line == nil thenSelection1 = Old_cursor1elseSelection1 = Old_selection1endendOld_cursor1, Old_selection1, Mousepress_shift = nilendendend--? print('select:', Selection1.line, Selection1.pos)endendfunction App.textinput(t)for _,line in ipairs(Lines) do line.y = nil end -- just in case we scrollif Search_term thenSearch_term = Search_term..tSearch_text = nilText.search_next()elseif Current_drawing_mode == 'name' thenlocal drawing = Lines.current_drawinglocal p = drawing.points[drawing.pending.target_point]p.name = p.name..telseText.textinput(t)endsave_to_disk(Lines, Filename)endfunction App.keychord_pressed(chord)if Search_term thenif chord == 'escape' thenSearch_term = nilSearch_text = nilCursor1 = Search_backup.cursorScreen_top1 = Search_backup.screen_topSearch_backup = nilText.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leakselseif chord == 'return' thenSearch_term = nilSearch_text = nilSearch_backup = nilelseif chord == 'backspace' thenlocal 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 = nilelseif chord == 'down' thenCursor1.pos = Cursor1.pos+1Text.search_next()elseif chord == 'up' thenText.search_previous()endreturnelseif chord == 'C-f' thenSearch_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-=' theninitialize_font_settings(Font_height+2)Text.redraw_all()elseif chord == 'C--' theninitialize_font_settings(Font_height-2)Text.redraw_all()elseif chord == 'C-0' theninitialize_font_settings(20)Text.redraw_all()elseif chord == 'C-z' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal event = undo_event()if event thenlocal src = event.beforeScreen_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 leaksendelseif chord == 'C-y' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal event = redo_event()if event thenlocal src = event.afterScreen_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 leaksend-- clipboardelseif chord == 'C-c' thenprint('C-c', collectgarbage('count'))for _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal s = Text.selection()if s thenApp.setClipboardText(s)endelseif chord == 'C-x' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal s = Text.cut_selection()if s thenApp.setClipboardText(s)endsave_to_disk(Lines, Filename)elseif chord == 'C-v' thenfor _,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.linelocal before = snapshot(before_line)local clipboard_data = App.getClipboardText()local num_newlines = 0 -- hack 1for _,code in utf8.codes(clipboard_data) dolocal c = utf8.char(code)if c == '\n' thenText.insert_return()num_newlines = num_newlines+1elseText.insert_at_cursor(c)endend-- hack 1: if we have too many newlines we definitely need to scrollfor i=before_line,Cursor1.line doLines[i].screen_line_starting_pos = nilText.populate_screen_line_starting_pos(i)endif Cursor1.line-Screen_top1.line+1 + num_newlines > App.screen.height/Line_height thenText.snap_cursor_to_bottom_of_screen()end-- hack 2: if we have too much text wrapping we definitely need to scrolllocal 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 thenText.snap_cursor_to_bottom_of_screen()endsave_to_disk(Lines, Filename)record_undo_event({before=before, after=snapshot(before_line, Cursor1.line)})-- dispatch to drawing or textelseif love.mouse.isDown('1') or chord:sub(1,2) == 'C-' then-- DON'T reset line.y hereDrawing.keychord_pressed(chord)elseif chord == 'escape' and love.mouse.isDown('1') thenlocal drawing = Drawing.current_drawing()if drawing thendrawing.pending = {}endelseif chord == 'escape' and not love.mouse.isDown('1') thenfor _,line in ipairs(Lines) doif line.mode == 'drawing' thenline.show_help = falseendendelseif Current_drawing_mode == 'name' thenif chord == 'return' thenCurrent_drawing_mode = Previous_drawing_modePrevious_drawing_mode = nilelselocal drawing = Lines.current_drawinglocal p = drawing.points[drawing.pending.target_point]if chord == 'escape' thenp.name = nilelseif chord == 'backspace' thenlocal len = utf8.len(p.name)local byte_offset = utf8.offset(p.name, len-1)p.name = string.sub(p.name, 1, byte_offset)endendsave_to_disk(Lines, Filename)elsefor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrollText.keychord_pressed(chord)endendfunction 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 strDateTimeend-- Get the string result without overrided __tostring.local function GetOriginalToStringResult(cObject)if not cObject thenreturn ""endlocal cMt = getmetatable(cObject)if not cMt thenreturn tostring(cObject)end-- Check tostring override.local strName = ""local cToString = rawget(cMt, "__tostring")if cToString thenrawset(cMt, "__tostring", nil)strName = tostring(cObject)rawset(cMt, "__tostring", cToString)elsestrName = tostring(cObject)endreturn strNameend-- 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 = cObjectReferenceCountcContainer.m_cObjectAddressToName = cObjectAddressToName-- For stack info.cContainer.m_nStackLevel = -1cContainer.m_strShortSrc = "None"cContainer.m_nCurrentLine = -1return cContainerend-- 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_cObjectReferenceCountlocal cNameInfo = cContainer.m_cObjectAddressToName-- Read each line from file.local cFile = assert(io.open(strFilePath, "rb"))for strLine in cFile:lines() dolocal strHeader = string.sub(strLine, 1, 2)if "--" ~= strHeader thenlocal _, _, strAddr, strName, strRefCount= string.find(strLine, "(.+)\t(.*)\t(%d+)")if strAddr thencRefInfo[strAddr] = strRefCountcNameInfo[strAddr] = strNameendendend-- Close and clear file handler.io.close(cFile)cFile = nilreturn cContainerend-- 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 = cObjectExistTagcContainer.m_cObjectAliasName = cObjectAliasNamecContainer.m_cObjectAccessTag = cObjectAccessTag-- For stack info.cContainer.m_nStackLevel = -1cContainer.m_strShortSrc = "None"cContainer.m_nCurrentLine = -1-- Init with object values.cContainer.m_strObjectName = strObjectNamecContainer.m_strAddressName = (("string" == type(cObject)) and ("\"" .. tostring(cObject) .. "\"")) or GetOriginalToStringResult(cObject)cContainer.m_cObjectExistTag[cObject] = truereturn cContainerend-- 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 thenreturnendif not strName thenstrName = ""end-- Check container.if (not cDumpInfoContainer) thencDumpInfoContainer = CreateObjectReferenceInfoContainer()end-- Check stack.if cDumpInfoContainer.m_nStackLevel > 0 thenlocal cStackInfo = debug.getinfo(cDumpInfoContainer.m_nStackLevel, "Sl")if cStackInfo thencDumpInfoContainer.m_strShortSrc = cStackInfo.short_srccDumpInfoContainer.m_nCurrentLine = cStackInfo.currentlineendcDumpInfoContainer.m_nStackLevel = -1end-- Get ref and name info.local cRefInfoContainer = cDumpInfoContainer.m_cObjectReferenceCountlocal cNameInfoContainer = cDumpInfoContainer.m_cObjectAddressToNamelocal strType = type(cObject)if "table" == strType then-- Check table with class name.if rawget(cObject, "__cname") thenif "string" == type(cObject.__cname) thenstrName = strName .. "[class:" .. cObject.__cname .. "]"endelseif rawget(cObject, "class") thenif "string" == type(cObject.class) thenstrName = strName .. "[class:" .. cObject.class .. "]"endelseif rawget(cObject, "_className") thenif "string" == type(cObject._className) thenstrName = strName .. "[class:" .. cObject._className .. "]"endend-- Check if table is _G.if cObject == _G thenstrName = strName .. "[_G]"end-- Get metatable.local bWeakK = falselocal bWeakV = falselocal cMt = getmetatable(cObject)if cMt then-- Check mode.local strMode = rawget(cMt, "__mode")if strMode thenif "k" == strMode thenbWeakK = trueelseif "v" == strMode thenbWeakV = trueelseif "kv" == strMode thenbWeakK = truebWeakV = trueendendend-- Add reference and name.cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1if cNameInfoContainer[cObject] thenreturnend-- 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 thenif not bWeakK thenCollectObjectReferenceInMemory(strName .. ".[table:key.table]", k, cDumpInfoContainer)endif not bWeakV thenCollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "function" == strKeyType thenif not bWeakK thenCollectObjectReferenceInMemory(strName .. ".[table:key.function]", k, cDumpInfoContainer)endif not bWeakV thenCollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "thread" == strKeyType thenif not bWeakK thenCollectObjectReferenceInMemory(strName .. ".[table:key.thread]", k, cDumpInfoContainer)endif not bWeakV thenCollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "userdata" == strKeyType thenif not bWeakK thenCollectObjectReferenceInMemory(strName .. ".[table:key.userdata]", k, cDumpInfoContainer)endif not bWeakV thenCollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseCollectObjectReferenceInMemory(strName .. "." .. k, v, cDumpInfoContainer)endend-- Dump metatable.if cMt thenCollectObjectReferenceInMemory(strName ..".[metatable]", cMt, cDumpInfoContainer)endelseif "function" == strType then-- Get function info.local cDInfo = debug.getinfo(cObject, "Su")-- Write this info.cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1if cNameInfoContainer[cObject] thenreturnend-- Set name.cNameInfoContainer[cObject] = strName .. "[line:" .. tostring(cDInfo.linedefined) .. "@file:" .. cDInfo.short_src .. "]"-- Get upvalues.local nUpsNum = cDInfo.nupsfor i = 1, nUpsNum dolocal strUpName, cUpValue = debug.getupvalue(cObject, i)local strUpValueType = type(cUpValue)--print(strUpName, cUpValue)if "table" == strUpValueType thenCollectObjectReferenceInMemory(strName .. ".[ups:table:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "function" == strUpValueType thenCollectObjectReferenceInMemory(strName .. ".[ups:function:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "thread" == strUpValueType thenCollectObjectReferenceInMemory(strName .. ".[ups:thread:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "userdata" == strUpValueType thenCollectObjectReferenceInMemory(strName .. ".[ups:userdata:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)endend-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectObjectReferenceInMemory(strName ..".[function:environment]", cEnv, cDumpInfoContainer)endendelseif "thread" == strType then-- Add reference and name.cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1if cNameInfoContainer[cObject] thenreturnend-- Set name.cNameInfoContainer[cObject] = strName-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectObjectReferenceInMemory(strName ..".[thread:environment]", cEnv, cDumpInfoContainer)endend-- Dump metatable.local cMt = getmetatable(cObject)if cMt thenCollectObjectReferenceInMemory(strName ..".[thread:metatable]", cMt, cDumpInfoContainer)endelseif "userdata" == strType then-- Add reference and name.cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1if cNameInfoContainer[cObject] thenreturnend-- Set name.cNameInfoContainer[cObject] = strName-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectObjectReferenceInMemory(strName ..".[userdata:environment]", cEnv, cDumpInfoContainer)endend-- Dump metatable.local cMt = getmetatable(cObject)if cMt thenCollectObjectReferenceInMemory(strName ..".[userdata:metatable]", cMt, cDumpInfoContainer)endelseif "string" == strType then-- Add reference and name.cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1if cNameInfoContainer[cObject] thenreturnend-- 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) .. "]"endend-- 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 thenreturnendif not strName thenstrName = ""end-- Check container.if (not cDumpInfoContainer) thencDumpInfoContainer = CreateObjectReferenceInfoContainer()end-- Check stack.if cDumpInfoContainer.m_nStackLevel > 0 thenlocal cStackInfo = debug.getinfo(cDumpInfoContainer.m_nStackLevel, "Sl")if cStackInfo thencDumpInfoContainer.m_strShortSrc = cStackInfo.short_srccDumpInfoContainer.m_nCurrentLine = cStackInfo.currentlineendcDumpInfoContainer.m_nStackLevel = -1endlocal cExistTag = cDumpInfoContainer.m_cObjectExistTaglocal cNameAllAlias = cDumpInfoContainer.m_cObjectAliasNamelocal cAccessTag = cDumpInfoContainer.m_cObjectAccessTaglocal strType = type(cObject)if "table" == strType then-- Check table with class name.if rawget(cObject, "__cname") thenif "string" == type(cObject.__cname) thenstrName = strName .. "[class:" .. cObject.__cname .. "]"endelseif rawget(cObject, "class") thenif "string" == type(cObject.class) thenstrName = strName .. "[class:" .. cObject.class .. "]"endelseif rawget(cObject, "_className") thenif "string" == type(cObject._className) thenstrName = strName .. "[class:" .. cObject._className .. "]"endend-- Check if table is _G.if cObject == _G thenstrName = strName .. "[_G]"end-- Get metatable.local bWeakK = falselocal bWeakV = falselocal cMt = getmetatable(cObject)if cMt then-- Check mode.local strMode = rawget(cMt, "__mode")if strMode thenif "k" == strMode thenbWeakK = trueelseif "v" == strMode thenbWeakV = trueelseif "kv" == strMode thenbWeakK = truebWeakV = trueendendend-- Check if the specified object.if cExistTag[cObject] and (not cNameAllAlias[strName]) thencNameAllAlias[strName] = trueend-- Add reference and name.if cAccessTag[cObject] thenreturnend-- 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 thenif not bWeakK thenCollectSingleObjectReferenceInMemory(strName .. ".[table:key.table]", k, cDumpInfoContainer)endif not bWeakV thenCollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "function" == strKeyType thenif not bWeakK thenCollectSingleObjectReferenceInMemory(strName .. ".[table:key.function]", k, cDumpInfoContainer)endif not bWeakV thenCollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "thread" == strKeyType thenif not bWeakK thenCollectSingleObjectReferenceInMemory(strName .. ".[table:key.thread]", k, cDumpInfoContainer)endif not bWeakV thenCollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "userdata" == strKeyType thenif not bWeakK thenCollectSingleObjectReferenceInMemory(strName .. ".[table:key.userdata]", k, cDumpInfoContainer)endif not bWeakV thenCollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseCollectSingleObjectReferenceInMemory(strName .. "." .. k, v, cDumpInfoContainer)endend-- Dump metatable.if cMt thenCollectSingleObjectReferenceInMemory(strName ..".[metatable]", cMt, cDumpInfoContainer)endelseif "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]) thencNameAllAlias[cCombinedName] = trueend-- Write this info.if cAccessTag[cObject] thenreturnend-- Set name.cAccessTag[cObject] = true-- Get upvalues.local nUpsNum = cDInfo.nupsfor i = 1, nUpsNum dolocal strUpName, cUpValue = debug.getupvalue(cObject, i)local strUpValueType = type(cUpValue)--print(strUpName, cUpValue)if "table" == strUpValueType thenCollectSingleObjectReferenceInMemory(strName .. ".[ups:table:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "function" == strUpValueType thenCollectSingleObjectReferenceInMemory(strName .. ".[ups:function:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "thread" == strUpValueType thenCollectSingleObjectReferenceInMemory(strName .. ".[ups:thread:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "userdata" == strUpValueType thenCollectSingleObjectReferenceInMemory(strName .. ".[ups:userdata:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)endend-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectSingleObjectReferenceInMemory(strName ..".[function:environment]", cEnv, cDumpInfoContainer)endendelseif "thread" == strType then-- Check if the specified object.if cExistTag[cObject] and (not cNameAllAlias[strName]) thencNameAllAlias[strName] = trueend-- Add reference and name.if cAccessTag[cObject] thenreturnend-- Get this name.cAccessTag[cObject] = true-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectSingleObjectReferenceInMemory(strName ..".[thread:environment]", cEnv, cDumpInfoContainer)endend-- Dump metatable.local cMt = getmetatable(cObject)if cMt thenCollectSingleObjectReferenceInMemory(strName ..".[thread:metatable]", cMt, cDumpInfoContainer)endelseif "userdata" == strType then-- Check if the specified object.if cExistTag[cObject] and (not cNameAllAlias[strName]) thencNameAllAlias[strName] = trueend-- Add reference and name.if cAccessTag[cObject] thenreturnend-- Get this name.cAccessTag[cObject] = true-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectSingleObjectReferenceInMemory(strName ..".[userdata:environment]", cEnv, cDumpInfoContainer)endend-- Dump metatable.local cMt = getmetatable(cObject)if cMt thenCollectSingleObjectReferenceInMemory(strName ..".[userdata:metatable]", cMt, cDumpInfoContainer)endelseif "string" == strType then-- Check if the specified object.if cExistTag[cObject] and (not cNameAllAlias[strName]) thencNameAllAlias[strName] = trueend-- Add reference and name.if cAccessTag[cObject] thenreturnend-- Get this name.cAccessTag[cObject] = trueelse-- For "number" and "boolean" type, they are not object type, skip.endend-- 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 thenreturnend-- Get time format string.local strDateTime = FormatDateTimeNow()-- Collect memory info.local cRefInfoBase = (cDumpInfoResultsBase and cDumpInfoResultsBase.m_cObjectReferenceCount) or nillocal cNameInfoBase = (cDumpInfoResultsBase and cDumpInfoResultsBase.m_cObjectAddressToName) or nillocal cRefInfo = cDumpInfoResults.m_cObjectReferenceCountlocal cNameInfo = cDumpInfoResults.m_cObjectAddressToName-- Create a cache result to sort by ref count.local cRes = {}local nIdx = 0for k in pairs(cRefInfo) donIdx = nIdx + 1cRes[nIdx] = kend-- 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 = nillocal cOutputEntry = printif bOutputFile then-- Check save path affix.local strAffix = string.sub(strSavePath, -1)if ("/" ~= strAffix) and ("\\" ~= strAffix) thenstrSavePath = strSavePath .. "/"end-- Combine file name.local strFileName = strSavePath .. "LuaMemRefInfo-All"if (not strExtraFileName) or (0 == string.len(strExtraFileName)) thenif cDumpInfoResultsBase thenif cConfig.m_bComparedMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "].txt"elsestrFileName = strFileName .. ".txt"endelseif cConfig.m_bAllMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "].txt"elsestrFileName = strFileName .. ".txt"endendelseif cDumpInfoResultsBase thenif cConfig.m_bComparedMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"elsestrFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"endelseif cConfig.m_bAllMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"elsestrFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"endendendlocal cFile = assert(io.open(strFileName, "w"))cOutputHandle = cFilecOutputEntry = cFile.writeendlocal cOutputer = function (strContent)if cOutputHandle thencOutputEntry(cOutputHandle, strContent)elsecOutputEntry(strContent)endend-- Write table header.if cDumpInfoResultsBase thencOutputer("--------------------------------------------------------\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")elsecOutputer("--------------------------------------------------------\n")cOutputer("-- Collect memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n")endcOutputer("--------------------------------------------------------\n")cOutputer("-- [Table/Function/String Address/Name]\t[Reference Path]\t[Reference Count]\n")cOutputer("--------------------------------------------------------\n")if strRootObjectName and cRootObject thenif "string" == type(cRootObject) thencOutputer("-- From Root Object: \"" .. tostring(cRootObject) .. "\" (" .. strRootObjectName .. ")\n")elsecOutputer("-- From Root Object: " .. GetOriginalToStringResult(cRootObject) .. " (" .. strRootObjectName .. ")\n")endend-- Save each info.for i, v in ipairs(cRes) doif (not cDumpInfoResultsBase) or (not cRefInfoBase[v]) thenif (nMaxRescords > 0) thenif (i <= nMaxRescords) thenif "string" == type(v) thenlocal strOrgString = tostring(v)local nPattenBegin, nPattenEnd = string.find(strOrgString, "string: \".*\"")if ((not cDumpInfoResultsBase) and ((nil == nPattenBegin) or (nil == nPattenEnd))) thenlocal strRepString = string.gsub(strOrgString, "([\n\r])", "\\n")cOutputer("string: \"" .. strRepString .. "\"\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")elsecOutputer(tostring(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")endelsecOutputer(GetOriginalToStringResult(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")endendelseif "string" == type(v) thenlocal strOrgString = tostring(v)local nPattenBegin, nPattenEnd = string.find(strOrgString, "string: \".*\"")if ((not cDumpInfoResultsBase) and ((nil == nPattenBegin) or (nil == nPattenEnd))) thenlocal strRepString = string.gsub(strOrgString, "([\n\r])", "\\n")cOutputer("string: \"" .. strRepString .. "\"\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")elsecOutputer(tostring(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")endelsecOutputer(GetOriginalToStringResult(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")endendendendif bOutputFile thenio.close(cOutputHandle)cOutputHandle = nilendend-- 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 thenreturnend-- 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 = nillocal cOutputEntry = printif bOutputFile then-- Check save path affix.local strAffix = string.sub(strSavePath, -1)if ("/" ~= strAffix) and ("\\" ~= strAffix) thenstrSavePath = strSavePath .. "/"end-- Combine file name.local strFileName = strSavePath .. "LuaMemRefInfo-Single"if (not strExtraFileName) or (0 == string.len(strExtraFileName)) thenif cConfig.m_bSingleMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "].txt"elsestrFileName = strFileName .. ".txt"endelseif cConfig.m_bSingleMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"elsestrFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"endendlocal cFile = assert(io.open(strFileName, "w"))cOutputHandle = cFilecOutputEntry = cFile.writeendlocal cOutputer = function (strContent)if cOutputHandle thencOutputEntry(cOutputHandle, strContent)elsecOutputEntry(strContent)endend-- 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 = 0for k in pairs(cObjectAliasName) donCount = nCount + 1end-- 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) doif (nMaxRescords > 0) thenif (i <= nMaxRescords) thencOutputer(k .. "\n")endelsecOutputer(k .. "\n")endendif bOutputFile thenio.close(cOutputHandle)cOutputHandle = nilendend-- 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)) thenprint("You need to specify a file path.")returnendif (not strFilter) or (0 == string.len(strFilter)) thenprint("You need to specify a filter string.")returnend-- Read file.local cFilteredResult = {}local cReadFile = assert(io.open(strFilePath, "rb"))for strLine in cReadFile:lines() dolocal nBegin, nEnd = string.find(strLine, strFilter)if nBegin and nEnd thenif bIncludeFilter thennBegin, nEnd = string.find(strLine, "[\r\n]")if nBegin and nEnd and (string.len(strLine) == nEnd) thentable.insert(cFilteredResult, string.sub(strLine, 1, nBegin - 1))elsetable.insert(cFilteredResult, strLine)endendelseif not bIncludeFilter thennBegin, nEnd = string.find(strLine, "[\r\n]")if nBegin and nEnd and (string.len(strLine) == nEnd) thentable.insert(cFilteredResult, string.sub(strLine, 1, nBegin - 1))elsetable.insert(cFilteredResult, strLine)endendendend-- Close and clear read file handle.io.close(cReadFile)cReadFile = nil-- Write filtered result.local cOutputHandle = nillocal cOutputEntry = printif 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 = cFilecOutputEntry = cFile.writeendlocal cOutputer = function (strContent)if cOutputHandle thencOutputEntry(cOutputHandle, strContent)elsecOutputEntry(strContent)endend-- Output result.for i, v in ipairs(cFilteredResult) docOutputer(v .. "\n")endif bOutputFile thenio.close(cOutputHandle)cOutputHandle = nilendend-- 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 thenif (not strRootObjectName) or (0 == string.len(strRootObjectName)) thenstrRootObjectName = tostring(cRootObject)endelsecRootObject = debug.getregistry()strRootObjectName = "registry"end-- Create container.local cDumpInfoContainer = CreateObjectReferenceInfoContainer()local cStackInfo = debug.getinfo(2, "Sl")if cStackInfo thencDumpInfoContainer.m_strShortSrc = cStackInfo.short_srccDumpInfoContainer.m_nCurrentLine = cStackInfo.currentlineend-- 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 thenreturnendif (not strObjectName) or (0 == string.len(strObjectName)) thenstrObjectName = 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 thencDumpInfoContainer.m_strShortSrc = cStackInfo.short_srccDumpInfoContainer.m_nCurrentLine = cStackInfo.currentlineend-- 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 = cConfigcPublications.m_cMethods.DumpMemorySnapshot = DumpMemorySnapshotcPublications.m_cMethods.DumpMemorySnapshotCompared = DumpMemorySnapshotComparedcPublications.m_cMethods.DumpMemorySnapshotComparedFile = DumpMemorySnapshotComparedFilecPublications.m_cMethods.DumpMemorySnapshotSingleObject = DumpMemorySnapshotSingleObjectcPublications.m_cHelpers.FormatDateTimeNow = FormatDateTimeNowcPublications.m_cHelpers.GetOriginalToStringResult = GetOriginalToStringResultcPublications.m_cBases.CreateObjectReferenceInfoContainer = CreateObjectReferenceInfoContainercPublications.m_cBases.CreateObjectReferenceInfoContainerFromFile = CreateObjectReferenceInfoContainerFromFilecPublications.m_cBases.CreateSingleObjectReferenceInfoContainer = CreateSingleObjectReferenceInfoContainercPublications.m_cBases.CollectObjectReferenceInMemory = CollectObjectReferenceInMemorycPublications.m_cBases.CollectSingleObjectReferenceInMemory = CollectSingleObjectReferenceInMemorycPublications.m_cBases.OutputMemorySnapshot = OutputMemorySnapshotcPublications.m_cBases.OutputMemorySnapshotSingleObject = OutputMemorySnapshotSingleObjectcPublications.m_cBases.OutputFilteredResult = OutputFilteredResultreturn 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 strDateTimeend-- Get the string result without overrided __tostring.local function GetOriginalToStringResult(cObject)if not cObject thenreturn ""endlocal cMt = getmetatable(cObject)if not cMt thenreturn tostring(cObject)end-- Check tostring override.local strName = ""local cToString = rawget(cMt, "__tostring")if cToString thenprint('tostring overridden:', tostring(cObject))--? rawset(cMt, "__tostring", nil)--? strName = tostring(cObject)--? rawset(cMt, "__tostring", cToString)--? else--? strName = tostring(cObject)endstrName = tostring(cObject)return strNameend-- 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 = cObjectReferenceCountcContainer.m_cObjectAddressToName = cObjectAddressToName-- For stack info.cContainer.m_nStackLevel = -1cContainer.m_strShortSrc = "None"cContainer.m_nCurrentLine = -1return cContainerend-- 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_cObjectReferenceCountlocal cNameInfo = cContainer.m_cObjectAddressToName-- Read each line from file.local cFile = assert(io.open(strFilePath, "rb"))for strLine in cFile:lines() dolocal strHeader = string.sub(strLine, 1, 2)if "--" ~= strHeader thenlocal _, _, strAddr, strName, strRefCount= string.find(strLine, "(.+)\t(.*)\t(%d+)")if strAddr thencRefInfo[strAddr] = strRefCountcNameInfo[strAddr] = strNameendendend-- Close and clear file handler.io.close(cFile)cFile = nilreturn cContainerend-- 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 = cObjectExistTagcContainer.m_cObjectAliasName = cObjectAliasNamecContainer.m_cObjectAccessTag = cObjectAccessTag-- For stack info.cContainer.m_nStackLevel = -1cContainer.m_strShortSrc = "None"cContainer.m_nCurrentLine = -1-- Init with object values.cContainer.m_strObjectName = strObjectNamecContainer.m_strAddressName = (("string" == type(cObject)) and ("\"" .. tostring(cObject) .. "\"")) or GetOriginalToStringResult(cObject)cContainer.m_cObjectExistTag[cObject] = truereturn cContainerend-- 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 thenreturnendif not strName thenstrName = ""end-- Check container.if (not cDumpInfoContainer) thencDumpInfoContainer = CreateObjectReferenceInfoContainer()end-- Check stack.if cDumpInfoContainer.m_nStackLevel > 0 thenlocal cStackInfo = debug.getinfo(cDumpInfoContainer.m_nStackLevel, "Sl")if cStackInfo thencDumpInfoContainer.m_strShortSrc = cStackInfo.short_srccDumpInfoContainer.m_nCurrentLine = cStackInfo.currentlineendcDumpInfoContainer.m_nStackLevel = -1end-- Get ref and name info.local cRefInfoContainer = cDumpInfoContainer.m_cObjectReferenceCountlocal cNameInfoContainer = cDumpInfoContainer.m_cObjectAddressToNamelocal strType = type(cObject)if "table" == strType then-- Check table with class name.if rawget(cObject, "__cname") thenif "string" == type(cObject.__cname) thenstrName = strName .. "[class:" .. cObject.__cname .. "]"endelseif rawget(cObject, "class") thenif "string" == type(cObject.class) thenstrName = strName .. "[class:" .. cObject.class .. "]"endelseif rawget(cObject, "_className") thenif "string" == type(cObject._className) thenstrName = strName .. "[class:" .. cObject._className .. "]"endend-- Check if table is _G.if cObject == _G thenstrName = strName .. "[_G]"end-- Get metatable.local bWeakK = falselocal bWeakV = falselocal cMt = getmetatable(cObject)if cMt then-- Check mode.local strMode = rawget(cMt, "__mode")if strMode thenif "k" == strMode thenbWeakK = trueelseif "v" == strMode thenbWeakV = trueelseif "kv" == strMode thenbWeakK = truebWeakV = trueendendend-- Add reference and name.cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1if cNameInfoContainer[cObject] thenreturnend-- 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 thenif not bWeakK thenCollectObjectReferenceInMemory(strName .. ".[table:key.table]", k, cDumpInfoContainer)endif not bWeakV thenCollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "function" == strKeyType thenif not bWeakK thenCollectObjectReferenceInMemory(strName .. ".[table:key.function]", k, cDumpInfoContainer)endif not bWeakV thenCollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "thread" == strKeyType thenif not bWeakK thenCollectObjectReferenceInMemory(strName .. ".[table:key.thread]", k, cDumpInfoContainer)endif not bWeakV thenCollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "userdata" == strKeyType thenif not bWeakK thenCollectObjectReferenceInMemory(strName .. ".[table:key.userdata]", k, cDumpInfoContainer)endif not bWeakV thenCollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseCollectObjectReferenceInMemory(strName .. "." .. tostring(k), v, cDumpInfoContainer)endend-- Dump metatable.if cMt thenCollectObjectReferenceInMemory(strName ..".[metatable]", cMt, cDumpInfoContainer)endelseif "function" == strType then-- Get function info.local cDInfo = debug.getinfo(cObject, "Su")-- Write this info.cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1if cNameInfoContainer[cObject] thenreturnend-- Set name.cNameInfoContainer[cObject] = strName .. "[line:" .. tostring(cDInfo.linedefined) .. "@file:" .. cDInfo.short_src .. "]"-- Get upvalues.local nUpsNum = cDInfo.nupsfor i = 1, nUpsNum dolocal strUpName, cUpValue = debug.getupvalue(cObject, i)local strUpValueType = type(cUpValue)--print(strUpName, cUpValue)if "table" == strUpValueType thenCollectObjectReferenceInMemory(strName .. ".[ups:table:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "function" == strUpValueType thenCollectObjectReferenceInMemory(strName .. ".[ups:function:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "thread" == strUpValueType thenCollectObjectReferenceInMemory(strName .. ".[ups:thread:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "userdata" == strUpValueType thenCollectObjectReferenceInMemory(strName .. ".[ups:userdata:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)endend-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectObjectReferenceInMemory(strName ..".[function:environment]", cEnv, cDumpInfoContainer)endendelseif "thread" == strType then-- Add reference and name.cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1if cNameInfoContainer[cObject] thenreturnend-- Set name.cNameInfoContainer[cObject] = strName-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectObjectReferenceInMemory(strName ..".[thread:environment]", cEnv, cDumpInfoContainer)endend-- Dump metatable.local cMt = getmetatable(cObject)if cMt thenCollectObjectReferenceInMemory(strName ..".[thread:metatable]", cMt, cDumpInfoContainer)endelseif "userdata" == strType then-- Add reference and name.cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1if cNameInfoContainer[cObject] thenreturnend-- Set name.cNameInfoContainer[cObject] = strName-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectObjectReferenceInMemory(strName ..".[userdata:environment]", cEnv, cDumpInfoContainer)endend-- Dump metatable.local cMt = getmetatable(cObject)if cMt thenCollectObjectReferenceInMemory(strName ..".[userdata:metatable]", cMt, cDumpInfoContainer)endelseif "string" == strType then-- Add reference and name.cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1if cNameInfoContainer[cObject] thenreturnend-- 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) .. "]"endend-- 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 thenreturnendif not strName thenstrName = ""end-- Check container.if (not cDumpInfoContainer) thencDumpInfoContainer = CreateObjectReferenceInfoContainer()end-- Check stack.if cDumpInfoContainer.m_nStackLevel > 0 thenlocal cStackInfo = debug.getinfo(cDumpInfoContainer.m_nStackLevel, "Sl")if cStackInfo thencDumpInfoContainer.m_strShortSrc = cStackInfo.short_srccDumpInfoContainer.m_nCurrentLine = cStackInfo.currentlineendcDumpInfoContainer.m_nStackLevel = -1endlocal cExistTag = cDumpInfoContainer.m_cObjectExistTaglocal cNameAllAlias = cDumpInfoContainer.m_cObjectAliasNamelocal cAccessTag = cDumpInfoContainer.m_cObjectAccessTaglocal strType = type(cObject)if "table" == strType then-- Check table with class name.if rawget(cObject, "__cname") thenif "string" == type(cObject.__cname) thenstrName = strName .. "[class:" .. cObject.__cname .. "]"endelseif rawget(cObject, "class") thenif "string" == type(cObject.class) thenstrName = strName .. "[class:" .. cObject.class .. "]"endelseif rawget(cObject, "_className") thenif "string" == type(cObject._className) thenstrName = strName .. "[class:" .. cObject._className .. "]"endend-- Check if table is _G.if cObject == _G thenstrName = strName .. "[_G]"end-- Get metatable.local bWeakK = falselocal bWeakV = falselocal cMt = getmetatable(cObject)if cMt then-- Check mode.local strMode = rawget(cMt, "__mode")if strMode thenif "k" == strMode thenbWeakK = trueelseif "v" == strMode thenbWeakV = trueelseif "kv" == strMode thenbWeakK = truebWeakV = trueendendend-- Check if the specified object.if cExistTag[cObject] and (not cNameAllAlias[strName]) thencNameAllAlias[strName] = trueend-- Add reference and name.if cAccessTag[cObject] thenreturnend-- 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 thenif not bWeakK thenCollectSingleObjectReferenceInMemory(strName .. ".[table:key.table]", k, cDumpInfoContainer)endif not bWeakV thenCollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "function" == strKeyType thenif not bWeakK thenCollectSingleObjectReferenceInMemory(strName .. ".[table:key.function]", k, cDumpInfoContainer)endif not bWeakV thenCollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "thread" == strKeyType thenif not bWeakK thenCollectSingleObjectReferenceInMemory(strName .. ".[table:key.thread]", k, cDumpInfoContainer)endif not bWeakV thenCollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseif "userdata" == strKeyType thenif not bWeakK thenCollectSingleObjectReferenceInMemory(strName .. ".[table:key.userdata]", k, cDumpInfoContainer)endif not bWeakV thenCollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer)endelseCollectSingleObjectReferenceInMemory(strName .. "." .. k, v, cDumpInfoContainer)endend-- Dump metatable.if cMt thenCollectSingleObjectReferenceInMemory(strName ..".[metatable]", cMt, cDumpInfoContainer)endelseif "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]) thencNameAllAlias[cCombinedName] = trueend-- Write this info.if cAccessTag[cObject] thenreturnend-- Set name.cAccessTag[cObject] = true-- Get upvalues.local nUpsNum = cDInfo.nupsfor i = 1, nUpsNum dolocal strUpName, cUpValue = debug.getupvalue(cObject, i)local strUpValueType = type(cUpValue)--print(strUpName, cUpValue)if "table" == strUpValueType thenCollectSingleObjectReferenceInMemory(strName .. ".[ups:table:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "function" == strUpValueType thenCollectSingleObjectReferenceInMemory(strName .. ".[ups:function:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "thread" == strUpValueType thenCollectSingleObjectReferenceInMemory(strName .. ".[ups:thread:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)elseif "userdata" == strUpValueType thenCollectSingleObjectReferenceInMemory(strName .. ".[ups:userdata:" .. strUpName .. "]", cUpValue, cDumpInfoContainer)endend-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectSingleObjectReferenceInMemory(strName ..".[function:environment]", cEnv, cDumpInfoContainer)endendelseif "thread" == strType then-- Check if the specified object.if cExistTag[cObject] and (not cNameAllAlias[strName]) thencNameAllAlias[strName] = trueend-- Add reference and name.if cAccessTag[cObject] thenreturnend-- Get this name.cAccessTag[cObject] = true-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectSingleObjectReferenceInMemory(strName ..".[thread:environment]", cEnv, cDumpInfoContainer)endend-- Dump metatable.local cMt = getmetatable(cObject)if cMt thenCollectSingleObjectReferenceInMemory(strName ..".[thread:metatable]", cMt, cDumpInfoContainer)endelseif "userdata" == strType then-- Check if the specified object.if cExistTag[cObject] and (not cNameAllAlias[strName]) thencNameAllAlias[strName] = trueend-- Add reference and name.if cAccessTag[cObject] thenreturnend-- Get this name.cAccessTag[cObject] = true-- Dump environment table.local getfenv = debug.getfenvif getfenv thenlocal cEnv = getfenv(cObject)if cEnv thenCollectSingleObjectReferenceInMemory(strName ..".[userdata:environment]", cEnv, cDumpInfoContainer)endend-- Dump metatable.local cMt = getmetatable(cObject)if cMt thenCollectSingleObjectReferenceInMemory(strName ..".[userdata:metatable]", cMt, cDumpInfoContainer)endelseif "string" == strType then-- Check if the specified object.if cExistTag[cObject] and (not cNameAllAlias[strName]) thencNameAllAlias[strName] = trueend-- Add reference and name.if cAccessTag[cObject] thenreturnend-- Get this name.cAccessTag[cObject] = trueelse-- For "number" and "boolean" type, they are not object type, skip.endend-- 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 thenreturnend-- Get time format string.local strDateTime = FormatDateTimeNow()-- Collect memory info.local cRefInfoBase = (cDumpInfoResultsBase and cDumpInfoResultsBase.m_cObjectReferenceCount) or nillocal cNameInfoBase = (cDumpInfoResultsBase and cDumpInfoResultsBase.m_cObjectAddressToName) or nillocal cRefInfo = cDumpInfoResults.m_cObjectReferenceCountlocal cNameInfo = cDumpInfoResults.m_cObjectAddressToName-- Create a cache result to sort by ref count.local cRes = {}local nIdx = 0for k in pairs(cRefInfo) donIdx = nIdx + 1cRes[nIdx] = kend-- 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 = nillocal cOutputEntry = printif bOutputFile then-- Check save path affix.local strAffix = string.sub(strSavePath, -1)if ("/" ~= strAffix) and ("\\" ~= strAffix) thenstrSavePath = strSavePath .. "/"end-- Combine file name.local strFileName = strSavePath .. "LuaMemRefInfo-All"if (not strExtraFileName) or (0 == string.len(strExtraFileName)) thenif cDumpInfoResultsBase thenif cConfig.m_bComparedMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "].txt"elsestrFileName = strFileName .. ".txt"endelseif cConfig.m_bAllMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "].txt"elsestrFileName = strFileName .. ".txt"endendelseif cDumpInfoResultsBase thenif cConfig.m_bComparedMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"elsestrFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"endelseif cConfig.m_bAllMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"elsestrFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"endendendlocal cFile = assert(io.open(strFileName, "w"))cOutputHandle = cFilecOutputEntry = cFile.writeendlocal cOutputer = function (strContent)if cOutputHandle thencOutputEntry(cOutputHandle, strContent)elsecOutputEntry(strContent)endend-- Write table header.if cDumpInfoResultsBase thencOutputer("--------------------------------------------------------\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")elsecOutputer("--------------------------------------------------------\n")cOutputer("-- Collect memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n")endcOutputer("--------------------------------------------------------\n")cOutputer("-- [Table/Function/String Address/Name]\t[Reference Path]\t[Reference Count]\n")cOutputer("--------------------------------------------------------\n")if strRootObjectName and cRootObject thenif "string" == type(cRootObject) thencOutputer("-- From Root Object: \"" .. tostring(cRootObject) .. "\" (" .. strRootObjectName .. ")\n")elsecOutputer("-- From Root Object: " .. GetOriginalToStringResult(cRootObject) .. " (" .. strRootObjectName .. ")\n")endend-- Save each info.for i, v in ipairs(cRes) doif (not cDumpInfoResultsBase) or (not cRefInfoBase[v]) thenif (nMaxRescords > 0) thenif (i <= nMaxRescords) thenif "string" == type(v) thenlocal strOrgString = tostring(v)local nPattenBegin, nPattenEnd = string.find(strOrgString, "string: \".*\"")if ((not cDumpInfoResultsBase) and ((nil == nPattenBegin) or (nil == nPattenEnd))) thenlocal strRepString = string.gsub(strOrgString, "([\n\r])", "\\n")cOutputer("string: \"" .. strRepString .. "\"\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")elsecOutputer(tostring(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")endelsecOutputer(GetOriginalToStringResult(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")endendelseif "string" == type(v) thenlocal strOrgString = tostring(v)local nPattenBegin, nPattenEnd = string.find(strOrgString, "string: \".*\"")if ((not cDumpInfoResultsBase) and ((nil == nPattenBegin) or (nil == nPattenEnd))) thenlocal strRepString = string.gsub(strOrgString, "([\n\r])", "\\n")cOutputer("string: \"" .. strRepString .. "\"\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")elsecOutputer(tostring(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")endelsecOutputer(GetOriginalToStringResult(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n")endendendendif bOutputFile thenio.close(cOutputHandle)cOutputHandle = nilendend-- 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 thenreturnend-- 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 = nillocal cOutputEntry = printif bOutputFile then-- Check save path affix.local strAffix = string.sub(strSavePath, -1)if ("/" ~= strAffix) and ("\\" ~= strAffix) thenstrSavePath = strSavePath .. "/"end-- Combine file name.local strFileName = strSavePath .. "LuaMemRefInfo-Single"if (not strExtraFileName) or (0 == string.len(strExtraFileName)) thenif cConfig.m_bSingleMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "].txt"elsestrFileName = strFileName .. ".txt"endelseif cConfig.m_bSingleMemoryRefFileAddTime thenstrFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt"elsestrFileName = strFileName .. "-[" .. strExtraFileName .. "].txt"endendlocal cFile = assert(io.open(strFileName, "w"))cOutputHandle = cFilecOutputEntry = cFile.writeendlocal cOutputer = function (strContent)if cOutputHandle thencOutputEntry(cOutputHandle, strContent)elsecOutputEntry(strContent)endend-- 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 = 0for k in pairs(cObjectAliasName) donCount = nCount + 1end-- 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) doif (nMaxRescords > 0) thenif (i <= nMaxRescords) thencOutputer(k .. "\n")endelsecOutputer(k .. "\n")endendif bOutputFile thenio.close(cOutputHandle)cOutputHandle = nilendend-- 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)) thenprint("You need to specify a file path.")returnendif (not strFilter) or (0 == string.len(strFilter)) thenprint("You need to specify a filter string.")returnend-- Read file.local cFilteredResult = {}local cReadFile = assert(io.open(strFilePath, "rb"))for strLine in cReadFile:lines() dolocal nBegin, nEnd = string.find(strLine, strFilter)if nBegin and nEnd thenif bIncludeFilter thennBegin, nEnd = string.find(strLine, "[\r\n]")if nBegin and nEnd and (string.len(strLine) == nEnd) thentable.insert(cFilteredResult, string.sub(strLine, 1, nBegin - 1))elsetable.insert(cFilteredResult, strLine)endendelseif not bIncludeFilter thennBegin, nEnd = string.find(strLine, "[\r\n]")if nBegin and nEnd and (string.len(strLine) == nEnd) thentable.insert(cFilteredResult, string.sub(strLine, 1, nBegin - 1))elsetable.insert(cFilteredResult, strLine)endendendend-- Close and clear read file handle.io.close(cReadFile)cReadFile = nil-- Write filtered result.local cOutputHandle = nillocal cOutputEntry = printif 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 = cFilecOutputEntry = cFile.writeendlocal cOutputer = function (strContent)if cOutputHandle thencOutputEntry(cOutputHandle, strContent)elsecOutputEntry(strContent)endend-- Output result.for i, v in ipairs(cFilteredResult) docOutputer(v .. "\n")endif bOutputFile thenio.close(cOutputHandle)cOutputHandle = nilendend-- 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 thenif (not strRootObjectName) or (0 == string.len(strRootObjectName)) thenstrRootObjectName = tostring(cRootObject)endelsecRootObject = debug.getregistry()strRootObjectName = "registry"end-- Create container.local cDumpInfoContainer = CreateObjectReferenceInfoContainer()local cStackInfo = debug.getinfo(2, "Sl")if cStackInfo thencDumpInfoContainer.m_strShortSrc = cStackInfo.short_srccDumpInfoContainer.m_nCurrentLine = cStackInfo.currentlineend-- 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 thenreturnendif (not strObjectName) or (0 == string.len(strObjectName)) thenstrObjectName = 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 thencDumpInfoContainer.m_strShortSrc = cStackInfo.short_srccDumpInfoContainer.m_nCurrentLine = cStackInfo.currentlineend-- 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 = cConfigcPublications.m_cMethods.DumpMemorySnapshot = DumpMemorySnapshotcPublications.m_cMethods.DumpMemorySnapshotCompared = DumpMemorySnapshotComparedcPublications.m_cMethods.DumpMemorySnapshotComparedFile = DumpMemorySnapshotComparedFilecPublications.m_cMethods.DumpMemorySnapshotSingleObject = DumpMemorySnapshotSingleObjectcPublications.m_cHelpers.FormatDateTimeNow = FormatDateTimeNowcPublications.m_cHelpers.GetOriginalToStringResult = GetOriginalToStringResultcPublications.m_cBases.CreateObjectReferenceInfoContainer = CreateObjectReferenceInfoContainercPublications.m_cBases.CreateObjectReferenceInfoContainerFromFile = CreateObjectReferenceInfoContainerFromFilecPublications.m_cBases.CreateSingleObjectReferenceInfoContainer = CreateSingleObjectReferenceInfoContainercPublications.m_cBases.CollectObjectReferenceInMemory = CollectObjectReferenceInMemorycPublications.m_cBases.CollectSingleObjectReferenceInMemory = CollectSingleObjectReferenceInMemorycPublications.m_cBases.OutputMemorySnapshot = OutputMemorySnapshotcPublications.m_cBases.OutputMemorySnapshotSingleObject = OutputMemorySnapshotSingleObjectcPublications.m_cBases.OutputFilteredResult = OutputFilteredResultreturn cPublications