diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 000000000..2543d135d --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,38 @@ +name: CodSpeed + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +permissions: + contents: read + id-token: write # for OpenID Connect authentication with CodSpeed + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r test/benchmarks/requirements.txt + + - name: Run benchmarks + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: pytest test/benchmarks/ --codspeed diff --git a/README.md b/README.md index 44e1bce50..f88ba99c6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ ![Latest Tag](https://img.shields.io/github/v/tag/neptunehub/AudioMuse-AI?label=latest-tag) ![Media Server Support: Navidrome 0.62.0, Jellyfin 12.0, LMS v3.69.0, Lyrion 9.0.2, Emby 4.9.1.80, Plex 1.43.2](https://img.shields.io/badge/Media%20Server-Navidrome%200.62.0%2C%20Jellyfin%2012.0%2C%20LMS%20v3.69.0%2C%20Lyrion%209.0.2%2C%20Emby%204.9.1.80%2C%20Plex%201.43.2-blue?style=flat-square&logo=server&logoColor=white) +[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://app.codspeed.io/NeptuneHub/AudioMuse-AI?utm_source=badge)

⭐ Leave a star on this project: One shines alone; together, they make it visible and keep it alive. diff --git a/test/benchmarks/__init__.py b/test/benchmarks/__init__.py new file mode 100644 index 000000000..23476669a --- /dev/null +++ b/test/benchmarks/__init__.py @@ -0,0 +1,9 @@ +# AudioMuse-AI - https://github.com/NeptuneHub/AudioMuse-AI +# Copyright (C) 2025 NeptuneHub +# SPDX-License-Identifier: AGPL-3.0-only +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License v3.0. See the LICENSE file +# in the project root or + +"""Package marker for the AudioMuse-AI CodSpeed performance benchmarks.""" diff --git a/test/benchmarks/requirements.txt b/test/benchmarks/requirements.txt new file mode 100644 index 000000000..5da6f9590 --- /dev/null +++ b/test/benchmarks/requirements.txt @@ -0,0 +1,8 @@ +# Dependencies for running the CodSpeed performance benchmarks in test/benchmarks/. +# These benchmarks target self-contained, dependency-light hot paths, so they +# only need numpy (for the sanitization helpers) plus the CodSpeed pytest plugin. +# numpy is pinned to match test/requirements.txt and requirements/common.txt. + +numpy==1.26.4 +pytest>=7.0.0 +pytest-codspeed diff --git a/test/benchmarks/test_bench_playlist_ordering.py b/test/benchmarks/test_bench_playlist_ordering.py new file mode 100644 index 000000000..d3449d02a --- /dev/null +++ b/test/benchmarks/test_bench_playlist_ordering.py @@ -0,0 +1,75 @@ +# AudioMuse-AI - https://github.com/NeptuneHub/AudioMuse-AI +# Copyright (C) 2025 NeptuneHub +# SPDX-License-Identifier: AGPL-3.0-only +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License v3.0. See the LICENSE file +# in the project root or + +"""CodSpeed performance benchmarks for the playlist sonic-ordering distances. + +Measures the pure distance helpers that drive the greedy nearest-neighbour walk +used when sequencing playlists. These run once per candidate pair, so their cost +scales quadratically with playlist size and is worth tracking. + +Main Features: +* Benchmarks the circle-of-fifths key distance across many key/scale pairs. +* Benchmarks the composite tempo/energy/key distance over a realistic song set. +""" + +import pytest + +from test.unit.conftest import _import_module + + +playlist_ordering = _import_module( + 'tasks.playlist_ordering', 'tasks/playlist_ordering.py' +) + +KEYS = ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'Db', 'Ab', 'Eb', 'Bb', 'F', None, 'XYZ'] +SCALES = ['major', 'minor', None] + +KEY_PAIRS = [ + (k1, s1, k2, s2) + for k1 in KEYS + for s1 in SCALES + for k2 in KEYS + for s2 in SCALES +] + +SONGS = [ + { + 'tempo': 60 + (i * 7) % 140, + 'energy': (i % 20) / 20.0, + 'key': KEYS[i % len(KEYS)], + 'scale': SCALES[i % len(SCALES)], + } + for i in range(200) +] + + +def _run_key_distances(): + total = 0.0 + for k1, s1, k2, s2 in KEY_PAIRS: + total += playlist_ordering._key_distance(k1, s1, k2, s2) + return total + + +def _run_composite_distances(): + total = 0.0 + for a in SONGS: + for b in SONGS: + total += playlist_ordering._composite_distance(a, b) + return total + + +@pytest.mark.benchmark +def test_bench_key_distance(benchmark): + result = benchmark(_run_key_distances) + assert result >= 0.0 + + +@pytest.mark.benchmark +def test_bench_composite_distance(benchmark): + result = benchmark(_run_composite_distances) + assert result >= 0.0 diff --git a/test/benchmarks/test_bench_sanitization.py b/test/benchmarks/test_bench_sanitization.py new file mode 100644 index 000000000..0b1e475da --- /dev/null +++ b/test/benchmarks/test_bench_sanitization.py @@ -0,0 +1,75 @@ +# AudioMuse-AI - https://github.com/NeptuneHub/AudioMuse-AI +# Copyright (C) 2025 NeptuneHub +# SPDX-License-Identifier: AGPL-3.0-only +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License v3.0. See the LICENSE file +# in the project root or + +"""CodSpeed performance benchmarks for the sanitization helpers. + +Measures the string and JSON sanitization paths that run on every database +write and API response, using realistic payload sizes so the benchmarks track +the cost of the regex stripping and numpy conversion hot paths. + +Main Features: +* Benchmarks NUL/control-character stripping on plain strings. +* Benchmarks nested-JSON sanitization and numpy-to-native conversion. +""" + +import numpy as np +import pytest + +import sanitization + + +DIRTY_STRING = ( + "Song\x00Title \x01with \x02control\x1f chars and a long tail " + "of unicode text \u00e9\u00e8\u00ea " * 40 +) + +NESTED_JSON = { + "tracks": [ + { + "item_id": f"track-{i}", + "title": f"Title\x00 {i}", + "author": f"Artist\x1f {i}", + "moods": {"happy": 0.8, "energetic": 0.6, "calm": 0.2}, + "tags": [f"tag\x00{j}" for j in range(5)], + } + for i in range(50) + ], + "meta": {"note": "clean\x00note", "count": 50}, +} + +NUMPY_PAYLOAD = { + "embedding": np.random.rand(200), + "scores": [np.float64(v) for v in np.random.rand(50)], + "counts": {"a": np.int64(3), "b": np.int32(7)}, + "flag": np.bool_(True), + "matrix": np.random.rand(20, 20), +} + + +@pytest.mark.benchmark +def test_bench_sanitize_string_for_db(benchmark): + result = benchmark(sanitization.sanitize_string_for_db, DIRTY_STRING) + assert "\x00" not in result + + +@pytest.mark.benchmark +def test_bench_sanitize_db_field(benchmark): + result = benchmark(sanitization.sanitize_db_field, DIRTY_STRING, 1000, "title") + assert "\x00" not in result + + +@pytest.mark.benchmark +def test_bench_sanitize_json_for_db(benchmark): + result = benchmark(sanitization.sanitize_json_for_db, NESTED_JSON) + assert result["meta"]["count"] == 50 + + +@pytest.mark.benchmark +def test_bench_sanitize_for_json(benchmark): + result = benchmark(sanitization.sanitize_for_json, NUMPY_PAYLOAD) + assert isinstance(result["embedding"], list)