-
Notifications
You must be signed in to change notification settings - Fork 366
Expand file tree
/
Copy pathperf_graph.py
More file actions
332 lines (279 loc) · 11.8 KB
/
perf_graph.py
File metadata and controls
332 lines (279 loc) · 11.8 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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
import random
import pyglet.clock
# Pyright can't figure out the dynamic import for the backends in Pyglet
from pyglet.graphics import Batch # type: ignore
from pyglet.shapes import Line
import arcade
from arcade.types import RGBA255, Color
__all__ = ["PerfGraph"]
class PerfGraph(arcade.Sprite):
"""
An auto-updating line chart of FPS or event handler execution times.
You must use :func:`arcade.enable_timings` to turn on performance
tracking for the chart to display data.
Aside from instantiation and updating the chart, this class behaves
like other :class:`arcade.Sprite` instances. You can use it with
:class:`SpriteList <arcade.SpriteList>` normally. See
:ref:`performance_statistics_example` for an example of how to use
this class.
Unlike other :class:`Sprite <arcade.Sprite>` instances, this class
neither loads an :class:`arcade.Texture` nor accepts one as a
constructor argument. Instead, it creates a new internal
:class:`Texture <arcade.Texture>` instance. The chart is
automatically redrawn to this internal
:class:`Texture <arcade.Texture>` every ``update_rate`` seconds.
Args:
width (int):
The width of the chart texture in pixels
height (int):
The height of the chart texture in pixels
graph_data (str):
The pyglet event handler or statistic to track
update_rate (float):
How often the graph updates, in seconds
background_color (RGBA255):
The background color of the chart
data_line_color (RGBA255):
Color of the line tracking drawn
axis_color (RGBA255):
The color to draw the x & y axes in
font_color (RGBA255):
The color of the label font
font_size (int):
The size of the label font in points
y_axis_num_lines (int):
How many grid lines should be used to divide the y scale of the graph.
view_y_scale_step (float):
The graph's view area will be scaled to a multiple of this value to
fit to the data currently displayed.
"""
def __init__(
self,
width: int,
height: int,
graph_data: str = "FPS",
update_rate: float = 0.1,
background_color: RGBA255 = arcade.color.BLACK,
data_line_color: RGBA255 = arcade.color.WHITE,
axis_color: RGBA255 = arcade.color.DARK_YELLOW,
grid_color: RGBA255 = arcade.color.DARK_YELLOW,
font_color: RGBA255 = arcade.color.WHITE,
font_size: int = 10,
y_axis_num_lines: int = 4,
view_y_scale_step: float = 20.0,
):
unique_id = str(random.random())
self.minimap_texture = arcade.Texture.create_empty(unique_id, (width, height))
super().__init__(self.minimap_texture)
self.proj = 0, self.width, 0, self.height
# The data line is redrawn each update by a function that does
# not cache vertices, so there is no need to make this attribute
# a property that updates geometry when set.
self.line_color = Color.from_iterable(data_line_color)
"""The color of the line tracking the data."""
# Store visual style info for cached pyglet shape geometry
self._background_color = Color.from_iterable(background_color)
self._grid_color = Color.from_iterable(grid_color)
self._axis_color = Color.from_iterable(axis_color)
self._font_color = Color.from_iterable(font_color)
self._font_size = font_size
self._y_axis_num_lines = y_axis_num_lines
self._left_x = 25
self._bottom_y = 15
# Variables for rendering the data line
self.graph_data = graph_data
"""The graphed data type, either "FPS" or a pyglet event handler name."""
self._data_to_graph: list[float] = []
self._view_max_value = 0.0 # We'll calculate this once we have data
self._view_y_scale_step = view_y_scale_step
self._view_height = self._texture.height - self._bottom_y # type: ignore
self._y_increment = self._view_height / self._y_axis_num_lines
# Set up internal Text object & line caches
self._pyglet_batch = Batch() # Used to draw graph elements
# Convenient storage for iteration during color updates
self._vertical_axis_text_objects: list[arcade.Text] = []
self._all_text_objects: list[arcade.Text] = []
self._grid_lines: list[Line] = []
# Create the bottom label text object
self._bottom_label = arcade.Text(
graph_data,
0,
2,
self._font_color,
self._font_size,
align="center",
width=int(width),
)
self._all_text_objects.append(self._bottom_label)
# Create the axes
self._x_axis = Line(
self._left_x,
self._bottom_y,
self._left_x,
height,
batch=self._pyglet_batch,
color=self._axis_color,
)
self._y_axis = Line(
self._left_x,
self._bottom_y,
width,
self._bottom_y,
batch=self._pyglet_batch,
color=self._axis_color,
)
# Create the Y scale text objects & lines
for i in range(self._y_axis_num_lines):
y_level = self._bottom_y + self._y_increment * i
self._vertical_axis_text_objects.append(
arcade.Text(
"0", # Ensure the lowest y axis label is always 0
self._left_x,
int(y_level),
self._font_color,
self._font_size,
anchor_x="right",
anchor_y="center",
)
)
self._grid_lines.append(
Line(
self._left_x,
y_level,
width,
y_level,
batch=self._pyglet_batch,
color=self._grid_color,
)
)
self._all_text_objects.extend(self._vertical_axis_text_objects)
# Enable auto-update
pyglet.clock.schedule_interval(self.update_graph, update_rate)
@property
def background_color(self) -> Color:
"""Get or set the background color of the graph."""
return self._background_color
@background_color.setter
def background_color(self, new_color: RGBA255):
self._background_color = Color.from_iterable(new_color)
@property
def grid_color(self) -> Color:
"""Get or set the color of the grid lines."""
return self._grid_color
@grid_color.setter
def grid_color(self, raw_color: RGBA255):
new_color = Color.from_iterable(raw_color)
for grid_line in self._grid_lines:
grid_line.color = new_color
@property
def axis_color(self) -> Color:
"""Get or set the color of the x and y axes."""
return self._axis_color
@axis_color.setter
def axis_color(self, raw_color: RGBA255):
new_color = Color.from_iterable(raw_color)
self._x_axis.color = new_color
self._y_axis.color = new_color
@property
def font_size(self) -> int:
"""Get or set the font size of the labels."""
return self._font_size
@font_size.setter
def font_size(self, new: int):
self._font_size = new
for text in self._all_text_objects:
text.font_size = new
@property
def font_color(self) -> Color:
"""Get or set the font color of the labels."""
return self._font_color
@font_color.setter
def font_color(self, raw_color: Color):
new_color = Color.from_iterable(raw_color)
self._font_color = new_color
for text in self._all_text_objects:
text.color = new_color
def remove_from_sprite_lists(self) -> None:
"""
Remove the sprite from all lists and cancel the update event.
"""
super().remove_from_sprite_lists()
# It is very important to call this to prevent potential
# issues such as crashes or excess memory use from failed
# garbage collection.
pyglet.clock.unschedule(self.update)
def update_graph(self, delta_time: float) -> None:
"""
Update the graph by redrawing the internal texture data.
.. warning:: You do not need to call this method! It will be
called automatically!
Args:
delta_time: Elapsed time in seconds. Passed by the pyglet
scheduler.
"""
# Skip update if there is no SpriteList that can draw this graph
if self.sprite_lists is None or len(self.sprite_lists) == 0:
return
sprite_list = self.sprite_lists[0]
atlas = sprite_list.atlas
# Clear and return if timings are disabled
if not arcade.timings_enabled():
# Please forgive the ugly spacing. It makes type checking work.
with atlas.render_into(self.minimap_texture, projection=self.proj) as fbo: # type: ignore
fbo.clear(color=(0, 0, 0, 255))
return
# Get FPS and add to our historical data
data_to_graph = self._data_to_graph
graph_data = self.graph_data
if graph_data == "FPS":
data_to_graph.append(arcade.get_fps())
else:
timings = arcade.get_timings()
if graph_data in timings:
timing_list = timings[self.graph_data]
avg_timing = sum(timing_list) / len(timing_list)
data_to_graph.append(avg_timing * 1000)
# Skip update if there is no data to graph
if len(data_to_graph) == 0:
return
# Using locals for frequently used values is faster than
# looking up instance variables repeatedly.
bottom_y = self._bottom_y
left_x = self._left_x
view_y_scale_step = self._view_y_scale_step
vertical_axis_text_objects = self._vertical_axis_text_objects
view_height = self._view_height
# We have to render at the internal texture's original size to
# prevent distortion and bugs when the sprite is scaled.
texture_width, texture_height = self._texture.size # type: ignore
# Toss old data by removing leftmost entries
while len(data_to_graph) > texture_width - left_x:
data_to_graph.pop(0)
# Calculate the value at the top of the chart
max_value = max(data_to_graph)
view_max_value = ((max_value + 1.5) // view_y_scale_step + 1) * view_y_scale_step
# Calculate draw positions of each pixel on the data line
point_list = []
x = left_x
for reading in data_to_graph:
y = (reading / view_max_value) * view_height + bottom_y
point_list.append((x, y))
x += 1
# Update the view scale & labels if needed
if view_max_value != self._view_max_value:
self._view_max_value = view_max_value
view_y_legend_increment = self._view_max_value // self._y_axis_num_lines
for index in range(1, len(vertical_axis_text_objects)):
text_object = vertical_axis_text_objects[index]
text_object.text = f"{int(index * view_y_legend_increment)}"
# Render to the internal texture
# This ugly spacing is intentional to make type checking work.
with atlas.render_into(self.minimap_texture, projection=self.proj) as fbo: # type: ignore
# Set the background color
fbo.clear(color=self.background_color)
# Draw lines & their labels
for text in self._all_text_objects:
text.draw()
self._pyglet_batch.draw()
# Draw the data line
arcade.draw_line_strip(point_list, self.line_color)