mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2025-08-15 10:47:21 +00:00
# Conflicts: # Moose Development/Moose/AI/AI_A2A_Dispatcher.lua # Moose Development/Moose/AI/AI_A2G_Dispatcher.lua # Moose Development/Moose/AI/AI_CAP.lua # Moose Development/Moose/AI/AI_CAS.lua # Moose Development/Moose/AI/AI_Patrol.lua # Moose Development/Moose/Core/Base.lua # Moose Development/Moose/Core/Beacon.lua # Moose Development/Moose/Core/Database.lua # Moose Development/Moose/Core/Fsm.lua # Moose Development/Moose/Core/MarkerOps_Base.lua # Moose Development/Moose/Core/Menu.lua # Moose Development/Moose/Core/Message.lua # Moose Development/Moose/Core/Point.lua # Moose Development/Moose/Core/ScheduleDispatcher.lua # Moose Development/Moose/Core/Scheduler.lua # Moose Development/Moose/Core/Set.lua # Moose Development/Moose/Core/Spawn.lua # Moose Development/Moose/Core/Zone.lua # Moose Development/Moose/DCS.lua # Moose Development/Moose/Functional/Detection.lua # Moose Development/Moose/Functional/Mantis.lua # Moose Development/Moose/Functional/Range.lua # Moose Development/Moose/Functional/Scoring.lua # Moose Development/Moose/Functional/Sead.lua # Moose Development/Moose/Modules.lua # Moose Development/Moose/Ops/ATIS.lua # Moose Development/Moose/Ops/Airboss.lua # Moose Development/Moose/Sound/UserSound.lua # Moose Development/Moose/Utilities/Enums.lua # Moose Development/Moose/Utilities/FiFo.lua # Moose Development/Moose/Utilities/Profiler.lua # Moose Development/Moose/Utilities/Routines.lua # Moose Development/Moose/Utilities/STTS.lua # Moose Development/Moose/Utilities/Utils.lua # Moose Development/Moose/Wrapper/Airbase.lua # Moose Development/Moose/Wrapper/Controllable.lua # Moose Development/Moose/Wrapper/Group.lua # Moose Development/Moose/Wrapper/Marker.lua # Moose Development/Moose/Wrapper/Positionable.lua # Moose Development/Moose/Wrapper/Unit.lua # Moose Setup/Moose.files
557 lines
18 KiB
Lua
557 lines
18 KiB
Lua
--- **Utils** - Lua Profiler.
|
|
--
|
|
-- Find out how many times functions are called and how much real time it costs.
|
|
--
|
|
-- ===
|
|
--
|
|
-- ### Author: **TAW CougarNL**, *funkyfranky*
|
|
--
|
|
-- @module Utilities.PROFILER
|
|
-- @image Utils_Profiler.jpg
|
|
|
|
--- PROFILER class.
|
|
-- @type PROFILER
|
|
-- @field #string ClassName Name of the class.
|
|
-- @field #table Counters Function counters.
|
|
-- @field #table dInfo Info.
|
|
-- @field #table fTime Function time.
|
|
-- @field #table fTimeTotal Total function time.
|
|
-- @field #table eventhandler Event handler to get mission end event.
|
|
-- @field #number TstartGame Game start time timer.getTime().
|
|
-- @field #number TstartOS OS real start time os.clock.
|
|
-- @field #boolean logUnknown Log unknown functions. Default is off.
|
|
-- @field #number ThreshCPS Low calls per second threshold. Only write output if function has more calls per second than this value.
|
|
-- @field #number ThreshTtot Total time threshold. Only write output if total function CPU time is more than this value.
|
|
-- @field #string fileNamePrefix Output file name prefix, e.g. "MooseProfiler".
|
|
-- @field #string fileNameSuffix Output file name prefix, e.g. "txt"
|
|
|
|
--- *The emperor counsels simplicity.* *First principles. Of each particular thing, ask: What is it in itself, in its own constitution? What is its causal nature?*
|
|
--
|
|
-- ===
|
|
--
|
|
-- 
|
|
--
|
|
-- # The PROFILER Concept
|
|
--
|
|
-- Profile your lua code. This tells you, which functions are called very often and which consume most real time.
|
|
-- With this information you can optimize the perfomance of your code.
|
|
--
|
|
-- # Prerequisites
|
|
--
|
|
-- The modules **os**, **io** and **lfs** need to be desanizied. Comment out the lines
|
|
--
|
|
-- --sanitizeModule('os')
|
|
-- --sanitizeModule('io')
|
|
-- --sanitizeModule('lfs')
|
|
--
|
|
-- in your *"DCS World OpenBeta/Scripts/MissionScripting.lua"* file.
|
|
--
|
|
-- But be aware that these changes can make you system vulnerable to attacks.
|
|
--
|
|
-- # Disclaimer
|
|
--
|
|
-- **Profiling itself is CPU expensive!** Don't use this when you want to fly or host a mission.
|
|
--
|
|
--
|
|
-- # Start
|
|
--
|
|
-- The profiler can simply be started with the @{#PROFILER.Start}(*Delay, Duration*) function
|
|
--
|
|
-- PROFILER.Start()
|
|
--
|
|
-- The optional parameter *Delay* can be used to delay the start by a certain amount of seconds and the optional parameter *Duration* can be used to
|
|
-- stop the profiler after a certain amount of seconds.
|
|
--
|
|
-- # Stop
|
|
--
|
|
-- The profiler automatically stops when the mission ends. But it can be stopped any time with the @{#PROFILER.Stop}(*Delay*) function
|
|
--
|
|
-- PROFILER.Stop()
|
|
--
|
|
-- The optional parameter *Delay* can be used to specify a delay after which the profiler is stopped.
|
|
--
|
|
-- When the profiler is stopped, the output is written to a file.
|
|
--
|
|
-- # Output
|
|
--
|
|
-- The profiler output is written to a file in your DCS home folder
|
|
--
|
|
-- X:\User\<Your User Name>\Saved Games\DCS OpenBeta\Logs
|
|
--
|
|
-- The default file name is "MooseProfiler.txt". If that file exists, the file name is "MooseProfiler-001.txt" etc.
|
|
--
|
|
-- ## Data
|
|
--
|
|
-- The data in the output file provides information on the functions that were called in the mission.
|
|
--
|
|
-- It will tell you how many times a function was called in total, how many times per second, how much time in total and the percentage of time.
|
|
--
|
|
-- If you only want output for functions that are called more than *X* times per second, you can set
|
|
--
|
|
-- PROFILER.ThreshCPS=1.5
|
|
--
|
|
-- With this setting, only functions which are called more than 1.5 times per second are displayed. The default setting is PROFILER.ThreshCPS=0.0 (no threshold).
|
|
--
|
|
-- Furthermore, you can limit the output for functions that consumed a certain amount of CPU time in total by
|
|
--
|
|
-- PROFILER.ThreshTtot=0.005
|
|
--
|
|
-- With this setting, which is also the default, only functions which in total used more than 5 milliseconds CPU time.
|
|
--
|
|
-- @field #PROFILER
|
|
PROFILER = {
|
|
ClassName = "PROFILER",
|
|
Counters = {},
|
|
dInfo = {},
|
|
fTime = {},
|
|
fTimeTotal = {},
|
|
eventHandler = {},
|
|
logUnknown = false,
|
|
ThreshCPS = 0.0,
|
|
ThreshTtot = 0.005,
|
|
fileNamePrefix = "MooseProfiler",
|
|
fileNameSuffix = "txt"
|
|
}
|
|
|
|
--- Waypoint data.
|
|
-- @type PROFILER.Data
|
|
-- @field #string func The function name.
|
|
-- @field #string src The source file.
|
|
-- @field #number line The line number
|
|
-- @field #number count Number of function calls.
|
|
-- @field #number tm Total time in seconds.
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Start/Stop Profiler
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- Start profiler.
|
|
-- @param #number Delay Delay in seconds before profiler is stated. Default is immediately.
|
|
-- @param #number Duration Duration in (game) seconds before the profiler is stopped. Default is when mission ends.
|
|
function PROFILER.Start( Delay, Duration )
|
|
|
|
-- Check if os, io and lfs are available.
|
|
local go = true
|
|
if not os then
|
|
env.error( "ERROR: Profiler needs os to be de-sanitized!" )
|
|
go = false
|
|
end
|
|
if not io then
|
|
env.error("ERROR: Profiler needs io to be desanitized!")
|
|
go=false
|
|
end
|
|
if not lfs then
|
|
env.error("ERROR: Profiler needs lfs to be desanitized!")
|
|
go=false
|
|
end
|
|
if not go then
|
|
return
|
|
end
|
|
|
|
if Delay and Delay > 0 then
|
|
BASE:ScheduleOnce( Delay, PROFILER.Start, 0, Duration )
|
|
else
|
|
|
|
-- Set start time.
|
|
PROFILER.TstartGame=timer.getTime()
|
|
PROFILER.TstartOS=os.clock()
|
|
|
|
-- Add event handler.
|
|
world.addEventHandler(PROFILER.eventHandler)
|
|
|
|
-- Info in log.
|
|
env.info( '############################ Profiler Started ############################' )
|
|
if Duration then
|
|
env.info( string.format( "- Will be running for %d seconds", Duration ) )
|
|
else
|
|
env.info( string.format( "- Will be stopped when mission ends" ) )
|
|
end
|
|
env.info(string.format("- Calls per second threshold %.3f/sec", PROFILER.ThreshCPS))
|
|
env.info(string.format("- Total function time threshold %.3f sec", PROFILER.ThreshTtot))
|
|
env.info(string.format("- Output file \"%s\" in your DCS log file folder", PROFILER.getfilename(PROFILER.fileNameSuffix)))
|
|
env.info(string.format("- Output file \"%s\" in CSV format", PROFILER.getfilename("csv")))
|
|
env.info('###############################################################################')
|
|
|
|
|
|
-- Message on screen
|
|
local duration=Duration or 600
|
|
trigger.action.outText("### Profiler running ###", duration)
|
|
|
|
-- Set hook.
|
|
debug.sethook(PROFILER.hook, "cr")
|
|
|
|
-- Auto stop profiler.
|
|
if Duration then
|
|
PROFILER.Stop( Duration )
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
--- Stop profiler.
|
|
-- @param #number Delay Delay before stop in seconds.
|
|
function PROFILER.Stop( Delay )
|
|
|
|
if Delay and Delay > 0 then
|
|
|
|
BASE:ScheduleOnce( Delay, PROFILER.Stop )
|
|
end
|
|
end
|
|
|
|
function PROFILER.Stop(Delay)
|
|
|
|
if Delay and Delay>0 then
|
|
|
|
BASE:ScheduleOnce(Delay, PROFILER.Stop)
|
|
|
|
else
|
|
|
|
-- Remove hook.
|
|
debug.sethook()
|
|
|
|
|
|
-- Run time game.
|
|
local runTimeGame=timer.getTime()-PROFILER.TstartGame
|
|
|
|
-- Run time real OS.
|
|
local runTimeOS=os.clock()-PROFILER.TstartOS
|
|
|
|
-- Show info.
|
|
PROFILER.showInfo(runTimeGame, runTimeOS)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
--- Event handler.
|
|
function PROFILER.eventHandler:onEvent( event )
|
|
if event.id == world.event.S_EVENT_MISSION_END then
|
|
PROFILER.Stop()
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Hook
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- Debug hook.
|
|
-- @param #table event Event.
|
|
function PROFILER.hook(event)
|
|
|
|
local f=debug.getinfo(2, "f").func
|
|
|
|
if event=='call' then
|
|
|
|
if PROFILER.Counters[f]==nil then
|
|
|
|
PROFILER.Counters[f]=1
|
|
PROFILER.dInfo[f]=debug.getinfo(2,"Sn")
|
|
|
|
if PROFILER.fTimeTotal[f]==nil then
|
|
PROFILER.fTimeTotal[f]=0
|
|
end
|
|
|
|
else
|
|
PROFILER.Counters[f] = PROFILER.Counters[f] + 1
|
|
end
|
|
|
|
if PROFILER.fTime[f]==nil then
|
|
PROFILER.fTime[f]=os.clock()
|
|
end
|
|
|
|
elseif (event=='return') then
|
|
|
|
if PROFILER.fTime[f]~=nil then
|
|
PROFILER.fTimeTotal[f]=PROFILER.fTimeTotal[f]+(os.clock()-PROFILER.fTime[f])
|
|
PROFILER.fTime[f]=nil
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Data
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- Get data.
|
|
-- @param #function func Function.
|
|
-- @return #string Function name.
|
|
-- @return #string Source file name.
|
|
-- @return #string Line number.
|
|
-- @return #number Function time in seconds.
|
|
function PROFILER.getData(func)
|
|
|
|
local n=PROFILER.dInfo[func]
|
|
|
|
if n.what=="C" then
|
|
return n.name, "?", "?", PROFILER.fTimeTotal[func]
|
|
end
|
|
|
|
return n.name, n.short_src, n.linedefined, PROFILER.fTimeTotal[func]
|
|
end
|
|
|
|
--- Write text to log file.
|
|
-- @param #function f The file.
|
|
-- @param #string txt The text.
|
|
function PROFILER._flog( f, txt )
|
|
f:write( txt .. "\r\n" )
|
|
end
|
|
|
|
--- Show table.
|
|
-- @param #table data Data table.
|
|
-- @param #function f The file.
|
|
-- @param #number runTimeGame Game run time in seconds.
|
|
function PROFILER.showTable( data, f, runTimeGame )
|
|
|
|
-- Loop over data.
|
|
for i=1, #data do
|
|
local t=data[i] --#PROFILER.Data
|
|
|
|
-- Calls per second.
|
|
local cps=t.count/runTimeGame
|
|
|
|
local threshCPS=cps>=PROFILER.ThreshCPS
|
|
local threshTot=t.tm>=PROFILER.ThreshTtot
|
|
|
|
if threshCPS and threshTot then
|
|
|
|
-- Output
|
|
local text=string.format("%30s: %8d calls %8.1f/sec - Time Total %8.3f sec (%.3f %%) %5.3f sec/call %s line %s", t.func, t.count, cps, t.tm, t.tm/runTimeGame*100, t.tm/t.count, tostring(t.src), tostring(t.line))
|
|
PROFILER._flog(f, text)
|
|
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
--- Print csv file.
|
|
-- @param #table data Data table.
|
|
-- @param #number runTimeGame Game run time in seconds.
|
|
function PROFILER.printCSV( data, runTimeGame )
|
|
|
|
-- Output file.
|
|
local file = PROFILER.getfilename( "csv" )
|
|
local g = io.open( file, 'w' )
|
|
|
|
-- Header.
|
|
local text="Function,Total Calls,Calls per Sec,Total Time,Total in %,Sec per Call,Source File;Line Number,"
|
|
g:write(text.."\r\n")
|
|
|
|
-- Loop over data.
|
|
for i=1, #data do
|
|
local t=data[i] --#PROFILER.Data
|
|
|
|
-- Calls per second.
|
|
local cps = t.count / runTimeGame
|
|
|
|
-- Output
|
|
local txt=string.format("%s,%d,%.1f,%.3f,%.3f,%.3f,%s,%s,", t.func, t.count, cps, t.tm, t.tm/runTimeGame*100, t.tm/t.count, tostring(t.src), tostring(t.line))
|
|
g:write(txt.."\r\n")
|
|
|
|
end
|
|
|
|
-- Close file.
|
|
g:close()
|
|
end
|
|
|
|
--- Write info to output file.
|
|
-- @param #string ext Extension.
|
|
-- @return #string File name.
|
|
function PROFILER.getfilename(ext)
|
|
|
|
local dir=lfs.writedir()..[[Logs\]]
|
|
|
|
ext=ext or PROFILER.fileNameSuffix
|
|
|
|
local file=dir..PROFILER.fileNamePrefix.."."..ext
|
|
|
|
if not UTILS.FileExists(file) then
|
|
return file
|
|
end
|
|
|
|
for i = 1, 999 do
|
|
|
|
local file = string.format( "%s%s-%03d.%s", dir, PROFILER.fileNamePrefix, i, ext )
|
|
|
|
if not UTILS.FileExists( file ) then
|
|
return file
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
--- Write info to output file.
|
|
-- @param #number runTimeGame Game time in seconds.
|
|
-- @param #number runTimeOS OS time in seconds.
|
|
function PROFILER.showInfo( runTimeGame, runTimeOS )
|
|
|
|
-- Output file.
|
|
local file=PROFILER.getfilename(PROFILER.fileNameSuffix)
|
|
local f=io.open(file, 'w')
|
|
|
|
-- Gather data.
|
|
local Ttot=0
|
|
local Calls=0
|
|
|
|
local t={}
|
|
|
|
local tcopy=nil --#PROFILER.Data
|
|
local tserialize=nil --#PROFILER.Data
|
|
local tforgen=nil --#PROFILER.Data
|
|
local tpairs=nil --#PROFILER.Data
|
|
|
|
|
|
for func, count in pairs(PROFILER.Counters) do
|
|
|
|
local s,src,line,tm=PROFILER.getData(func)
|
|
|
|
if PROFILER.logUnknown==true then
|
|
if s==nil then s="<Unknown>" end
|
|
end
|
|
|
|
if s~=nil then
|
|
|
|
-- Profile data.
|
|
local T=
|
|
{ func=s,
|
|
src=src,
|
|
line=line,
|
|
count=count,
|
|
tm=tm,
|
|
} --#PROFILER.Data
|
|
|
|
-- Collect special cases. Somehow, e.g. "_copy" appears multiple times so we try to gather all data.
|
|
if s == "_copy" then
|
|
if tcopy == nil then
|
|
tcopy = T
|
|
else
|
|
tcopy.count = tcopy.count + T.count
|
|
tcopy.tm = tcopy.tm + T.tm
|
|
end
|
|
elseif s == "_Serialize" then
|
|
if tserialize == nil then
|
|
tserialize = T
|
|
else
|
|
tserialize.count=tserialize.count+T.count
|
|
tserialize.tm=tserialize.tm+T.tm
|
|
end
|
|
elseif s=="(for generator)" then
|
|
if tforgen==nil then
|
|
tforgen=T
|
|
else
|
|
tforgen.count=tforgen.count+T.count
|
|
tforgen.tm=tforgen.tm+T.tm
|
|
end
|
|
elseif s=="pairs" then
|
|
if tpairs==nil then
|
|
tpairs=T
|
|
else
|
|
tpairs.count=tpairs.count+T.count
|
|
tpairs.tm=tpairs.tm+T.tm
|
|
end
|
|
else
|
|
table.insert( t, T )
|
|
end
|
|
|
|
-- Total function time.
|
|
Ttot=Ttot+tm
|
|
|
|
-- Total number of calls.
|
|
Calls=Calls+count
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- Add special cases.
|
|
if tcopy then
|
|
table.insert( t, tcopy )
|
|
end
|
|
if tserialize then
|
|
table.insert(t, tserialize)
|
|
end
|
|
if tforgen then
|
|
table.insert( t, tforgen )
|
|
end
|
|
if tpairs then
|
|
table.insert(t, tpairs)
|
|
end
|
|
|
|
env.info('############################ Profiler Stopped ############################')
|
|
env.info(string.format("* Runtime Game : %s = %d sec", UTILS.SecondsToClock(runTimeGame, true), runTimeGame))
|
|
env.info(string.format("* Runtime Real : %s = %d sec", UTILS.SecondsToClock(runTimeOS, true), runTimeOS))
|
|
env.info(string.format("* Function time : %s = %.1f sec (%.1f percent of runtime game)", UTILS.SecondsToClock(Ttot, true), Ttot, Ttot/runTimeGame*100))
|
|
env.info(string.format("* Total functions : %d", #t))
|
|
env.info(string.format("* Total func calls : %d", Calls))
|
|
env.info(string.format("* Writing to file : \"%s\"", file))
|
|
env.info(string.format("* Writing to file : \"%s\"", PROFILER.getfilename("csv")))
|
|
env.info("##############################################################################")
|
|
|
|
-- Sort by total time.
|
|
table.sort(t, function(a,b) return a.tm>b.tm end)
|
|
|
|
-- Write data.
|
|
PROFILER._flog(f,"")
|
|
PROFILER._flog(f,"************************************************************************************************************************")
|
|
PROFILER._flog(f,"************************************************************************************************************************")
|
|
PROFILER._flog(f,"************************************************************************************************************************")
|
|
PROFILER._flog(f,"")
|
|
PROFILER._flog(f,"-------------------------")
|
|
PROFILER._flog(f,"---- Profiler Report ----")
|
|
PROFILER._flog(f,"-------------------------")
|
|
PROFILER._flog(f,"")
|
|
PROFILER._flog(f,string.format("* Runtime Game : %s = %.1f sec", UTILS.SecondsToClock(runTimeGame, true), runTimeGame))
|
|
PROFILER._flog(f,string.format("* Runtime Real : %s = %.1f sec", UTILS.SecondsToClock(runTimeOS, true), runTimeOS))
|
|
PROFILER._flog(f,string.format("* Function time : %s = %.1f sec (%.1f %% of runtime game)", UTILS.SecondsToClock(Ttot, true), Ttot, Ttot/runTimeGame*100))
|
|
PROFILER._flog(f,"")
|
|
PROFILER._flog(f,string.format("* Total functions = %d", #t))
|
|
PROFILER._flog(f,string.format("* Total func calls = %d", Calls))
|
|
PROFILER._flog(f,"")
|
|
PROFILER._flog(f,string.format("* Calls per second threshold = %.3f/sec", PROFILER.ThreshCPS))
|
|
PROFILER._flog(f,string.format("* Total func time threshold = %.3f sec", PROFILER.ThreshTtot))
|
|
PROFILER._flog(f,"")
|
|
PROFILER._flog(f,"************************************************************************************************************************")
|
|
PROFILER._flog(f,"")
|
|
PROFILER.showTable(t, f, runTimeGame)
|
|
|
|
-- Sort by number of calls.
|
|
table.sort(t, function(a,b) return a.tm/a.count>b.tm/b.count end)
|
|
|
|
-- Detailed data.
|
|
PROFILER._flog(f,"")
|
|
PROFILER._flog(f,"************************************************************************************************************************")
|
|
PROFILER._flog(f,"")
|
|
PROFILER._flog(f,"--------------------------------------")
|
|
PROFILER._flog(f,"---- Data Sorted by Time per Call ----")
|
|
PROFILER._flog(f,"--------------------------------------")
|
|
PROFILER._flog(f,"")
|
|
PROFILER.showTable(t, f, runTimeGame)
|
|
|
|
-- Sort by number of calls.
|
|
table.sort(t, function(a,b) return a.count>b.count end)
|
|
|
|
-- Detailed data.
|
|
PROFILER._flog(f,"")
|
|
PROFILER._flog(f,"************************************************************************************************************************")
|
|
PROFILER._flog(f,"")
|
|
PROFILER._flog(f,"------------------------------------")
|
|
PROFILER._flog(f,"---- Data Sorted by Total Calls ----")
|
|
PROFILER._flog(f,"------------------------------------")
|
|
PROFILER._flog(f,"")
|
|
PROFILER.showTable(t, f, runTimeGame)
|
|
|
|
-- Closing.
|
|
PROFILER._flog( f, "" )
|
|
PROFILER._flog( f, "************************************************************************************************************************" )
|
|
PROFILER._flog( f, "************************************************************************************************************************" )
|
|
PROFILER._flog( f, "************************************************************************************************************************" )
|
|
-- Close file.
|
|
f:close()
|
|
|
|
-- Print csv file.
|
|
PROFILER.printCSV( t, runTimeGame )
|
|
end
|