--[[ ProFi v1.3, by Luke Perkin 2012. MIT Licence http://www.opensource.org/licenses/mit-license.php. Updated to v1.4 by Robert Machmer 2017 Example: ProFi = require 'ProFi' ProFi:start() some_function() another_function() coroutine.resume( some_coroutine ) ProFi:stop() ProFi:writeReport( 'MyProfilingReport.txt' ) API: *Arguments are specified as: type/name/default. ProFi:start( string/once/nil ) ProFi:stop() ProFi:checkMemory( number/interval/0, string/note/'' ) ProFi:writeReport( string/filename/'ProFi.txt' ) ProFi:reset() ProFi:setHookCount( number/hookCount/0 ) ProFi:setGetTimeMethod( function/getTimeMethod/os.clock ) ProFi:setInspect( string/methodName, number/levels/1 ) ]] ----------------------- -- Locals: ----------------------- local ProFi = {} local onDebugHook, sortByDurationDesc, sortByCallCount, getTime local DEFAULT_DEBUG_HOOK_COUNT = 0 local FORMAT_HEADER_LINE = "| %-50s: %-40s: %-20s: %-12s: %-12s: %-12s|\n" local FORMAT_OUTPUT_LINE = "| %s: %-12s: %-12s: %-12s|\n" local FORMAT_INSPECTION_LINE = "> %s: %-12s\n" local FORMAT_TOTALTIME_LINE = "| TOTAL TIME = %f\n" local FORMAT_MEMORY_LINE = "| %-20s: %-16s: %-16s| %s\n" local FORMAT_HIGH_MEMORY_LINE = "H %-20s: %-16s: %-16sH %s\n" local FORMAT_LOW_MEMORY_LINE = "L %-20s: %-16s: %-16sL %s\n" local FORMAT_TITLE = "%-50.50s: %-40.40s: %-20s" local FORMAT_LINENUM = "%4i" local FORMAT_TIME = "%04.3f" local FORMAT_RELATIVE = "%03.2f%%" local FORMAT_COUNT = "%7i" local FORMAT_KBYTES = "%7i Kbytes" local FORMAT_MBYTES = "%7.1f Mbytes" local FORMAT_MEMORY_HEADER1 = "\n=== HIGH & LOW MEMORY USAGE ===============================\n" local FORMAT_MEMORY_HEADER2 = "=== MEMORY USAGE ==========================================\n" local FORMAT_BANNER = [[ ############################################################################################################### ##### ProFi, a lua profiler. This profile was generated on: %s ##### ProFi is created by Luke Perkin 2012 under the MIT Licence, www.locofilm.co.uk ##### Version 1.3 Original gist can be found at https://gist.github.com/perky/2838755 ##### Version 1.4 Cleaned up code to work with luacheck https://gist.github.com/rm-code/383c98a6af04652ed9f39b7ae536bcc5 ############################################################################################################### ]] ----------------------- -- Public Methods: ----------------------- --[[ Starts profiling any method that is called between this and ProFi:stop(). Pass the parameter 'once' to so that this methodis only run once. Example: ProFi:start( 'once' ) ]] function ProFi:start( param ) if param == 'once' then if self:shouldReturn() then return else self.should_run_once = true end end self.has_started = true self.has_finished = false self:resetReports( self.reports ) self:startHooks() self.startTime = getTime() end --[[ Stops profiling. ]] function ProFi:stop() if self:shouldReturn() then return end self.stopTime = getTime() self:stopHooks() self.has_finished = true end function ProFi:checkMemory( ninterval, note ) local time = getTime() local interval = ninterval or 0 if self.lastCheckMemoryTime and time < self.lastCheckMemoryTime + interval then return end self.lastCheckMemoryTime = time local memoryReport = { ['time'] = time; ['memory'] = collectgarbage('count'); ['note'] = note or ''; } table.insert( self.memoryReports, memoryReport ) self:setHighestMemoryReport( memoryReport ) self:setLowestMemoryReport( memoryReport ) end --[[ Writes the profile report to a file. Param: [filename:string:optional] defaults to 'ProFi.txt' if not specified. ]] function ProFi:writeReport( filename ) if #self.reports > 0 or #self.memoryReports > 0 then filename = filename or 'ProFi.txt' self:sortReportsWithSortMethod( self.reports, self.sortMethod ) self:writeReportsToFilename( filename ) print( string.format("[ProFi]\t Report written to %s", filename) ) return true; end end --[[ Resets any profile information stored. ]] function ProFi:reset() self.reports = {} self.reportsByTitle = {} self.memoryReports = {} self.highestMemoryReport = nil self.lowestMemoryReport = nil self.has_started = false self.has_finished = false self.should_run_once = false self.lastCheckMemoryTime = nil self.hookCount = self.hookCount or DEFAULT_DEBUG_HOOK_COUNT self.sortMethod = self.sortMethod or sortByDurationDesc self.inspect = nil end --[[ Set how often a hook is called. See http://pgl.yoyo.org/luai/i/debug.sethook for information. Param: [hookCount:number] if 0 ProFi counts every time a function is called. if 2 ProFi counts every other 2 function calls. ]] function ProFi:setHookCount( hookCount ) self.hookCount = hookCount end --[[ Set how the report is sorted when written to file. Param: [sortType:string] either 'duration' or 'count'. 'duration' sorts by the time a method took to run. 'count' sorts by the number of times a method was called. ]] function ProFi:setSortMethod( sortType ) if sortType == 'duration' then self.sortMethod = sortByDurationDesc elseif sortType == 'count' then self.sortMethod = sortByCallCount end end --[[ By default the getTime method is os.clock (CPU time), If you wish to use other time methods pass it to this function. Param: [getTimeMethod:function] ]] function ProFi:setGetTimeMethod( getTimeMethod ) getTime = getTimeMethod end --[[ Allows you to inspect a specific method. Will write to the report a list of methods that call this method you're inspecting, you can optionally provide a levels parameter to traceback a number of levels. Params: [methodName:string] the name of the method you wish to inspect. [levels:number:optional] the amount of levels you wish to traceback, defaults to 1. ]] function ProFi:setInspect( methodName, levels ) if self.inspect then self.inspect.methodName = methodName self.inspect.levels = levels or 1 else self.inspect = { ['methodName'] = methodName; ['levels'] = levels or 1; } end end ----------------------- -- Implementations methods: ----------------------- function ProFi:shouldReturn( ) return self.should_run_once and self.has_finished end function ProFi:getFuncReport( funcInfo ) local title = self:getTitleFromFuncInfo( funcInfo ) local funcReport = self.reportsByTitle[ title ] if not funcReport then funcReport = self:createFuncReport( funcInfo ) self.reportsByTitle[ title ] = funcReport table.insert( self.reports, funcReport ) end return funcReport end function ProFi:getTitleFromFuncInfo( funcInfo ) local name = funcInfo.name or 'anonymous' local source = funcInfo.short_src or 'C_FUNC' local linedefined = funcInfo.linedefined or 0 linedefined = string.format( FORMAT_LINENUM, linedefined ) return string.format(FORMAT_TITLE, source, name, linedefined) end function ProFi:createFuncReport( funcInfo ) local funcReport = { ['title'] = self:getTitleFromFuncInfo( funcInfo ); ['count'] = 0; ['timer'] = 0; } return funcReport end function ProFi:startHooks() debug.sethook( onDebugHook, 'cr', self.hookCount ) end function ProFi:stopHooks() debug.sethook() end function ProFi:sortReportsWithSortMethod( reports, sortMethod ) if reports then table.sort( reports, sortMethod ) end end function ProFi:writeReportsToFilename( filename ) local file, err = io.open( filename, 'w' ) assert( file, err ) self:writeBannerToFile( file ) if #self.reports > 0 then self:writeProfilingReportsToFile( self.reports, file ) end if #self.memoryReports > 0 then self:writeMemoryReportsToFile( self.memoryReports, file ) end file:close() end function ProFi:writeProfilingReportsToFile( reports, file ) local totalTime = self.stopTime - self.startTime local totalTimeOutput = string.format(FORMAT_TOTALTIME_LINE, totalTime) file:write( totalTimeOutput ) local header = string.format( FORMAT_HEADER_LINE, "FILE", "FUNCTION", "LINE", "TIME", "RELATIVE", "CALLED" ) file:write( header ) for _, funcReport in ipairs( reports ) do local timer = string.format(FORMAT_TIME, funcReport.timer) local count = string.format(FORMAT_COUNT, funcReport.count) local relTime = string.format(FORMAT_RELATIVE, (funcReport.timer / totalTime) * 100 ) local outputLine = string.format(FORMAT_OUTPUT_LINE, funcReport.title, timer, relTime, count ) file:write( outputLine ) if funcReport.inspections then self:writeInpsectionsToFile( funcReport.inspections, file ) end end end function ProFi:writeMemoryReportsToFile( reports, file ) file:write( FORMAT_MEMORY_HEADER1 ) self:writeHighestMemoryReportToFile( file ) self:writeLowestMemoryReportToFile( file ) file:write( FORMAT_MEMORY_HEADER2 ) for _, memoryReport in ipairs( reports ) do local outputLine = self:formatMemoryReportWithFormatter( memoryReport, FORMAT_MEMORY_LINE ) file:write( outputLine ) end end function ProFi:writeHighestMemoryReportToFile( file ) local memoryReport = self.highestMemoryReport local outputLine = self:formatMemoryReportWithFormatter( memoryReport, FORMAT_HIGH_MEMORY_LINE ) file:write( outputLine ) end function ProFi:writeLowestMemoryReportToFile( file ) local memoryReport = self.lowestMemoryReport local outputLine = self:formatMemoryReportWithFormatter( memoryReport, FORMAT_LOW_MEMORY_LINE ) file:write( outputLine ) end function ProFi:formatMemoryReportWithFormatter( memoryReport, formatter ) local time = string.format(FORMAT_TIME, memoryReport.time) local kbytes = string.format(FORMAT_KBYTES, memoryReport.memory) local mbytes = string.format(FORMAT_MBYTES, memoryReport.memory/1024) local outputLine = string.format(formatter, time, kbytes, mbytes, memoryReport.note) return outputLine end function ProFi:writeBannerToFile( file ) local banner = string.format(FORMAT_BANNER, os.date()) file:write( banner ) end function ProFi:writeInpsectionsToFile( inspections, file ) local inspectionsList = self:sortInspectionsIntoList( inspections ) file:write('\n==^ INSPECT ^======================================================================================================== COUNT ===\n') for _, inspection in ipairs( inspectionsList ) do local line = string.format(FORMAT_LINENUM, inspection.line) local title = string.format(FORMAT_TITLE, inspection.source, inspection.name, line) local count = string.format(FORMAT_COUNT, inspection.count) local outputLine = string.format(FORMAT_INSPECTION_LINE, title, count ) file:write( outputLine ) end file:write('===============================================================================================================================\n\n') end function ProFi:sortInspectionsIntoList( inspections ) local inspectionsList = {} for _, inspection in pairs(inspections) do inspectionsList[#inspectionsList+1] = inspection end table.sort( inspectionsList, sortByCallCount ) return inspectionsList end function ProFi:resetReports( reports ) for _, report in ipairs( reports ) do report.timer = 0 report.count = 0 report.inspections = nil end end function ProFi:shouldInspect( funcInfo ) return self.inspect and self.inspect.methodName == funcInfo.name end function ProFi:getInspectionsFromReport( funcReport ) local inspections = funcReport.inspections if not inspections then inspections = {} funcReport.inspections = inspections end return inspections end function ProFi:getInspectionWithKeyFromInspections( key, inspections ) local inspection = inspections[key] if not inspection then inspection = { ['count'] = 0; } inspections[key] = inspection end return inspection end function ProFi:doInspection( inspect, funcReport ) local inspections = self:getInspectionsFromReport( funcReport ) local levels = 5 + inspect.levels local currentLevel = 5 while currentLevel < levels do local funcInfo = debug.getinfo( currentLevel, 'nS' ) if funcInfo then local source = funcInfo.short_src or '[C]' local name = funcInfo.name or 'anonymous' local line = funcInfo.linedefined local key = source..name..line local inspection = self:getInspectionWithKeyFromInspections( key, inspections ) inspection.source = source inspection.name = name inspection.line = line inspection.count = inspection.count + 1 currentLevel = currentLevel + 1 else break end end end function ProFi:onFunctionCall( funcInfo ) local funcReport = ProFi:getFuncReport( funcInfo ) funcReport.callTime = getTime() funcReport.count = funcReport.count + 1 if self:shouldInspect( funcInfo ) then self:doInspection( self.inspect, funcReport ) end end function ProFi:onFunctionReturn( funcInfo ) local funcReport = ProFi:getFuncReport( funcInfo ) if funcReport.callTime then funcReport.timer = funcReport.timer + (getTime() - funcReport.callTime) end end function ProFi:setHighestMemoryReport( memoryReport ) if not self.highestMemoryReport then self.highestMemoryReport = memoryReport else if memoryReport.memory > self.highestMemoryReport.memory then self.highestMemoryReport = memoryReport end end end function ProFi:setLowestMemoryReport( memoryReport ) if not self.lowestMemoryReport then self.lowestMemoryReport = memoryReport else if memoryReport.memory < self.lowestMemoryReport.memory then self.lowestMemoryReport = memoryReport end end end ----------------------- -- Local Functions: ----------------------- getTime = os.clock onDebugHook = function( hookType ) local funcInfo = debug.getinfo( 2, 'nS' ) if hookType == "call" then ProFi:onFunctionCall( funcInfo ) elseif hookType == "return" then ProFi:onFunctionReturn( funcInfo ) end end sortByDurationDesc = function( a, b ) return a.timer > b.timer end sortByCallCount = function( a, b ) return a.count > b.count end ----------------------- -- Return Module: ----------------------- ProFi:reset() return ProFi