This Trac instance is not used for development anymore!

We migrated our development workflow to git and Gitea.
To test the future redirection, replace trac by ariadne in the page URL.

source: ps/trunk/build/premake/premake5/tests/pepperfish_profiler.lua

Last change on this file was 22021, checked in by Itms, 6 years ago

Upgrade premake5 from alpha12 to alpha13, refs #3729.

Fixes an issue on macOS Mojave that was patched in rP21941.

The list of changes can be found under build/premake/premake5/CHANGES.txt.

File size: 19.1 KB
Line 
1--[[
2
3== Introduction ==
4
5 Note that this requires os.clock(), debug.sethook(),
6 and debug.getinfo() or your equivalent replacements to
7 be available if this is an embedded application.
8
9 Example usage:
10
11 profiler = newProfiler()
12 profiler:start()
13
14 < call some functions that take time >
15
16 profiler:stop()
17
18 local outfile = io.open( "profile.txt", "w+" )
19 profiler:report( outfile )
20 outfile:close()
21
22== Optionally choosing profiling method ==
23
24The rest of this comment can be ignored if you merely want a good profiler.
25
26 newProfiler(method, sampledelay):
27
28If method is omitted or "time", will profile based on real performance.
29optionally, frequency can be provided to control the number of opcodes
30per profiling tick. By default this is 100000, which (on my system) provides
31one tick approximately every 2ms and reduces system performance by about 10%.
32This can be reduced to increase accuracy at the cost of performance, or
33increased for the opposite effect.
34
35If method is "call", will profile based on function calls. Frequency is
36ignored.
37
38
39"time" may bias profiling somewhat towards large areas with "simple opcodes",
40as the profiling function (which introduces a certain amount of unavoidable
41overhead) will be called more often. This can be minimized by using a larger
42sample delay - the default should leave any error largely overshadowed by
43statistical noise. With a delay of 1000 I was able to achieve inaccuray of
44approximately 25%. Increasing the delay to 100000 left inaccuracy below my
45testing error.
46
47"call" may bias profiling heavily towards areas with many function calls.
48Testing found a degenerate case giving a figure inaccurate by approximately
4920,000%. (Yes, a multiple of 200.) This is, however, more directly comparable
50to common profilers (such as gprof) and also gives accurate function call
51counts, which cannot be retrieved from "time".
52
53I strongly recommend "time" mode, and it is now the default.
54
55== History ==
56
572008-09-16 - Time-based profiling and conversion to Lua 5.1
58 by Ben Wilhelm ( zorba-pepperfish@pavlovian.net ).
59 Added the ability to optionally choose profiling methods, along with a new
60 profiling method.
61
62Converted to Lua 5, a few improvements, and
63additional documentation by Tom Spilman ( tom@sickheadgames.com )
64
65Additional corrections and tidying by original author
66Daniel Silverstone ( dsilvers@pepperfish.net )
67
68== Status ==
69
70Daniel Silverstone is no longer using this code, and judging by how long it's
71been waiting for Lua 5.1 support, I don't think Tom Spilman is either. I'm
72perfectly willing to take on maintenance, so if you have problems or
73questions, go ahead and email me :)
74-- Ben Wilhelm ( zorba-pepperfish@pavlovian.net ) '
75
76== Copyright ==
77
78Lua profiler - Copyright Pepperfish 2002,2003,2004
79
80Permission is hereby granted, free of charge, to any person obtaining a copy
81of this software and associated documentation files (the "Software"), to
82deal in the Software without restriction, including without limitation the
83rights to use, copy, modify, merge, publish, distribute, and/or sell copies
84of the Software, and to permit persons to whom the Software is furnished to
85do so, subject to the following conditions:
86
87The above copyright notice and this permission notice shall be included in
88all copies or substantial portions of the Software.
89
90THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
91IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
92FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
93AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
94LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
95FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
96IN THE SOFTWARE.
97
98--]]
99
100
101--
102-- All profiler related stuff is stored in the top level table '_profiler'
103--
104_profiler = {}
105
106
107--
108-- newProfiler() creates a new profiler object for managing
109-- the profiler and storing state. Note that only one profiler
110-- object can be executing at one time.
111--
112function newProfiler(variant, sampledelay)
113 if _profiler.running then
114 print("Profiler already running.")
115 return
116 end
117
118 variant = variant or "time"
119
120 if variant ~= "time" and variant ~= "call" then
121 print("Profiler method must be 'time' or 'call'.")
122 return
123 end
124
125 local newprof = {}
126 for k,v in pairs(_profiler) do
127 newprof[k] = v
128 end
129 newprof.variant = variant
130 newprof.sampledelay = sampledelay or 100000
131 return newprof
132end
133
134
135--
136-- This function starts the profiler. It will do nothing
137-- if this (or any other) profiler is already running.
138--
139function _profiler.start(self)
140 if _profiler.running then
141 return
142 end
143 -- Start the profiler. This begins by setting up internal profiler state
144 _profiler.running = self
145 self.rawstats = {}
146 self.callstack = {}
147 if self.variant == "time" then
148 self.lastclock = os.clock()
149 debug.sethook( _profiler_hook_wrapper_by_time, "", self.sampledelay )
150 elseif self.variant == "call" then
151 debug.sethook( _profiler_hook_wrapper_by_call, "cr" )
152 else
153 print("Profiler method must be 'time' or 'call'.")
154 sys.exit(1)
155 end
156end
157
158
159--
160-- This function stops the profiler. It will do nothing
161-- if a profiler is not running, and nothing if it isn't
162-- the currently running profiler.
163--
164function _profiler.stop(self)
165 if _profiler.running ~= self then
166 return
167 end
168 -- Stop the profiler.
169 debug.sethook( nil )
170 _profiler.running = nil
171end
172
173
174--
175-- Simple wrapper to handle the hook. You should not
176-- be calling this directly. Duplicated to reduce overhead.
177--
178function _profiler_hook_wrapper_by_call(action)
179 if _profiler.running == nil then
180 debug.sethook( nil )
181 end
182 _profiler.running:_internal_profile_by_call(action)
183end
184function _profiler_hook_wrapper_by_time(action)
185 if _profiler.running == nil then
186 debug.sethook( nil )
187 end
188 _profiler.running:_internal_profile_by_time(action)
189end
190
191
192--
193-- This is the main by-function-call function of the profiler and should not
194-- be called except by the hook wrapper
195--
196function _profiler._internal_profile_by_call(self,action)
197 -- Since we can obtain the 'function' for the item we've had call us, we
198 -- can use that...
199 local caller_info = debug.getinfo( 3 )
200 if caller_info == nil then
201 print "No caller_info"
202 return
203 end
204
205 --SHG_LOG("[_profiler._internal_profile] "..(caller_info.name or "<nil>"))
206
207 -- Retrieve the most recent activation record...
208 local latest_ar = nil
209 local callstackLen = #self.callstack
210 if callstackLen > 0 then
211 latest_ar = self.callstack[callstackLen]
212 end
213
214 -- Are we allowed to profile this function?
215 local should_not_profile = 0
216 for k,v in pairs(self.prevented_functions) do
217 if k == caller_info.func then
218 should_not_profile = v
219 end
220 end
221 -- Also check the top activation record...
222 if latest_ar then
223 if latest_ar.should_not_profile == 2 then
224 should_not_profile = 2
225 end
226 end
227
228 -- Now then, are we in 'call' or 'return' ?
229 -- print("Profile:", caller_info.name, "SNP:", should_not_profile,
230 -- "Action:", action )
231 if action == "call" then
232 -- Making a call...
233 local this_ar = {}
234 this_ar.should_not_profile = should_not_profile
235 this_ar.parent_ar = latest_ar
236 this_ar.anon_child = 0
237 this_ar.name_child = 0
238 this_ar.children = {}
239 this_ar.children_time = {}
240 this_ar.clock_start = os.clock()
241 -- Last thing to do on a call is to insert this onto the ar stack...
242 table.insert( self.callstack, this_ar )
243 else
244 local this_ar = latest_ar
245 if this_ar == nil then
246 return -- No point in doing anything if no upper activation record
247 end
248
249 -- Right, calculate the time in this function...
250 this_ar.clock_end = os.clock()
251 this_ar.this_time = this_ar.clock_end - this_ar.clock_start
252
253 -- Now, if we have a parent, update its call info...
254 if this_ar.parent_ar then
255 this_ar.parent_ar.children[caller_info.func] =
256 (this_ar.parent_ar.children[caller_info.func] or 0) + 1
257 this_ar.parent_ar.children_time[caller_info.func] =
258 (this_ar.parent_ar.children_time[caller_info.func] or 0 ) +
259 this_ar.this_time
260 if caller_info.name == nil then
261 this_ar.parent_ar.anon_child =
262 this_ar.parent_ar.anon_child + this_ar.this_time
263 else
264 this_ar.parent_ar.name_child =
265 this_ar.parent_ar.name_child + this_ar.this_time
266 end
267 end
268 -- Now if we're meant to record information about ourselves, do so...
269 if this_ar.should_not_profile == 0 then
270 local inforec = self:_get_func_rec(caller_info.func,1)
271 inforec.count = inforec.count + 1
272 inforec.time = inforec.time + this_ar.this_time
273 inforec.anon_child_time = inforec.anon_child_time + this_ar.anon_child
274 inforec.name_child_time = inforec.name_child_time + this_ar.name_child
275 inforec.func_info = caller_info
276 for k,v in pairs(this_ar.children) do
277 inforec.children[k] = (inforec.children[k] or 0) + v
278 inforec.children_time[k] =
279 (inforec.children_time[k] or 0) + this_ar.children_time[k]
280 end
281 end
282
283 -- Last thing to do on return is to drop the last activation record...
284 local callstackLen = #self.callstack
285 table.remove( self.callstack, callstackLen )
286 end
287end
288
289
290--
291-- This is the main by-time internal function of the profiler and should not
292-- be called except by the hook wrapper
293--
294function _profiler._internal_profile_by_time(self,action)
295 -- we do this first so we add the minimum amount of extra time to this call
296 local timetaken = os.clock() - self.lastclock
297
298 local depth = 3
299 local at_top = true
300 local last_caller
301 local caller = debug.getinfo(depth)
302 while caller do
303 if not caller.func then caller.func = "(tail call)" end
304 if self.prevented_functions[caller.func] == nil then
305 local info = self:_get_func_rec(caller.func, 1, caller)
306 info.count = info.count + 1
307 info.time = info.time + timetaken
308 if last_caller then
309 -- we're not the head, so update the "children" times also
310 if last_caller.name then
311 info.name_child_time = info.name_child_time + timetaken
312 else
313 info.anon_child_time = info.anon_child_time + timetaken
314 end
315 info.children[last_caller.func] =
316 (info.children[last_caller.func] or 0) + 1
317 info.children_time[last_caller.func] =
318 (info.children_time[last_caller.func] or 0) + timetaken
319 end
320 end
321 depth = depth + 1
322 last_caller = caller
323 caller = debug.getinfo(depth)
324 end
325
326 self.lastclock = os.clock()
327end
328
329
330--
331-- This returns a (possibly empty) function record for
332-- the specified function. It is for internal profiler use.
333--
334function _profiler._get_func_rec(self,func,force,info)
335 -- Find the function ref for 'func' (if force and not present, create one)
336 local ret = self.rawstats[func]
337 if ret == nil and force ~= 1 then
338 return nil
339 end
340 if ret == nil then
341 -- Build a new function statistics table
342 ret = {}
343 ret.func = func
344 ret.count = 0
345 ret.time = 0
346 ret.anon_child_time = 0
347 ret.name_child_time = 0
348 ret.children = {}
349 ret.children_time = {}
350 ret.func_info = info
351 self.rawstats[func] = ret
352 end
353 return ret
354end
355
356
357--
358-- This writes a profile report to the output file object. If
359-- sort_by_total_time is nil or false the output is sorted by
360-- the function time minus the time in it's children.
361--
362function _profiler.report( self, outfile, sort_by_total_time )
363
364 outfile:write
365 [[Lua Profile output created by profiler.lua. Copyright Pepperfish 2002+
366
367]]
368
369 -- This is pretty awful.
370 local terms = {}
371 if self.variant == "time" then
372 terms.capitalized = "Sample"
373 terms.single = "sample"
374 terms.pastverb = "sampled"
375 elseif self.variant == "call" then
376 terms.capitalized = "Call"
377 terms.single = "call"
378 terms.pastverb = "called"
379 else
380 assert(false)
381 end
382
383 local total_time = 0
384 local ordering = {}
385 for func,record in pairs(self.rawstats) do
386 table.insert(ordering, func)
387 end
388
389 if sort_by_total_time then
390 table.sort( ordering,
391 function(a,b) return self.rawstats[a].time > self.rawstats[b].time end
392 )
393 else
394 table.sort( ordering,
395 function(a,b)
396 local arec = self.rawstats[a]
397 local brec = self.rawstats[b]
398 local atime = arec.time - (arec.anon_child_time + arec.name_child_time)
399 local btime = brec.time - (brec.anon_child_time + brec.name_child_time)
400 return atime > btime
401 end
402 )
403 end
404
405 local len = #ordering
406 for i=1,len do
407 local func = ordering[i]
408 local record = self.rawstats[func]
409 local thisfuncname = " " .. self:_pretty_name(func) .. " "
410 if string.len( thisfuncname ) < 42 then
411 local nameLen = math.ceil((42 - string.len(thisfuncname))/2)
412 thisfuncname =
413 string.rep( "-", nameLen) .. thisfuncname
414 thisfuncname =
415 thisfuncname .. string.rep( "-", 42 - string.len(thisfuncname) )
416 end
417
418 total_time = total_time + ( record.time - ( record.anon_child_time +
419 record.name_child_time ) )
420 outfile:write( string.rep( "-", 19 ) .. thisfuncname ..
421 string.rep( "-", 19 ) .. "\n" )
422 outfile:write( terms.capitalized.." count: " ..
423 string.format( "%4d", record.count ) .. "\n" )
424 outfile:write( "Time spend total: " ..
425 string.format( "%4.3f", record.time ) .. "s\n" )
426 outfile:write( "Time spent in children: " ..
427 string.format("%4.3f",record.anon_child_time+record.name_child_time) ..
428 "s\n" )
429 local timeinself =
430 record.time - (record.anon_child_time + record.name_child_time)
431 outfile:write( "Time spent in self: " ..
432 string.format("%4.3f", timeinself) .. "s\n" )
433 outfile:write( "Time spent per " .. terms.single .. ": " ..
434 string.format("%4.5f", record.time/record.count) ..
435 "s/" .. terms.single .. "\n" )
436 outfile:write( "Time spent in self per "..terms.single..": " ..
437 string.format( "%4.5f", timeinself/record.count ) .. "s/" ..
438 terms.single.."\n" )
439
440 -- Report on each child in the form
441 -- Child <funcname> called n times and took a.bs
442 local added_blank = 0
443 for k,v in pairs(record.children) do
444 if self.prevented_functions[k] == nil or
445 self.prevented_functions[k] == 0
446 then
447 if added_blank == 0 then
448 outfile:write( "\n" ) -- extra separation line
449 added_blank = 1
450 end
451 outfile:write( "Child " .. self:_pretty_name(k) ..
452 string.rep( " ", 41-string.len(self:_pretty_name(k)) ) .. " " ..
453 terms.pastverb.." " .. string.format("%6d", v) )
454 outfile:write( " times. Took " ..
455 string.format("%4.2f", record.children_time[k] ) .. "s\n" )
456 end
457 end
458
459 outfile:write( "\n" ) -- extra separation line
460 outfile:flush()
461 end
462 outfile:write( "\n\n" )
463 outfile:write( "Total time spent in profiled functions: " ..
464 string.format("%5.3g",total_time) .. "s\n" )
465 outfile:write( [[
466
467END
468]] )
469 outfile:flush()
470end
471
472
473--
474-- This writes the profile to the output file object as
475-- loadable Lua source.
476--
477function _profiler.lua_report(self,outfile)
478 -- Purpose: Write out the entire raw state in a cross-referenceable form.
479 local ordering = {}
480 local functonum = {}
481 for func,record in pairs(self.rawstats) do
482 table.insert(ordering, func)
483 local len = #ordering
484 functonum[func] = len
485 end
486
487 outfile:write(
488 "-- Profile generated by profiler.lua Copyright Pepperfish 2002+\n\n" )
489 outfile:write( "-- Function names\nfuncnames = {}\n" )
490 local len = #ordering
491 for i=1,len do
492 local thisfunc = ordering[i]
493 outfile:write( "funcnames[" .. i .. "] = " ..
494 string.format("%q", self:_pretty_name(thisfunc)) .. "\n" )
495 end
496 outfile:write( "\n" )
497 outfile:write( "-- Function times\nfunctimes = {}\n" )
498
499 local len = #ordering
500 for i=1,len do
501 local thisfunc = ordering[i]
502 local record = self.rawstats[thisfunc]
503 outfile:write( "functimes[" .. i .. "] = { " )
504 outfile:write( "tot=" .. record.time .. ", " )
505 outfile:write( "achild=" .. record.anon_child_time .. ", " )
506 outfile:write( "nchild=" .. record.name_child_time .. ", " )
507 outfile:write( "count=" .. record.count .. " }\n" )
508 end
509 outfile:write( "\n" )
510 outfile:write( "-- Child links\nchildren = {}\n" )
511
512 local len = #ordering
513 for i=1,len do
514 local thisfunc = ordering[i]
515 local record = self.rawstats[thisfunc]
516 outfile:write( "children[" .. i .. "] = { " )
517 for k,v in pairs(record.children) do
518 if functonum[k] then -- non-recorded functions will be ignored now
519 outfile:write( functonum[k] .. ", " )
520 end
521 end
522 outfile:write( "}\n" )
523 end
524 outfile:write( "\n" )
525 outfile:write( "-- Child call counts\nchildcounts = {}\n" )
526
527 local len = #ordering
528 for i=1,len do
529 local thisfunc = ordering[i]
530 local record = self.rawstats[thisfunc]
531 outfile:write( "children[" .. i .. "] = { " )
532 for k,v in record.children do
533 if functonum[k] then -- non-recorded functions will be ignored now
534 outfile:write( v .. ", " )
535 end
536 end
537 outfile:write( "}\n" )
538 end
539 outfile:write( "\n" )
540 outfile:write( "-- Child call time\nchildtimes = {}\n" )
541 local len = #ordering
542 for i=1,len do
543 local thisfunc = ordering[i]
544 local record = self.rawstats[thisfunc];
545 outfile:write( "children[" .. i .. "] = { " )
546 for k,v in pairs(record.children) do
547 if functonum[k] then -- non-recorded functions will be ignored now
548 outfile:write( record.children_time[k] .. ", " )
549 end
550 end
551 outfile:write( "}\n" )
552 end
553 outfile:write( "\n\n-- That is all.\n\n" )
554 outfile:flush()
555end
556
557-- Internal function to calculate a pretty name for the profile output
558function _profiler._pretty_name(self,func)
559
560 -- Only the data collected during the actual
561 -- run seems to be correct.... why?
562 local info = self.rawstats[ func ].func_info
563 -- local info = debug.getinfo( func )
564
565 local name = ""
566 if info.what == "Lua" then
567 name = "L:"
568 end
569 if info.what == "C" then
570 name = "C:"
571 end
572 if info.what == "main" then
573 name = " :"
574 end
575
576 if info.name == nil then
577 name = name .. "<"..tostring(func) .. ">"
578 else
579 name = name .. info.name
580 end
581
582 if info.source then
583 name = name .. "@" .. info.source
584 else
585 if info.what == "C" then
586 name = name .. "@?"
587 else
588 name = name .. "@<string>"
589 end
590 end
591 name = name .. ":"
592 if info.what == "C" then
593 name = name .. "?"
594 else
595 name = name .. info.linedefined
596 end
597
598 return name
599end
600
601
602--
603-- This allows you to specify functions which you do
604-- not want profiled. Setting level to 1 keeps the
605-- function from being profiled. Setting level to 2
606-- keeps both the function and its children from
607-- being profiled.
608--
609-- BUG: 2 will probably act exactly like 1 in "time" mode.
610-- If anyone cares, let me (zorba) know and it can be fixed.
611--
612function _profiler.prevent(self, func, level)
613 self.prevented_functions[func] = (level or 1)
614end
615
616
617_profiler.prevented_functions = {
618 [_profiler.start] = 2,
619 [_profiler.stop] = 2,
620 [_profiler._internal_profile_by_time] = 2,
621 [_profiler._internal_profile_by_call] = 2,
622 [_profiler_hook_wrapper_by_time] = 2,
623 [_profiler_hook_wrapper_by_call] = 2,
624 [_profiler.prevent] = 2,
625 [_profiler._get_func_rec] = 2,
626 [_profiler.report] = 2,
627 [_profiler.lua_report] = 2,
628 [_profiler._pretty_name] = 2
629}
Note: See TracBrowser for help on using the repository browser.