remove some memory leaks from rendered fragments

[?]
Jun 10, 2022, 3:24 PM
OGUV4HSA7XGSQLUVWBAE3AE263Z7Z6G3BZOB4CN2AOYD2DEJMOZAC

Dependencies

  • [2] LS55YKGW switch copy/paste to ctrl- hotkeys
  • [3] NP7PIUBT bugfix: restore state after C-f (find)
  • [*] BULPIBEG beginnings of a module for the text editor
  • [*] HMODUNJE scroll on backspace
  • [*] XNFTJHC4 split keyboard handling between Text and Drawing
  • [*] KOYAJWE4 extract a couple more methods
  • [*] 3OKKTUT4 up and down arrow now moving by screen line where possible
  • [*] 242L3OQX bugfix: ensure Cursor_line is always on a text line
  • [*] HOSPP2AN crisp font rendering
  • [*] R5QXEHUI somebody stop me
  • [*] OTIBCAUJ love2d scaffold
  • [*] Z4XRNDTR find text
  • [*] NQWWTGXR switch undo/redo to ctrl- hotkeys

Change contents

  • edit in text.lua at line 226
    [6.3103]
    [7.2992]
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
  • edit in text.lua at line 427
    [8.1214]
    [8.1214]
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
  • edit in text.lua at line 667
    [9.1161]
    [10.931]
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
  • edit in text.lua at line 901
    [11.4025]
    [11.4025]
    print('clearing fragments')
  • file addition: main.lua. (----------)
    [12.2]
    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
  • edit in main.lua at line 345
    [3.564]
    [14.3262]
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
  • edit in main.lua at line 384
    [15.351]
    [15.351]
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
  • edit in main.lua at line 395
    [15.638]
    [2.38]
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
  • file addition: MemoryReferenceInfo.lua.0 (----------)
    [12.2]
    --
    -- 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
  • file addition: MemoryReferenceInfo.lua. (----------)
    [12.2]
    --
    -- 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