forked from panda3d/panda3d
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFrameProfiler.py
More file actions
executable file
·186 lines (167 loc) · 7.85 KB
/
FrameProfiler.py
File metadata and controls
executable file
·186 lines (167 loc) · 7.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
from direct.directnotify.DirectNotifyGlobal import directNotify
from direct.fsm.StatePush import FunctionCall
from direct.showbase.PythonUtil import formatTimeExact, normalDistrib
from direct.task import Task
class FrameProfiler:
notify = directNotify.newCategory('FrameProfiler')
# because of precision requirements, all times related to the profile/log
# schedule are stored as integers
Minute = 60
Hour = 60 * Minute
Day = 24 * Hour
def __init__(self):
Hour = FrameProfiler.Hour
# how long to wait between frame profiles
self._period = 2 * FrameProfiler.Minute
if config.GetBool('frequent-frame-profiles', 0):
self._period = 1 * FrameProfiler.Minute
# used to prevent profile from being taken exactly every 'period' seconds
self._jitterMagnitude = self._period * .75
# when to log output
# each entry must be an integer multiple of all previous entries
# as well as an integer multiple of the period
self._logSchedule = [ 1 * FrameProfiler.Hour,
4 * FrameProfiler.Hour,
12 * FrameProfiler.Hour,
1 * FrameProfiler.Day,
] # day schedule proceeds as 1, 2, 4, 8 days, etc.
if config.GetBool('frequent-frame-profiles', 0):
self._logSchedule = [ 1 * FrameProfiler.Minute,
4 * FrameProfiler.Minute,
12 * FrameProfiler.Minute,
24 * FrameProfiler.Minute,
]
for t in self._logSchedule:
#assert isInteger(t)
# make sure the period is evenly divisible into each element of the log schedule
assert (t % self._period) == 0
# make sure each element of the schedule is evenly divisible into each subsequent element
for i in range(len(self._logSchedule)):
e = self._logSchedule[i]
for j in range(i, len(self._logSchedule)):
assert (self._logSchedule[j] % e) == 0
#assert isInteger(self._period)
self._enableFC = FunctionCall(self._setEnabled, taskMgr.getProfileFramesSV())
self._enableFC.pushCurrentState()
def destroy(self):
self._enableFC.set(False)
self._enableFC.destroy()
def _setEnabled(self, enabled):
if enabled:
self.notify.info('frame profiler started')
self._startTime = globalClock.getFrameTime()
self._profileCounter = 0
self._jitter = None
self._period2aggregateProfile = {}
self._id2session = {}
self._id2task = {}
# don't profile process startup
self._task = taskMgr.doMethodLater(self._period, self._scheduleNextProfileDoLater,
'FrameProfilerStart-%s' % serialNum())
else:
self._task.remove()
del self._task
for session in self._period2aggregateProfile.values():
session.release()
del self._period2aggregateProfile
for task in self._id2task.values():
task.remove()
del self._id2task
for session in self._id2session.values():
session.release()
del self._id2session
self.notify.info('frame profiler stopped')
def _scheduleNextProfileDoLater(self, task):
self._scheduleNextProfile()
return task.done
def _scheduleNextProfile(self):
self._profileCounter += 1
self._timeElapsed = self._profileCounter * self._period
#assert isInteger(self._timeElapsed)
time = self._startTime + self._timeElapsed
# vary the actual delay between profiles by a random amount to prevent interaction
# with periodic events
jitter = self._jitter
if jitter is None:
jitter = normalDistrib(-self._jitterMagnitude, self._jitterMagnitude)
time += jitter
else:
time -= jitter
jitter = None
self._jitter = jitter
sessionId = serialNum()
session = taskMgr.getProfileSession('FrameProfile-%s' % sessionId)
self._id2session[sessionId] = session
taskMgr.profileFrames(num=1, session=session, callback=Functor(
self._analyzeResults, sessionId))
# schedule the next profile
delay = max(time - globalClock.getFrameTime(), 0.)
self._task = taskMgr.doMethodLater(delay, self._scheduleNextProfileDoLater,
'FrameProfiler-%s' % serialNum())
def _analyzeResults(self, sessionId):
# do the analysis in a task 1) to separate the processing from the profiled frame,
# and 2) to get the processing to show up in a named task instead of in the taskMgr
self._id2task[sessionId] = taskMgr.add(
Functor(self._doAnalysis, sessionId), 'FrameProfilerAnalysis-%s' % sessionId)
def _doAnalysis(self, sessionId, task):
if hasattr(task, '_generator'):
gen = task._generator
else:
gen = self._doAnalysisGen(sessionId)
task._generator = gen
result = next(gen)
if result == Task.done:
del task._generator
return result
def _doAnalysisGen(self, sessionId):
# generator to limit max number of profile loggings per frame
p2ap = self._period2aggregateProfile
self._id2task.pop(sessionId)
session = self._id2session.pop(sessionId)
if session.profileSucceeded():
# always add this profile to the first aggregated profile
period = self._logSchedule[0]
if period not in self._period2aggregateProfile:
p2ap[period] = session.getReference()
else:
p2ap[period].aggregate(session)
else:
self.notify.warning('frame profile did not succeed')
session.release()
session = None
counter = 0
# log profiles when it's time, and aggregate them upwards into the
# next-longer profile
for pi in range(len(self._logSchedule)):
period = self._logSchedule[pi]
if (self._timeElapsed % period) == 0:
if period in p2ap:
# delay until the next frame if we've already processed N profiles this frame
if counter >= 3:
counter = 0
yield Task.cont
self.notify.info('aggregate profile of sampled frames over last %s\n%s' %
(formatTimeExact(period), p2ap[period].getResults()))
counter += 1
# aggregate this profile into the next larger profile
nextIndex = pi + 1
if nextIndex >= len(self._logSchedule):
# if we're adding a new period to the end of the log period table,
# set it to double the duration of the current longest period
nextPeriod = period * 2
self._logSchedule.append(nextPeriod)
else:
nextPeriod = self._logSchedule[nextIndex]
if nextPeriod not in p2ap:
p2ap[nextPeriod] = p2ap[period].getReference()
else:
p2ap[nextPeriod].aggregate(p2ap[period])
# this profile is now represented in the next larger profile
# throw it out
p2ap[period].release()
del p2ap[period]
else:
# current time is not divisible evenly into selected period, and all higher
# periods are multiples of this one
break
yield Task.done