diff --git a/.gitignore b/.gitignore index 80de261..724848b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,8 @@ *.csv *.svg /.vscode/ -arcade.egg-info/ -arcade/__pycache__/ +*egg-info* +__pycache__ dist/ build/ .coverage diff --git a/README.md b/README.md new file mode 100644 index 0000000..d92b31f --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Arcade Performance Tests + +Various performance tests for arcade. +We are in many cases comparing the performance with pygame. + +## Installing requirements + +Preferably in a virtual environment: + +```bash +$ pip install -r requirements.txt +``` + +## Running Tests + +Run ``src/__main__.py`` which should: + +* Run all tests +* Generate all graphs +* Generate all documents + +## Step 3 + +Look at the resulting documents in ``doc/build`` diff --git a/arcade_perf/__init__.py b/arcade_perf/__init__.py new file mode 100644 index 0000000..941b750 --- /dev/null +++ b/arcade_perf/__init__.py @@ -0,0 +1,17 @@ +import os +from pathlib import Path +import arcade + +# Root of the repository +PACKAGE_ROOT= Path(__file__).parent.resolve() +# Package directory +PROJECT_ROOT = PACKAGE_ROOT.parent +# Resources directory +RESOURCES_ROOT = PACKAGE_ROOT / "resources" +# Output directory +OUT_DIR = PROJECT_ROOT / "output" + +arcade.resources.add_resource_handle("textures", RESOURCES_ROOT) + +# We don't want pygame support message in logs +os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide" diff --git a/arcade_perf/__main__.py b/arcade_perf/__main__.py new file mode 100644 index 0000000..3f3d2cc --- /dev/null +++ b/arcade_perf/__main__.py @@ -0,0 +1,44 @@ +import sys +import argparse +from datetime import datetime +from arcade_perf.manager import TestManager + + +def main(): + args = parse_args(sys.argv[1:]) + print(f"Session name: '{args.session}'") + manager = TestManager(args.session, debug=True) + manager.find_test_classes(args.type, args.name) + manager.create_test_instances() + manager.run() + + # -- Graphs -- + # draw_stationary_sprites [pygame, arcade] + + # draw_moving_sprites [pygame, arcade[basic, sprite]] + + # collision + # Time To Detect Collisions + # - arcade.collision-2 Arcade GPU + # - arcade.collision-3 Arcade Simple + + # shapes + + +# run -s test -t arcade, -n collision +def parse_args(args): + parser = argparse.ArgumentParser() + parser.add_argument( + "-s", + "--session", + help="Session name", + type=str, + default=datetime.now().strftime("%Y-%m-%dT%H-%M-%S"), + ) + parser.add_argument("-t", "--type", help="Test type", type=str) + parser.add_argument("-n", "--name", help="Test name", type=str) + return parser.parse_args(args) + + +if __name__ == "__main__": + main() diff --git a/arcade_perf/graph.py b/arcade_perf/graph.py new file mode 100644 index 0000000..34312ab --- /dev/null +++ b/arcade_perf/graph.py @@ -0,0 +1,83 @@ +import csv +from pathlib import Path +import matplotlib.pyplot as plt +import seaborn as sns + +sns.set_style("whitegrid") + +FPS = 1 +SPRITE_COUNT = 2 +DRAWING_TIME = 3 +PROCESSING_TIME = 4 + +class DataSeries: + + def __init__(self, name: str, path: Path) -> None: + self.name = name + self.path = path + # Data + self.count = [] + self.processing_time = [] + self.draw_time = [] + self.fps = [] + # Process data + self._process_data() + + def _process_data(self): + rows = self._read_file(self.path) + for row in rows: + self.count.append(row[SPRITE_COUNT]) + self.fps.append(row[FPS]) + self.processing_time.append(row[PROCESSING_TIME]) + self.draw_time.append(row[DRAWING_TIME]) + + def _read_file(self, path: Path): + results = [] + with open(path) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + first_row = True + for row in csv_reader: + if first_row: + first_row = False + else: + results.append([float(cell) for cell in row]) + + return results + +class PerfGraph: + + def __init__(self, title: str, label_x: str, label_y: str) -> None: + self.title = title + self.label_x = label_x + self.label_y = label_y + self.series = [] + + def add_series(self, series: DataSeries): + self.series.append(series) + + def create(self, output_path: Path): + plt.title(self.title) + + for series in self.series: + plt.plot(series.count, series.processing_time, label=series.name) + + plt.legend(loc='upper left', shadow=True, fontsize='large') + plt.xlabel(self.label_x) + plt.ylabel(self.label_y) + + plt.savefig(output_path) + plt.clf() + + +if __name__ == "__main__": + from arcade_perf import OUT_DIR + OUTPUT_ROOT = OUT_DIR / "test" / "graphs" + OUTPUT_ROOT.mkdir(parents=True, exist_ok=True) + path = OUT_DIR / "test" / "data" + + graph = PerfGraph("Time To Detect Collisions", label_x="Sprite Count", label_y="Time") + graph.add_series(DataSeries("Arcade 0", path / "arcade_collision-0.csv")) + graph.add_series(DataSeries("Arcade 1", path / "arcade_collision-1.csv")) + graph.add_series(DataSeries("Arcade 2", path / "arcade_collision-2.csv")) + graph.add_series(DataSeries("Arcade 3", path / "arcade_collision-3.csv")) + graph.create(OUTPUT_ROOT / "arcade_collision.png") diff --git a/arcade_perf/manager.py b/arcade_perf/manager.py new file mode 100644 index 0000000..41cc53a --- /dev/null +++ b/arcade_perf/manager.py @@ -0,0 +1,154 @@ +import importlib +import pkgutil +from typing import List, Type, Optional + +from arcade_perf.graph import DataSeries, PerfGraph +from arcade_perf import OUT_DIR +from arcade_perf.tests.base import PerfTest + + +def find_test_classes(path: str) -> List[Type[PerfTest]]: + """Find all test classes in submodules""" + target_module = importlib.import_module(f"arcade_perf.tests.{path}") + + classes = [] + for v in pkgutil.iter_modules(target_module.__path__): + module = importlib.import_module(f"arcade_perf.tests.{path}.{v.name}") + if hasattr(module, "Test"): + classes.append(module.Test) + else: + print(( + "WARNING: " + f"Module '{module.__name__}' does not have a Test class. " + "Please add a test class or rename the class to 'Test'." + )) + + return classes + + +class TestManager: + """ + Finds and executes tests + + :param str session: The session name. + :param bool debug: If True, print debug messages. + """ + def __init__(self, session: str, debug: bool = True): + self.debug = debug + self.session = session + self.session_dir = OUT_DIR / session + self.session_dir.mkdir(parents=True, exist_ok=True) + self.data_dir = self.session_dir / "data" + + self.test_classes: List[Type[PerfTest]] = [] + self.test_instances: List[PerfTest] = [] + + @property + def num_test_classes(self) -> int: + return len(self.test_classes) + + @property + def num_test_instances(self) -> int: + return len(self.test_instances) + + def find_test_classes( + self, + type: Optional[str] = None, + name: Optional[str] = None, + ): + """ + Find test classes based on type and name. + + :param str type: The type of test to run. + :param str name: The name of the test to run. + :return: The number of test classes found. + """ + all_classes = find_test_classes("arcade") + all_classes += find_test_classes("pygame") + all_classes += find_test_classes("pyglet") + + for cls in all_classes: + if type is not None and cls.type != type: + continue + if name is not None and cls.name != name: + continue + self.test_classes.append(cls) + + if self.debug: + num_classes = len(self.test_classes) + print(f"Found {num_classes} test classes") + for cls in self.test_classes: + print(f" -> {cls.type}.{cls.name}") + + def create_test_instances(self): + """ + Create test instances based on each test's instances attribute. + """ + for cls in self.test_classes: + # If a test have multiple instances, create one instance for each + if cls.instances: + for params, _ in cls.instances: + self.add_test_instance(cls(**params)) + else: + self.add_test_instance(cls()) + + if self.debug: + num_instances = len(self.test_instances) + print(f"Created {num_instances} test instances") + for instance in self.test_instances: + print(f" -> {instance.type}.{instance.name}") + + def add_test_instance(self, instance: PerfTest): + """Validate instance""" + if instance.name == "default": + raise ValueError(( + "Test name cannot be 'default'." + "Please add a class attribute 'name' to your test class." + f"Class: {instance}" + )) + self.test_instances.append(instance) + + def get_test_instance(self, name: str) -> Optional[PerfTest]: + for instance in self.test_instances: + if instance.instance_name == name: + return instance + + def run(self): + """Run all tests""" + for instance in self.test_instances: + instance.run(self.session_dir) + + def create_graph( + self, + file_name: str, + title: str, + x_label: str, + y_label: str, + series_names = [], + ): + """Create a graph using matplotlib""" + print("Creating graph : {title}} [{x_label}, {y_label}]}]") + series = [] + skip = False + for _series in series_names: + # Check if we have a test instance with this name + instance = self.get_test_instance(_series) + if instance is None: + print(f" -> No test instance found for series '{_series}'") + skip = True + + path = self.data_dir / f"{_series}.csv" + if not path.exists(): + print(f"No data found for series '{_series}' in session '{self.session}'") + skip = True + + if skip: + continue + + series.append(DataSeries(instance.name, path)) + + out_path = self.session_dir / "graphs" + out_path.mkdir(parents=True, exist_ok=True) + out_path = out_path / f"{file_name}.png" + graph = PerfGraph(title, x_label, y_label, series) + graph.create(out_path) diff --git a/src/resources/coinGold.png b/arcade_perf/resources/coinGold.png similarity index 100% rename from src/resources/coinGold.png rename to arcade_perf/resources/coinGold.png diff --git a/src/resources/femalePerson_idle.png b/arcade_perf/resources/femalePerson_idle.png similarity index 100% rename from src/resources/femalePerson_idle.png rename to arcade_perf/resources/femalePerson_idle.png diff --git a/arcade_perf/tests/arcade/collision.py b/arcade_perf/tests/arcade/collision.py new file mode 100644 index 0000000..1ba5c9a --- /dev/null +++ b/arcade_perf/tests/arcade/collision.py @@ -0,0 +1,102 @@ +import arcade +import random +from arcade_perf.tests.base import ArcadePerfTest + +SPRITE_SCALING_COIN = 0.09 +SPRITE_SCALING_PLAYER = 0.5 +SPRITE_NATIVE_SIZE = 128 +SPRITE_SIZE = int(SPRITE_NATIVE_SIZE * SPRITE_SCALING_COIN) +SCREEN_WIDTH = 1800 +SCREEN_HEIGHT = 1000 +SCREEN_TITLE = "Moving Sprite Stress Test - Arcade" +USE_SPATIAL_HASHING = True +DEFAULT_METHOD = 3 + + +class Test(ArcadePerfTest): + name = "collision" + instances = ( + ({"method": 0}, "Arcade Auto"), + ({"method": 1}, "Arcade Spatial"), + ({"method": 2}, "Arcade GPU"), + ({"method": 3}, "Arcade Simple"), + ) + + def __init__(self, method: int = DEFAULT_METHOD): + super().__init__( + size=(SCREEN_WIDTH, SCREEN_HEIGHT), + title=SCREEN_TITLE, + start_count=0, + increment_count=1000, + duration=60.0, + ) + self.method = method + self.name = f"collision-{self.method}" + self.series_name = self.get_instance_name(method=self.method) + + # Variables that will hold sprite lists + self.coin_list = None + self.player_list = None + self.player = None + + def setup(self): + self.window.background_color = arcade.color.AMAZON + self.coin_texture = arcade.load_texture(":resources:images/items/coinGold.png") + # Sprite lists + self.coin_list = arcade.SpriteList(use_spatial_hash=USE_SPATIAL_HASHING) + self.player_list = arcade.SpriteList() + self.player = arcade.Sprite( + ":resources:images/animated_characters/female_person/femalePerson_idle.png", + scale=SPRITE_SCALING_PLAYER, + ) + self.player.center_x = random.randrange(SCREEN_WIDTH) + self.player.center_y = random.randrange(SCREEN_HEIGHT) + self.player.change_x = 3 + self.player.change_y = 5 + self.player_list.append(self.player) + + def add_coins(self, amount): + """Add a new set of coins""" + for i in range(amount): + coin = arcade.Sprite( + self.coin_texture, + center_x=random.randrange(SPRITE_SIZE, SCREEN_WIDTH - SPRITE_SIZE), + center_y=random.randrange(SPRITE_SIZE, SCREEN_HEIGHT - SPRITE_SIZE), + scale=SPRITE_SCALING_COIN, + ) + self.coin_list.append(coin) + + def on_draw(self): + super().on_draw() + self.coin_list.draw() + self.player_list.draw() + + def on_update(self, delta_time: float): + super().on_update(delta_time) + + self.player_list.update() + if self.player.center_x < 0 and self.player.change_x < 0: + self.player.change_x *= -1 + if self.player.center_y < 0 and self.player.change_y < 0: + self.player.change_y *= -1 + + if self.player.center_x > SCREEN_WIDTH and self.player.change_x > 0: + self.player.change_x *= -1 + if self.player.center_y > SCREEN_HEIGHT and self.player.change_y > 0: + self.player.change_y *= -1 + + coin_hit_list = arcade.check_for_collision_with_list(self.player, self.coin_list, method=self.method) + for coin in coin_hit_list: + coin.center_x = random.randrange(SCREEN_WIDTH) + coin.center_y = random.randrange(SCREEN_HEIGHT) + + def update_state(self): + # Figure out if we need more coins + if self.timing.target_n > len(self.coin_list): + new_coin_amount = self.timing.target_n - len(self.coin_list) + self.add_coins(new_coin_amount) + self.coin_list.write_sprite_buffers_to_gpu() + + +if __name__ == "__main__": + Test().run_test() diff --git a/arcade_perf/tests/arcade/moving_shapes.py b/arcade_perf/tests/arcade/moving_shapes.py new file mode 100644 index 0000000..6154c85 --- /dev/null +++ b/arcade_perf/tests/arcade/moving_shapes.py @@ -0,0 +1,228 @@ +import random + +from arcade_perf.tests.base import ArcadePerfTest +import arcade + +# Set up the constants +SCREEN_WIDTH = 1800 +SCREEN_HEIGHT = 1000 +SCREEN_TITLE = "Arcade - Moving Shapes Non-Buffered" + +RECT_WIDTH = 50 +RECT_HEIGHT = 50 + + +class Shape: + """ Generic base shape class """ + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + self.x = x + self.y = y + self.width = width + self.height = height + self.angle = angle + self.delta_x = delta_x + self.delta_y = delta_y + self.delta_angle = delta_angle + self.color = color + + def move(self): + self.x += self.delta_x + self.y += self.delta_y + self.angle += self.delta_angle + if self.x < 0 and self.delta_x < 0: + self.delta_x *= -1 + if self.y < 0 and self.delta_y < 0: + self.delta_y *= -1 + if self.x > SCREEN_WIDTH and self.delta_x > 0: + self.delta_x *= -1 + if self.y > SCREEN_HEIGHT and self.delta_y > 0: + self.delta_y *= -1 + + +class Ellipse(Shape): + + def draw(self): + arcade.draw_ellipse_filled(self.x, self.y, self.width, self.height, + self.color, self.angle) + + +class Rectangle(Shape): + + def draw(self): + arcade.draw_rectangle_filled(self.x, self.y, self.width, self.height, + self.color, self.angle) + + +class Line(Shape): + + def draw(self): + arcade.draw_line(self.x, self.y, + self.x + self.width, self.y + self.height, + self.color, 2) + + +class ShapeBuffered: + """ Generic base shape class """ + + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + self.x = x + self.y = y + self.width = width + self.height = height + self.angle = angle + self.delta_x = delta_x + self.delta_y = delta_y + self.delta_angle = delta_angle + self.color = color + self.shape_list = None + + def move(self): + self.x += self.delta_x + self.y += self.delta_y + if self.delta_angle: + self.angle += self.delta_angle + if self.x < 0 and self.delta_x < 0: + self.delta_x *= -1 + if self.y < 0 and self.delta_y < 0: + self.delta_y *= -1 + if self.x > SCREEN_WIDTH and self.delta_x > 0: + self.delta_x *= -1 + if self.y > SCREEN_HEIGHT and self.delta_y > 0: + self.delta_y *= -1 + + def draw(self): + self.shape_list.center_x = self.x + self.shape_list.center_y = self.y + # self.shape_list.angle = self.angle + self.shape_list.draw() + + +class EllipseBuffered(ShapeBuffered): + + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + + super().__init__(x, y, width, height, angle, delta_x, delta_y, + delta_angle, color) + + shape = arcade.create_ellipse_filled(0, 0, + self.width, self.height, + self.color, self.angle) + self.shape_list = arcade.ShapeElementList() + self.shape_list.append(shape) + + +class RectangleBuffered(ShapeBuffered): + + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + + super().__init__(x, y, width, height, angle, delta_x, delta_y, + delta_angle, color) + + shape = arcade.shape_list.create_rectangle_filled(0, 0, + self.width, self.height, + self.color, self.angle) + self.shape_list = arcade.shape_list.ShapeElementList() + self.shape_list.append(shape) + + +class LineBuffered(ShapeBuffered): + + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + + super().__init__(x, y, width, height, angle, delta_x, delta_y, + delta_angle, color) + + shape = arcade.shape_list.create_line(0, 0, + self.width, self.height, + self.color, 2) + self.shape_list = arcade.shape_list.ShapeElementList() + self.shape_list.append(shape) + + +class Test(ArcadePerfTest): + name = "moving-shapes" + instances = [ + ({"mode": "buffered"}, "Shapes Unbuffered"), + ({"mode": "unbuffered"}, "Shapes buffered"), + ] + + def __init__(self, mode: str = "buffered"): + super().__init__( + size=(SCREEN_WIDTH, SCREEN_HEIGHT), + title=SCREEN_TITLE, + start_count=0, + increment_count=20, + duration=60.0, + ) + self.shape_list = None + self.buffered = True if mode == "buffered" else False + self.name = f"{self.name}-{mode}" + + def setup(self): + """ Set up the game and initialize the variables. """ + self.shape_list = [] + + def add_shapes(self, amount): + for i in range(amount): + x = random.randrange(0, SCREEN_WIDTH) + y = random.randrange(0, SCREEN_HEIGHT) + width = random.randrange(10, 30) + height = random.randrange(10, 30) + # angle = random.randrange(0, 360) + angle = 0 + + d_x = random.randrange(-3, 4) + d_y = random.randrange(-3, 4) + # d_angle = random.randrange(-3, 4) + d_angle = 0 + + red = random.randrange(256) + green = random.randrange(256) + blue = random.randrange(256) + alpha = random.randrange(256) + + shape_type = random.randrange(1) + # shape_type = 1 + + if not self.buffered: + if shape_type == 0: + shape = Rectangle(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + elif shape_type == 1: + shape = Ellipse(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + elif shape_type == 2: + shape = Line(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + else: + if shape_type == 0: + shape = RectangleBuffered(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + elif shape_type == 1: + shape = EllipseBuffered(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + elif shape_type == 2: + shape = LineBuffered(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + + self.shape_list.append(shape) + + def on_update(self, dt): + """ Move everything """ + for shape in self.shape_list: + shape.move() + + def update_state(self): + # Figure out if we need more coins + if self.timing.target_n > len(self.shape_list): + new_coin_amount = self.timing.target_n - len(self.shape_list) + self.add_shapes(new_coin_amount) + + def on_draw(self): + for shape in self.shape_list: + shape.draw() diff --git a/arcade_perf/tests/arcade/moving_shapes2.py b/arcade_perf/tests/arcade/moving_shapes2.py new file mode 100644 index 0000000..87e4be4 --- /dev/null +++ b/arcade_perf/tests/arcade/moving_shapes2.py @@ -0,0 +1,130 @@ +import random +import arcade +import arcade.shape_list +from arcade_perf.tests.base import ArcadePerfTest + +# Set up the constants +SCREEN_WIDTH = 1800 +SCREEN_HEIGHT = 1000 +SCREEN_TITLE = "Arcade - Moving Shapes" + +RECT_WIDTH = 50 +RECT_HEIGHT = 50 + + +class Shape: + """ Generic base shape class """ + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + self.x = x + self.y = y + self.width = width + self.height = height + self.angle = angle + self.delta_x = delta_x + self.delta_y = delta_y + self.delta_angle = delta_angle + self.color = color + + def move(self): + self.x += self.delta_x + self.y += self.delta_y + self.angle += self.delta_angle + if self.x < 0 and self.delta_x < 0: + self.delta_x *= -1 + if self.y < 0 and self.delta_y < 0: + self.delta_y *= -1 + if self.x > SCREEN_WIDTH and self.delta_x > 0: + self.delta_x *= -1 + if self.y > SCREEN_HEIGHT and self.delta_y > 0: + self.delta_y *= -1 + + +class Ellipse(Shape): + + def get_shape(self): + shape = arcade.shape_list.create_ellipse_filled(self.x, self.y, + self.width, self.height, + self.color, self.angle) + return shape + + +class Rectangle(Shape): + + def get_shape(self): + shape = arcade.shape_list.create_rectangle_filled(self.x, self.y, + self.width, self.height, + self.color, self.angle) + return shape + + + +class Test(ArcadePerfTest): + name = "moving-shapes2" + instances = [ + ({"mode": "buffered"}, "Unbuffered"), + ({"mode": "unbuffered"}, "Buffered"), + ] + + def __init__(self, mode: str): + super().__init__( + size=(SCREEN_WIDTH, SCREEN_HEIGHT), + title=SCREEN_TITLE, + start_count=0, + increment_count=20, + duration=60, + ) + self.shape_list = None + self.buffered = True if mode == "buffered" else False + self.name = f"{self.name}-{mode}" + + def setup(self): + """ Set up the game and initialize the variables. """ + self.shape_list = [] + + def add_shapes(self, amount): + for i in range(amount): + x = random.randrange(0, SCREEN_WIDTH) + y = random.randrange(0, SCREEN_HEIGHT) + width = random.randrange(10, 30) + height = random.randrange(10, 30) + angle = random.randrange(0, 360) + + d_x = random.randrange(-3, 4) + d_y = random.randrange(-3, 4) + d_angle = random.randrange(-3, 4) + + red = random.randrange(256) + green = random.randrange(256) + blue = random.randrange(256) + alpha = random.randrange(256) + + shape_type = random.randrange(2) + # shape_type = 1 + + if shape_type == 0: + shape = Rectangle(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + elif shape_type == 1: + shape = Ellipse(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + + self.shape_list.append(shape) + + def on_update(self, dt): + for shape in self.shape_list: + shape.move() + + def update_state(self): + if self.timing.target_n > len(self.shape_list): + new_coin_amount = self.timing.target_n - len(self.shape_list) + self.add_shapes(new_coin_amount) + + def on_draw(self): + self.window.clear() + + shape_element_list = arcade.shape_list.ShapeElementList() + for shape in self.shape_list: + shape_element_list.append(shape.get_shape()) + + shape_element_list.draw() diff --git a/arcade_perf/tests/arcade/moving_sprites.py b/arcade_perf/tests/arcade/moving_sprites.py new file mode 100644 index 0000000..93898fd --- /dev/null +++ b/arcade_perf/tests/arcade/moving_sprites.py @@ -0,0 +1,83 @@ +""" +Moving Sprite Stress Test + +Simple program to test how fast we can draw sprites that are moving + +Artwork from https://kenney.nl +""" +import random +import arcade +from arcade_perf.tests.base import ArcadePerfTest + +# --- Constants --- +SPRITE_SCALING_COIN = 0.25 +SPRITE_NATIVE_SIZE = 128 +SPRITE_SIZE = int(SPRITE_NATIVE_SIZE * SPRITE_SCALING_COIN) +SCREEN_WIDTH = 1800 +SCREEN_HEIGHT = 1000 +SCREEN_TITLE = "Arcade - Moving Sprite Stress Test" + + +class Coin(arcade.Sprite): + + def update(self): + """ + Update the sprite. + """ + self.position = ( + self.position[0] + self.change_x, + self.position[1] + self.change_y, + ) + + +class Test(ArcadePerfTest): + name = "moving-sprites" + coin_cls = Coin + + def __init__(self): + """ Initializer """ + super().__init__( + size=(SCREEN_WIDTH, SCREEN_HEIGHT), + title=SCREEN_TITLE, + ) + + def add_coins(self, amount): + """Add mount coins to the spritelist""" + for _ in range(amount): + coin = self.coin_cls( + self.coin_texture, + center_x=random.randrange(SPRITE_SIZE, SCREEN_WIDTH - SPRITE_SIZE), + center_y=random.randrange(SPRITE_SIZE, SCREEN_HEIGHT - SPRITE_SIZE), + scale=SPRITE_SCALING_COIN + ) + coin.change_x = random.randrange(-3, 4) + coin.change_y = random.randrange(-3, 4) + self.coin_list.append(coin) + + def setup(self): + """ Set up the game and initialize the variables. """ + self.coin_texture = arcade.load_texture(":textures:coinGold.png") + self.window.background_color = arcade.color.AMAZON + self.coin_list = arcade.SpriteList(use_spatial_hash=False) + + def on_draw(self): + self.coin_list.draw() + + def on_update(self, delta_time): + self.coin_list.update() + + for sprite in self.coin_list: + if sprite.position[0] < 0: + sprite.change_x *= -1 + elif sprite.position[0] > SCREEN_WIDTH: + sprite.change_x *= -1 + if sprite.position[1] < 0: + sprite.change_y *= -1 + elif sprite.position[1] > SCREEN_HEIGHT: + sprite.change_y *= -1 + + def update_state(self): + # Figure out if we need more coins + if self.timing.target_n > len(self.coin_list): + new_coin_amount = self.timing.target_n - len(self.coin_list) + self.add_coins(new_coin_amount) diff --git a/arcade_perf/tests/arcade/moving_sprites_basic.py b/arcade_perf/tests/arcade/moving_sprites_basic.py new file mode 100644 index 0000000..05120c9 --- /dev/null +++ b/arcade_perf/tests/arcade/moving_sprites_basic.py @@ -0,0 +1,24 @@ +import arcade +from .moving_sprites import Test as MovingSpritesTest + + +class Coin(arcade.BasicSprite): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.change_x = 0 + self.change_y = 0 + + def update(self): + """ + Update the sprite. + """ + self.position = ( + self.position[0] + self.change_x, + self.position[1] + self.change_y, + ) + + +class Test(MovingSpritesTest): + name = "moving-sprites-basic" + coin_cls = Coin diff --git a/arcade_perf/tests/arcade/stationary_sprites.py b/arcade_perf/tests/arcade/stationary_sprites.py new file mode 100644 index 0000000..cb510b3 --- /dev/null +++ b/arcade_perf/tests/arcade/stationary_sprites.py @@ -0,0 +1,72 @@ +""" +Moving Sprite Stress Test + +Simple program to test how fast we can draw sprites that are moving + +Artwork from https://kenney.nl +""" +import random +import arcade +from arcade_perf.tests.base import ArcadePerfTest + +# --- Constants --- +SPRITE_SCALING_COIN = 0.25 +SPRITE_NATIVE_SIZE = 128 +SPRITE_SIZE = int(SPRITE_NATIVE_SIZE * SPRITE_SCALING_COIN) +SCREEN_WIDTH = 1800 +SCREEN_HEIGHT = 1000 +SCREEN_TITLE = "Arcade - Stationary Sprite Stress Test" + + +class Coin(arcade.Sprite): + + def on_update(self, delta_time): + """ + Update the sprite. + """ + self.position = ( + self.position[0] + self.change_x, + self.position[1] + self.change_y, + ) + + +class Test(ArcadePerfTest): + name = "stationary-sprites" + + def __init__(self): + super().__init__( + size=(SCREEN_WIDTH, SCREEN_HEIGHT), + title=SCREEN_TITLE, + start_count=0, + increment_count=250, + duration=60, + ) + self.coin_list = None + + def setup(self): + """Set up the game and initialize the variables""" + self.coin_list = arcade.SpriteList() + self.coin_texture = arcade.load_texture(":textures:coinGold.png") + + def add_coins(self, amount): + """add mount coins to the spritelist""" + for _ in range(amount): + coin = Coin( + self.coin_texture, + center_x=random.randrange(SPRITE_SIZE, SCREEN_WIDTH - SPRITE_SIZE), + center_y=random.randrange(SPRITE_SIZE, SCREEN_HEIGHT - SPRITE_SIZE), + scale=SPRITE_SCALING_COIN, + ) + self.coin_list.append(coin) + + def on_draw(self): + self.coin_list.draw() + + def on_update(self, delta_time): + pass + + def update_state(self): + # Figure out if we need more coins + if self.timing.target_n > len(self.coin_list): + new_coin_amount = self.timing.target_n - len(self.coin_list) + self.add_coins(new_coin_amount) diff --git a/arcade_perf/tests/arcade/straight_shapes.py b/arcade_perf/tests/arcade/straight_shapes.py new file mode 100644 index 0000000..168778d --- /dev/null +++ b/arcade_perf/tests/arcade/straight_shapes.py @@ -0,0 +1,229 @@ +import random +import arcade +from arcade_perf.tests.base import ArcadePerfTest +from arcade import shape_list + +# Set up the constants +SCREEN_WIDTH = 1800 +SCREEN_HEIGHT = 1000 +SCREEN_TITLE = "Arcade - Moving Shapes Non-Buffered" + +RECT_WIDTH = 50 +RECT_HEIGHT = 50 + + +class Shape: + """ Generic base shape class """ + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + self.x = x + self.y = y + self.width = width + self.height = height + self.angle = angle + self.delta_x = delta_x + self.delta_y = delta_y + self.delta_angle = delta_angle + self.color = color + + def move(self): + self.x += self.delta_x + self.y += self.delta_y + self.angle += self.delta_angle + if self.x < 0 and self.delta_x < 0: + self.delta_x *= -1 + if self.y < 0 and self.delta_y < 0: + self.delta_y *= -1 + if self.x > SCREEN_WIDTH and self.delta_x > 0: + self.delta_x *= -1 + if self.y > SCREEN_HEIGHT and self.delta_y > 0: + self.delta_y *= -1 + + +class Ellipse(Shape): + + def draw(self): + arcade.draw_ellipse_filled(self.x, self.y, self.width, self.height, + self.color, self.angle) + + +class Rectangle(Shape): + + def draw(self): + arcade.draw_rectangle_filled(self.x, self.y, self.width, self.height, + self.color, self.angle) + + +class Line(Shape): + + def draw(self): + arcade.draw_line(self.x, self.y, + self.x + self.width, self.y + self.height, + self.color, 2) + + +class ShapeBuffered: + """ Generic base shape class """ + + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + self.x = x + self.y = y + self.width = width + self.height = height + self.angle = angle + self.delta_x = delta_x + self.delta_y = delta_y + self.delta_angle = delta_angle + self.color = color + self.shape_list = None + + def move(self): + self.x += self.delta_x + self.y += self.delta_y + if self.delta_angle: + self.angle += self.delta_angle + if self.x < 0 and self.delta_x < 0: + self.delta_x *= -1 + if self.y < 0 and self.delta_y < 0: + self.delta_y *= -1 + if self.x > SCREEN_WIDTH and self.delta_x > 0: + self.delta_x *= -1 + if self.y > SCREEN_HEIGHT and self.delta_y > 0: + self.delta_y *= -1 + + def draw(self): + self.shape_list.center_x = self.x + self.shape_list.center_y = self.y + # self.shape_list.angle = self.angle + self.shape_list.draw() + + +class EllipseBuffered(ShapeBuffered): + + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + + super().__init__(x, y, width, height, angle, delta_x, delta_y, + delta_angle, color) + + shape = shape_list.create_ellipse_filled(0, 0, + self.width, self.height, + self.color, self.angle) + self.shape_list = shape_list.ShapeElementList() + self.shape_list.append(shape) + + +class RectangleBuffered(ShapeBuffered): + + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + + super().__init__(x, y, width, height, angle, delta_x, delta_y, + delta_angle, color) + + shape = shape_list.create_rectangle_filled(0, 0, + self.width, self.height, + self.color, self.angle) + self.shape_list = shape_list.ShapeElementList() + self.shape_list.append(shape) + + +class LineBuffered(ShapeBuffered): + + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + + super().__init__(x, y, width, height, angle, delta_x, delta_y, + delta_angle, color) + + shape = shape_list.create_line(0, 0, + self.width, self.height, + self.color, 2) + self.shape_list = shape_list.ShapeElementList() + self.shape_list.append(shape) + + +class Test(ArcadePerfTest): + name = "straight-shapes" + instances = [ + ({"mode": "buffered"}, "Shapes Unbuffered"), + ({"mode": "unbuffered"}, "Shapes buffered"), + ] + + def __init__(self, mode: str = "buffered"): + super().__init__( + size=(SCREEN_WIDTH, SCREEN_HEIGHT), + title=SCREEN_TITLE, + start_count=0, + increment_count=100, + duration=60, + ) + self.shape_list = None + self.buffered = True if mode == "buffered" else False + self.name = self.get_instance_name(mode=mode) + + def setup(self): + """ Set up the game and initialize the variables. """ + self.shape_list = [] + + def add_shapes(self, amount): + for i in range(amount): + x = random.randrange(0, SCREEN_WIDTH) + y = random.randrange(0, SCREEN_HEIGHT) + width = random.randrange(10, 30) + height = random.randrange(10, 30) + # angle = random.randrange(0, 360) + angle = 0 + + # d_x = random.randrange(-3, 4) + # d_y = random.randrange(-3, 4) + d_x = 0 + d_y = 0 + # d_angle = random.randrange(-3, 4) + d_angle = 0 + + red = random.randrange(256) + green = random.randrange(256) + blue = random.randrange(256) + # alpha = random.randrange(256) + alpha = 255 + shape_type = random.randrange(1) + # shape_type = 1 + + if not self.buffered: + if shape_type == 0: + shape = Rectangle(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + elif shape_type == 1: + shape = Ellipse(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + elif shape_type == 2: + shape = Line(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + else: + if shape_type == 0: + shape = RectangleBuffered(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + elif shape_type == 1: + shape = EllipseBuffered(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + elif shape_type == 2: + shape = LineBuffered(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + + self.shape_list.append(shape) + + def on_update(self, dt): + for shape in self.shape_list: + shape.move() + + def on_draw(self): + for shape in self.shape_list: + shape.draw() + + def update_state(self): + # Figure out if we need more coins + if self.timing.target_n > len(self.shape_list): + new_coin_amount = self.timing.target_n - len(self.shape_list) + self.add_shapes(new_coin_amount) diff --git a/arcade_perf/tests/base.py b/arcade_perf/tests/base.py new file mode 100644 index 0000000..f7e9e39 --- /dev/null +++ b/arcade_perf/tests/base.py @@ -0,0 +1,234 @@ +from pathlib import Path +from typing import Tuple +import pygame +import arcade +from arcade_perf.timing import PerformanceTiming + + +class PerfTest: + """ + Base class for performance tests. + + This class is responsible for setting up the test, running the test, and + saving the results. + + :param size: The size of the window to create. + :param title: The title of the window. + :param start_count: The number of objects to start with. + :param increment_count: The number of objects to add each time. + :param duration: The number of seconds to run the test. + """ + name = "default" + type = "default" + series_name = "default" + instances = [] + + def __init__( + self, + size: Tuple[int, int], + title: str = "Perf Test", + start_count: int = 0, + increment_count: int = 100, + duration: float = 60.0, + **kwargs, + ): + self.size = size + self.title = title + self.start_count = start_count + self.increment_count = increment_count + self.duration = duration + self.frame = 0 + self.timing = None + + @property + def instance_name(self) -> str: + """Get the instance name""" + return f"{self.type}_{self.name}" + + def get_instance_name(self, **kwargs): + """Get information from the instance values""" + for k, v in self.instances: + if k == kwargs: + return v + + raise ValueError(f"Unknown instance value: {kwargs}") + + def on_draw(self): + pass + + def on_update(self, delta_time: float): + self.frame += 1 + + def update_state(self): + pass + + def run(self, session_dir: Path): + self.frame = 0 + out_path = session_dir / "data" + out_path.mkdir(parents=True, exist_ok=True) + + self.timing = PerformanceTiming( + out_path / f"{self.instance_name}.csv", + start_n=self.start_count, + increment_n=self.increment_count, + end_time=self.duration, + ) + + +class ArcadePerfTest(PerfTest): + type = "arcade" + + def __init__( + self, + size: Tuple[int, int], + title: str = "Perf Test", + start_count: int = 0, + increment_count: int = 100, + duration: float = 60.0, + **kwargs + ): + super().__init__( + size=size, + title=title, + start_count=start_count, + increment_count=increment_count, + # duration=duration, + duration=10, + **kwargs + ) + self.window = None + + def on_draw(self): + pass + + def on_update(self, delta_time: float): + return super().on_update(delta_time) + + def update_state(self): + pass + + def run_test(self): + """Run the test without collecting data""" + super().run() + self.create_window() + self.setup() + while not self.timing.end_run(): + self.window.dispatch_events() + self.on_update(1 / 60) + self.on_draw() + self.update_state() + self.window.flip() + + + def run(self, session_dir: Path, screenshot: bool = True): + """Run the test collecting data.""" + super().run(session_dir) + self.create_window() + self.setup() + + # last_time = time.time() + # current_time = time.time() + + while not self.timing.end_run(): + self.window.dispatch_events() + + self.timing.start_timer("update") + self.on_update(1 / 60) + self.timing.stop_timer("update") + + self.window.clear() + + self.timing.start_timer("draw") + self.on_draw() + self.window.ctx.flush() # Wait for draw to finish + self.timing.stop_timer("draw") + + self.update_state() + + self.window.flip() + + self.timing.write() + + # Save screenshot + if screenshot: + path = session_dir / "images" + path.mkdir(parents=True, exist_ok=True) + arcade.get_image().save( + path / f"{self.type}_{self.name}.png" + ) + + def create_window(self): + try: + self.window = arcade.get_window() + self.window.set_size(*self.size) + except RuntimeError: + self.window = arcade.open_window(*self.size, self.title) + # Run a few fames to warm up the window + for _ in range(10): + self.window.clear() + self.window.flip() + + +class PygamePerfTest(PerfTest): + type = "pygame" + + def __init__( + self, + size: Tuple[int, int], + title: str = "Perf Test", + start_count: int = 0, + increment_count: int = 100, + duration: float = 60.0, + **kwargs + ): + super().__init__( + size, title, start_count, increment_count, duration, **kwargs + ) + self.window = None + + def on_draw(self): + super().on_draw() + self.window.fill((0, 0, 0)) + + def on_update(self, delta_time: float): + return super().on_update(delta_time) + + def run(self, session_dir: Path, screenshot: bool = True): + """Run the test.""" + super().run(session_dir) + self.window = None + try: + self.window = pygame.display.get_surface() + self.window = pygame.display.set_mode(self.size) + except pygame.error: + self.window = pygame.display.set_mode(self.size) + + pygame.display.set_caption(self.title) + + self.setup() + + while not self.timing.end_run(): + pygame.event.get() + + self.timing.start_timer("update") + self.on_update(1 / 60) + self.timing.stop_timer("update") + + self.window.fill((59, 122, 87)) + + self.timing.start_timer("draw") + self.on_draw() + self.timing.stop_timer("draw") + + self.update_state() + pygame.display.flip() + + self.timing.write() + + if screenshot: + path = session_dir / "images" + path.mkdir(parents=True, exist_ok=True) + pygame.image.save( + self.window, path / f"{self.type}_{self.name}.png" + ) + \ No newline at end of file diff --git a/arcade_perf/tests/pygame/collision.py b/arcade_perf/tests/pygame/collision.py new file mode 100644 index 0000000..391feb6 --- /dev/null +++ b/arcade_perf/tests/pygame/collision.py @@ -0,0 +1,178 @@ +""" +Sample Python/Pygame Programs +Simpson College Computer Science +http://programarcadegames.com/ +http://simpson.edu/computer-science/ +""" +import arcade +import pygame +import random +from arcade_perf.tests.base import PygamePerfTest + +pygame.init() + +# Define some colors +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +RED = (255, 0, 0) + +# --- Constants --- +SPRITE_SCALING_COIN = 0.09 +SPRITE_SCALING_PLAYER = 0.5 +SPRITE_NATIVE_SIZE = 128 +SPRITE_SIZE = int(SPRITE_NATIVE_SIZE * SPRITE_SCALING_COIN) + +SCREEN_WIDTH = 1800 +SCREEN_HEIGHT = 1000 +SCREEN_TITLE = "Pygame - Moving Sprite Stress Test" + +RESULTS_FILE = "../../result_data/pygame/collision.csv" +RESULTS_IMAGE = "../../result_data/pygame/collision.png" + + +class Coin(pygame.sprite.Sprite): + """ + This class represents the ball + It derives from the "Sprite" class in Pygame + """ + # Static coin image + coin_image = None + + def __init__(self): + """ Constructor. Pass in the color of the block, + and its x and y position. """ + # Call the parent class (Sprite) constructor + super().__init__() + + # In Pygame, if we load and scale a coin image every time we create a sprite, + # this will result in a noticeable performance hit. Therefore we do it once, + # and re-use that image over-and-over. + image_path = arcade.resources.resolve_resource_path(":textures:coinGold.png") + if Coin.coin_image is None: + # Create an image of the block, and fill it with a color. + # This could also be an image loaded from the disk. + Coin.coin_image = pygame.image.load(image_path) + rect = Coin.coin_image.get_rect() + Coin.coin_image = pygame.transform.scale( + Coin.coin_image, + (int(rect.width * SPRITE_SCALING_COIN), int(rect.height * SPRITE_SCALING_COIN))) + Coin.coin_image.convert() + Coin.coin_image.set_colorkey(BLACK) + + self.image = Coin.coin_image + + # Fetch the rectangle object that has the dimensions of the image + # image. + # Update the position of this object by setting the values + # of rect.x and rect.y + self.rect = self.image.get_rect() + + +class Player(pygame.sprite.Sprite): + """ + This class represents the ball + It derives from the "Sprite" class in Pygame + """ + + def __init__(self): + """ Constructor. Pass in the color of the block, + and its x and y position. """ + # Call the parent class (Sprite) constructor + super().__init__() + + # Create an image of the block, and fill it with a color. + # This could also be an image loaded from the disk. + image_path = arcade.resources.resolve_resource_path(":textures:femalePerson_idle.png") + image = pygame.image.load(image_path) + rect = image.get_rect() + image = pygame.transform.scale(image, ( + int(rect.width * SPRITE_SCALING_PLAYER), int(rect.height * SPRITE_SCALING_PLAYER))) + self.image = image.convert() + self.image.set_colorkey(BLACK) + + # Fetch the rectangle object that has the dimensions of the image + # image. + # Update the position of this object by setting the values + # of rect.x and rect.y + self.rect = self.image.get_rect() + + self.change_x = 0 + self.change_y = 0 + + def update(self): + """ Called each frame. """ + self.rect.x += self.change_x + self.rect.y += self.change_y + + +class Test(PygamePerfTest): + name = "collision" + + def __init__(self): + """ Initializer """ + super().__init__( + size=(SCREEN_WIDTH, SCREEN_HEIGHT), + title=SCREEN_TITLE, + ) + + def setup(self): + # This is a list of every sprite. All blocks and the player block as well. + self.coin_list = pygame.sprite.Group() + self.player_list = pygame.sprite.Group() + + # Create the player instance + self.player = Player() + + self.player.rect.x = random.randrange(SPRITE_SIZE, SCREEN_WIDTH - SPRITE_SIZE) + self.player.rect.y = random.randrange(SPRITE_SIZE, SCREEN_HEIGHT - SPRITE_SIZE) + self.player.change_x = 3 + self.player.change_y = 5 + + self.player_list.add(self.player) + + self.font = pygame.font.SysFont('Calibri', 25, True, False) + + def add_coins(self, amount): + + # Create the coins + for i in range(amount): + # Create the coin instance + # Coin image from kenney.nl + coin = Coin() + + # Position the coin + coin.rect.x = random.randrange(SPRITE_SIZE, SCREEN_WIDTH - SPRITE_SIZE) + coin.rect.y = random.randrange(SPRITE_SIZE, SCREEN_HEIGHT - SPRITE_SIZE) + + # Add the coin to the lists + self.coin_list.add(coin) + + def on_draw(self): + """ Draw everything """ + self.coin_list.draw(self.window) + self.player_list.draw(self.window) + + def on_update(self, delta_time): + # Start update timer + self.player_list.update() + + if self.player.rect.x < 0 and self.player.change_x < 0: + self.player.change_x *= -1 + if self.player.rect.y < 0 and self.player.change_y < 0: + self.player.change_y *= -1 + + if self.player.rect.x > SCREEN_WIDTH and self.player.change_x > 0: + self.player.change_x *= -1 + if self.player.rect.y > SCREEN_HEIGHT and self.player.change_y > 0: + self.player.change_y *= -1 + + coin_hit_list = pygame.sprite.spritecollide(self.player, self.coin_list, False) + for coin in coin_hit_list: + coin.rect.x = random.randrange(SCREEN_WIDTH) + coin.rect.y = random.randrange(SCREEN_HEIGHT) + + def update_state(self): + # Figure out if we need more coins + if self.timing.target_n > len(self.coin_list): + new_coin_amount = self.timing.target_n - len(self.coin_list) + self.add_coins(new_coin_amount) diff --git a/arcade_perf/tests/pygame/moving_shapes.py b/arcade_perf/tests/pygame/moving_shapes.py new file mode 100644 index 0000000..193b758 --- /dev/null +++ b/arcade_perf/tests/pygame/moving_shapes.py @@ -0,0 +1,133 @@ +# noinspection PyPackageRequirements +import pygame +import random +from arcade_perf.tests.base import PygamePerfTest + +pygame.init() + +# Define some colors +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +RED = (255, 0, 0) + +# --- Constants --- +SPRITE_SCALING_COIN = 0.25 +SPRITE_NATIVE_SIZE = 128 +SPRITE_SIZE = int(SPRITE_NATIVE_SIZE * SPRITE_SCALING_COIN) + +SCREEN_WIDTH = 1800 +SCREEN_HEIGHT = 1000 +SCREEN_TITLE = "Pygame - Moving Shapes Stress Test" + + +class Shape: + """ Generic base shape class """ + def __init__(self, x, y, width, height, angle, delta_x, delta_y, + delta_angle, color): + self.x = x + self.y = y + self.width = width + self.height = height + self.angle = angle + self.delta_x = delta_x + self.delta_y = delta_y + self.delta_angle = delta_angle + self.color = color + self.orig_image = pygame.Surface((width , height)) + self.orig_image.fill(color) + + def move(self): + self.x += self.delta_x + self.y += self.delta_y + self.angle += self.delta_angle + if self.x < 0 and self.delta_x < 0: + self.delta_x *= -1 + if self.y < 0 and self.delta_y < 0: + self.delta_y *= -1 + if self.x > SCREEN_WIDTH and self.delta_x > 0: + self.delta_x *= -1 + if self.y > SCREEN_HEIGHT and self.delta_y > 0: + self.delta_y *= -1 + + +class Rectangle(Shape): + + def draw(self, surface): + image = self.orig_image.copy() + image.set_colorkey(BLACK) + rotated_image = pygame.transform.rotate(image, self.angle) + surface.blit(rotated_image, (self.x, self.y)) + + +class Ellipse(Shape): + + def draw(self, surface): + rect = pygame.Rect(self.x, self.y, self.width, self.height) + pygame.draw.ellipse(surface, self.color, rect) + + +class Test(PygamePerfTest): + name = "moving-shapes" + + def __init__(self): + super().__init__( + size=(SCREEN_WIDTH, SCREEN_HEIGHT), + title=SCREEN_TITLE, + start_count=0, + increment_count=100, + duration=60, + ) + + def setup(self): + self.shape_list = [] + + def add_shapes(self, amount): + for i in range(amount): + x = random.randrange(0, SCREEN_WIDTH) + y = random.randrange(0, SCREEN_HEIGHT) + width = random.randrange(10, 30) + height = random.randrange(10, 30) + angle = random.randrange(0, 360) + + d_x = random.randrange(-3, 4) + d_y = random.randrange(-3, 4) + d_angle = random.randrange(-3, 4) + + # d_x = 0 + # d_y = 0 + # d_angle = 0 + + red = random.randrange(256) + green = random.randrange(256) + blue = random.randrange(256) + # alpha = random.randrange(256) + alpha = 255 + + shape_type = random.randrange(1) + # shape_type = 0 + + if shape_type == 0: + shape = Rectangle(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + elif shape_type == 1: + shape = Ellipse(x, y, width, height, angle, d_x, d_y, + d_angle, (red, green, blue, alpha)) + # elif shape_type == 2: + # shape = Line(x, y, width, height, angle, d_x, d_y, + # d_angle, (red, green, blue, alpha)) + + self.shape_list.append(shape) + + def on_draw(self): + for shape in self.shape_list: + shape.draw(self.window) + + def on_update(self, _delta_time): + for shape in self.shape_list: + shape.move() + + def update_state(self): + # Figure out if we need more coins + if self.timing.target_n > len(self.shape_list): + amount = self.timing.target_n - len(self.shape_list) + self.add_shapes(amount) diff --git a/arcade_perf/tests/pygame/moving_sprites.py b/arcade_perf/tests/pygame/moving_sprites.py new file mode 100644 index 0000000..cb27303 --- /dev/null +++ b/arcade_perf/tests/pygame/moving_sprites.py @@ -0,0 +1,134 @@ +""" +Moving Sprite Stress Test + +Simple program to test how fast we can draw sprites that are moving + +Artwork from http://kenney.nl +""" +# noinspection PyPackageRequirements +import pygame +import random +from arcade_perf.tests.base import PygamePerfTest +from arcade.resources import resolve_resource_path + +pygame.init() + +# Define some colors +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +RED = (255, 0, 0) + +# --- Constants --- +SPRITE_SCALING_COIN = 0.25 +SPRITE_NATIVE_SIZE = 128 +SPRITE_SIZE = int(SPRITE_NATIVE_SIZE * SPRITE_SCALING_COIN) + +RESULTS_FILE = "../../result_data/pygame/draw_moving_sprites.csv" +RESULTS_IMAGE = "../../result_data/pygame/draw_moving_sprites.png" +SCREEN_WIDTH = 1800 +SCREEN_HEIGHT = 1000 +SCREEN_TITLE = "Pygame - Moving Sprite Stress Test" + + +class Coin(pygame.sprite.Sprite): + """ + This class represents the ball + It derives from the "Sprite" class in Pygame + """ + + # Static coin image + coin_image = None + + def __init__(self): + """ Constructor. Pass in the color of the block, + and its x and y position. """ + # Call the parent class (Sprite) constructor + super().__init__() + + # In Pygame, if we load and scale a coin image every time we create a sprite, + # this will result in a noticeable performance hit. Therefore we do it once, + # and re-use that image over-and-over. + if Coin.coin_image is None: + # Create an image of the block, and fill it with a color. + # This could also be an image loaded from the disk. + path = resolve_resource_path(":textures:coinGold.png") + Coin.coin_image = pygame.image.load(path) + rect = Coin.coin_image.get_rect() + Coin.coin_image = pygame.transform.scale( + Coin.coin_image, + (int(rect.width * SPRITE_SCALING_COIN), int(rect.height * SPRITE_SCALING_COIN))) + Coin.coin_image.convert() + Coin.coin_image.set_colorkey(BLACK) + + self.image = Coin.coin_image + + # Fetch the rectangle object that has the dimensions of the image + # image. + # Update the position of this object by setting the values + # of rect.x and rect.y + self.rect = self.image.get_rect() + + # Instance variables for our current speed and direction + self.change_x = 0 + self.change_y = 0 + + def update(self): + """ Called each frame. """ + self.rect.x += self.change_x + self.rect.y += self.change_y + + +class Test(PygamePerfTest): + name = "moving-sprites" + + def __init__(self): + super().__init__( + size=(SCREEN_WIDTH, SCREEN_HEIGHT), + title=SCREEN_TITLE, + start_count=0, + increment_count=250, + duration=60, + ) + + def setup(self): + """ Set up the game and initialize the variables. """ + self.coin_list = pygame.sprite.Group() + + def add_coins(self, amount): + """Add mount coins to the list""" + for _ in range(amount): + coin = Coin() + + # Position the coin + coin.rect.x = random.randrange(SPRITE_SIZE, SCREEN_WIDTH - SPRITE_SIZE) + coin.rect.y = random.randrange(SPRITE_SIZE, SCREEN_HEIGHT - SPRITE_SIZE) + + coin.change_x = random.randrange(-3, 4) + coin.change_y = random.randrange(-3, 4) + + # Add the coin to the lists + self.coin_list.add(coin) + + def on_draw(self): + # Draw all the spites + self.coin_list.draw(self.window) + + def on_update(self, _delta_time): + self.coin_list.update() + + for sprite in self.coin_list: + + if sprite.rect.x < 0: + sprite.change_x *= -1 + elif sprite.rect.x > SCREEN_WIDTH: + sprite.change_x *= -1 + if sprite.rect.y < 0: + sprite.change_y *= -1 + elif sprite.rect.y > SCREEN_HEIGHT: + sprite.change_y *= -1 + + def update_state(self): + # Figure out if we need more coins + if self.timing.target_n > len(self.coin_list): + new_coin_amount = self.timing.target_n - len(self.coin_list) + self.add_coins(new_coin_amount) diff --git a/arcade_perf/tests/pyglet/moving_shapes.py b/arcade_perf/tests/pyglet/moving_shapes.py new file mode 100644 index 0000000..a1d7216 --- /dev/null +++ b/arcade_perf/tests/pyglet/moving_shapes.py @@ -0,0 +1,106 @@ +import random +import pyglet.shapes +from arcade_perf.tests.arcade.moving_sprites import ArcadePerfTest + +# Set up the constants +SCREEN_WIDTH = 1800 +SCREEN_HEIGHT = 1000 +SCREEN_TITLE = "Pyglet Moving Shapes" + + +class MovingEllipse(pyglet.shapes.Rectangle): + """ Generic base shape class """ + def __init__(self, x, y, a, b, color=(255, 255, 255, 255), batch=None, group=None): + super().__init__(x, y, a, b, color, batch, group) + self.delta_x = 0 + self.delta_y = 0 + self.delta_angle = 0 + # Anchor the rotation to the middle of the rectangle, instead of the corner. + self.anchor_x = a / 2 + self.anchor_y = b / 2 + + def move(self): + # self.x += self.delta_x + # self.y += self.delta_y + # self.rotation += self.delta_angle + # if self.x < 0 and self.delta_x < 0: + # self.delta_x *= -1 + # if self.y < 0 and self.delta_y < 0: + # self.delta_y *= -1 + # if self.x > SCREEN_WIDTH and self.delta_x > 0: + # self.delta_x *= -1 + # if self.y > SCREEN_HEIGHT and self.delta_y > 0: + # self.delta_y *= -1 + + x, y = self.position + x += self.delta_x + y += self.delta_y + self.position = x, y + self.rotation += self.delta_angle + if x < 0 and self.delta_x < 0: + self.delta_x *= -1 + if y < 0 and self.delta_y < 0: + self.delta_y *= -1 + if x > SCREEN_WIDTH and self.delta_x > 0: + self.delta_x *= -1 + if y > SCREEN_HEIGHT and self.delta_y > 0: + self.delta_y *= -1 + + +class Test(ArcadePerfTest): + """ Main application class. """ + type = "pyglet" + name = "moving-shapes" + + def __init__(self): + super().__init__( + size=(SCREEN_WIDTH, SCREEN_HEIGHT), + title=SCREEN_TITLE, + start_count=0, + increment_count=1, + duration=60, + ) + + def setup(self): + """ Set up the game and initialize the variables. """ + self.batch = pyglet.graphics.Batch() + self.shape_list = [] + + def add_shapes(self, amount): + for i in range(amount): + x = random.randrange(0, SCREEN_WIDTH) + y = random.randrange(0, SCREEN_HEIGHT) + width = random.randrange(10, 30) + height = random.randrange(10, 30) + angle = random.randrange(0, 360) + + d_x = random.randrange(-3, 4) + d_y = random.randrange(-3, 4) + d_angle = random.randrange(-3, 4) + + red = random.randrange(256) + green = random.randrange(256) + blue = random.randrange(256) + alpha = random.randrange(256) + + shape = MovingEllipse(x, y, width, height, + color=(red, green, blue, alpha), + batch=self.batch) + shape.rotation = angle + shape.delta_x = d_x + shape.delta_y = d_y + shape.delta_angle = d_angle + self.shape_list.append(shape) + + def on_update(self, dt): + for shape in self.shape_list: + shape.move() + + def on_draw(self): + self.batch.draw() + + def update_state(self): + # Figure out if we need more shapes + if self.timing.target_n > len(self.shape_list): + new_amount = self.timing.target_n - len(self.shape_list) + self.add_shapes(new_amount) diff --git a/src/performance_timing.py b/arcade_perf/timing.py similarity index 85% rename from src/performance_timing.py rename to arcade_perf/timing.py index 64d5038..cc6875a 100644 --- a/src/performance_timing.py +++ b/arcade_perf/timing.py @@ -1,3 +1,4 @@ +from typing import List import timeit import statistics @@ -5,7 +6,7 @@ class PerformanceTiming: def __init__(self, results_file, start_n, increment_n, end_time): self.program_start_time = timeit.default_timer() - self.results_file = open(results_file, "w") + self.result_path = results_file self.last_report = 0 self.start_timers = {} self.timing_lists = {} @@ -14,6 +15,7 @@ def __init__(self, results_file, start_n, increment_n, end_time): self.start_n = start_n self.increment_n = increment_n self.end_time = end_time + self.output: List[str] = [] @property def total_program_time(self): @@ -26,7 +28,6 @@ def target_n(self): def end_run(self): if self.total_program_time > self.end_time: - self.results_file.close() return True else: return False @@ -45,10 +46,9 @@ def report(self): current_time = self.total_program_time if self.first_line: self.first_line = False - output = f"Time, FPS, Sprite Count, Draw Time, Update Time" + output = "Time, FPS, Sprite Count, Draw Time, Update Time" + self.output.append(output) print(output) - self.results_file.write(output) - self.results_file.write("\n") if int(current_time) > int(self.last_report): exact_time = current_time - self.last_report @@ -68,8 +68,11 @@ def report(self): fps = update_count / exact_time output = f"{int(current_time)}, {fps:.1f}, {self.target_n}, {draw_time:.6f}, {update_time:.6f}" print(output) - self.results_file.write(output) - self.results_file.write("\n") + self.output.append(output) # Reset timers self.timing_lists = {} + + def write(self): + with open(self.result_path, 'w') as fd: + fd.write("\n".join(self.output)) diff --git a/output/test/graphs/arcade_collision.png b/output/test/graphs/arcade_collision.png new file mode 100644 index 0000000..36cab95 Binary files /dev/null and b/output/test/graphs/arcade_collision.png differ diff --git a/output/test/images/arcade_collision-0.png b/output/test/images/arcade_collision-0.png new file mode 100644 index 0000000..3adcce3 Binary files /dev/null and b/output/test/images/arcade_collision-0.png differ diff --git a/output/test/images/arcade_collision-1.png b/output/test/images/arcade_collision-1.png new file mode 100644 index 0000000..bb4aa94 Binary files /dev/null and b/output/test/images/arcade_collision-1.png differ diff --git a/output/test/images/arcade_collision-2.png b/output/test/images/arcade_collision-2.png new file mode 100644 index 0000000..3daeeba Binary files /dev/null and b/output/test/images/arcade_collision-2.png differ diff --git a/output/test/images/arcade_collision-3.png b/output/test/images/arcade_collision-3.png new file mode 100644 index 0000000..4450680 Binary files /dev/null and b/output/test/images/arcade_collision-3.png differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8f2d4bf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "arcade_perf" +version = "0.2.0" +description = "Arcade Performance Tests" +readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "pygame-ce==2.1.4", + "arcade==3.0.0.dev18", + "matplotlib==3.7.1", + "sphinx==6.1.3", + "sphinx_rtd_theme==1.2.0", + "seaborn==0.12.2", +] +[project.scripts] +arcade-perf = "arcade_perf.__main__:main" + +[tool.setuptools.packages.find] +include = ["arcade_perf", "arcade_perf.*"] diff --git a/readme.rst b/readme.rst deleted file mode 100644 index 57e53fb..0000000 --- a/readme.rst +++ /dev/null @@ -1,21 +0,0 @@ -Timing Comparison Website -========================= - -Step 1 ------- - -Make sure ``requirements.txt`` is loaded for your environment. - -Step 2 ------- - -Run ``src/__main__.py`` which should: - -* Run all tests -* Generate all graphs -* Generate all documents - -Step 3 ------- - -Look at the resulting documents in ``doc/build`` diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0e6e645..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -pygame-ce -arcade==3.0.0.dev18 -matplotlib -pyglet -sphinx -sphinx_rtd_theme -seaborn diff --git a/result_charts/collision/empty.txt b/result_charts/collision/empty.txt deleted file mode 100644 index e69de29..0000000 diff --git a/result_charts/draw_moving_sprites/empty.txt b/result_charts/draw_moving_sprites/empty.txt deleted file mode 100644 index e69de29..0000000 diff --git a/result_charts/draw_stationary_sprites/empty.txt b/result_charts/draw_stationary_sprites/empty.txt deleted file mode 100644 index e69de29..0000000 diff --git a/result_charts/shapes/empty.txt b/result_charts/shapes/empty.txt deleted file mode 100644 index e69de29..0000000 diff --git a/result_data/arcade/empty.txt b/result_data/arcade/empty.txt deleted file mode 100644 index e69de29..0000000 diff --git a/result_data/pygame/empty.txt b/result_data/pygame/empty.txt deleted file mode 100644 index e69de29..0000000