diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 00000000..8e23e6b0 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,36 @@ +name: CodSpeed + +on: + push: + branches: + - "main" + pull_request: + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pytest-codspeed + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run benchmarks + uses: CodSpeedHQ/action@v4.5.1 + with: + mode: simulation + run: pytest benchmarks/ --codspeed diff --git a/.gitignore b/.gitignore index 68cca6d0..2670d53e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ *.log *.claude *CLAUDE.md -*.log \ No newline at end of file +*.log +.codspeed/ +.pytest_cache/ diff --git a/README.md b/README.md index 4802038d..f2863ffe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # DLSS Updater [![CodeQL](https://github.com/Recol/DLSS-Updater/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/Recol/DLSS-Updater/actions?query=workflow%3ACodeQL) +[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/Recol/DLSS-Updater?utm_source=badge) ![Version](./version.svg) ![Downloads](https://img.shields.io/badge/Downloads-57414-blue) [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-donate-yellow.svg)](https://buymeacoffee.com/decouk) diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmarks/test_scanner_performance.py b/benchmarks/test_scanner_performance.py new file mode 100644 index 00000000..bbda41aa --- /dev/null +++ b/benchmarks/test_scanner_performance.py @@ -0,0 +1,150 @@ +""" +Performance benchmarks for the scanner module. +Tests directory traversal and DLL detection performance. +""" +import pytest +from pathlib import Path +import tempfile +import os + + +@pytest.fixture +def temp_game_directory(): + """Create a temporary directory structure simulating a game directory""" + with tempfile.TemporaryDirectory() as tmpdir: + base = Path(tmpdir) + + # Create a typical game directory structure + game_dirs = [ + base / "Game1" / "bin", + base / "Game1" / "Engine" / "Binaries" / "Win64", + base / "Game2" / "x64", + base / "Game3" / "Game" / "Binaries", + ] + + for game_dir in game_dirs: + game_dir.mkdir(parents=True, exist_ok=True) + + # Create some dummy DLL files + (game_dir / "nvngx_dlss.dll").touch() + (game_dir / "nvngx_dlssg.dll").touch() + (game_dir / "libxess.dll").touch() + (game_dir / "ffx_fsr2_api_x64.dll").touch() + + # Create some other files to make scanning more realistic + (game_dir / "game.exe").touch() + (game_dir / "config.ini").touch() + (game_dir / "readme.txt").touch() + + yield base + + +def test_path_traversal_performance(benchmark, temp_game_directory): + """Benchmark directory traversal performance""" + def traverse_directory(): + dll_count = 0 + for root, dirs, files in os.walk(temp_game_directory): + # Skip common directories that should be excluded + dirs[:] = [d for d in dirs if d not in {'__pycache__', '.git', 'cache'}] + + for file in files: + if file.lower().endswith('.dll'): + dll_count += 1 + return dll_count + + result = benchmark(traverse_directory) + assert result > 0 # Should find at least some DLLs + + +def test_dll_name_filtering(benchmark): + """Benchmark DLL name filtering performance""" + dll_names = { + 'nvngx_dlss.dll', 'nvngx_dlssg.dll', 'nvngx_dlssd.dll', + 'libxess.dll', 'ffx_fsr2_api_x64.dll', 'ffx_fsr2_api_dx12_x64.dll', + 'amd_fidelityfx_dx12.dll', 'amd_fidelityfx_vk.dll' + } + + # Simulate checking many file names + test_files = [ + 'nvngx_dlss.dll', 'game.exe', 'config.dll', 'nvngx_dlssg.dll', + 'random.dll', 'libxess.dll', 'texture.dat', 'ffx_fsr2_api_x64.dll' + ] * 100 # Multiply to make the benchmark more meaningful + + def filter_dll_names(): + matched = [] + dll_names_lower = frozenset(d.lower() for d in dll_names) + for filename in test_files: + if filename.lower() in dll_names_lower: + matched.append(filename) + return len(matched) + + result = benchmark(filter_dll_names) + assert result > 0 + + +def test_path_manipulation(benchmark): + """Benchmark path manipulation operations""" + test_paths = [ + "/path/to/game/bin/nvngx_dlss.dll", + "C:\\Games\\Steam\\steamapps\\common\\GameName\\Engine\\Binaries\\Win64\\nvngx_dlss.dll", + "/mnt/games/GOG/GameTitle/x64/libxess.dll", + ] * 100 + + def extract_game_names(): + game_names = [] + for path_str in test_paths: + path = Path(path_str) + # Simple game name extraction logic + parts = path.parts + for i, part in enumerate(parts): + if part.lower() in {'common', 'steamapps', 'games', 'gog'}: + if i + 1 < len(parts): + game_names.append(parts[i + 1]) + break + return len(game_names) + + result = benchmark(extract_game_names) + assert result > 0 + + +def test_file_extension_check(benchmark): + """Benchmark file extension checking performance""" + test_files = [ + "nvngx_dlss.dll", "game.exe", "config.ini", "texture.dds", + "shader.hlsl", "nvngx_dlssg.dll", "readme.txt", "libxess.dll" + ] * 200 + + def check_dll_extensions(): + dll_files = [] + for filename in test_files: + if filename.lower().endswith('.dll'): + dll_files.append(filename) + return len(dll_files) + + result = benchmark(check_dll_extensions) + assert result > 0 + + +def test_set_membership_check(benchmark): + """Benchmark set membership checking for skip directories""" + skip_dirs = frozenset({ + '__pycache__', '.git', '.svn', '.hg', 'node_modules', + 'logs', 'log', 'saves', 'save', 'screenshots', 'crash', + 'crashdumps', 'dumps', 'temp', 'tmp', 'cache', '.cache', + 'shader_cache', 'shadercache', 'gpucache', 'webcache' + }) + + test_dirs = [ + 'bin', 'cache', 'Engine', '__pycache__', 'Binaries', + 'logs', 'x64', 'temp', 'Win64', 'shader_cache' + ] * 150 + + def check_skip_dirs(): + to_scan = [] + for dirname in test_dirs: + if dirname.lower() not in skip_dirs: + to_scan.append(dirname) + return len(to_scan) + + result = benchmark(check_skip_dirs) + assert result >= 0 diff --git a/benchmarks/test_utils_performance.py b/benchmarks/test_utils_performance.py new file mode 100644 index 00000000..fdbdb95b --- /dev/null +++ b/benchmarks/test_utils_performance.py @@ -0,0 +1,164 @@ +""" +Performance benchmarks for utility functions. +Tests string operations, path handling, and data processing. +""" +import pytest +from pathlib import Path + + +def test_string_comparison_performance(benchmark): + """Benchmark case-insensitive string comparisons""" + game_names = ["warframe", "cyberpunk2077", "3dmark", "portal2", "halflife2"] * 50 + blacklist = {"warframe", "3dmark", "benchmark"} + + def check_blacklist(): + matches = [] + for name in game_names: + if any(bl in name.lower() for bl in blacklist): + matches.append(name) + return len(matches) + + result = benchmark(check_blacklist) + assert result > 0 + + +def test_version_string_parsing(benchmark): + """Benchmark version string parsing""" + version_strings = [ + "3.10.4.0", "2.5.1.0", "4.0.2.0", "1.2.3.4", + "10.20.30.40", "0.1.2.3", "99.99.99.99" + ] * 100 + + def parse_versions(): + parsed = [] + for version in version_strings: + parts = version.split('.') + # Convert to tuple of integers for comparison + version_tuple = tuple(int(p) for p in parts) + parsed.append(version_tuple) + return len(parsed) + + result = benchmark(parse_versions) + assert result == len(version_strings) + + +def test_path_normalization(benchmark): + """Benchmark path normalization""" + paths = [ + "C:\\Games\\Steam\\steamapps\\common\\Game\\bin\\nvngx_dlss.dll", + "/home/user/games/game1/lib/libxess.dll", + "D:\\Program Files\\Epic Games\\GameName\\Engine\\Binaries\\Win64\\nvngx_dlss.dll", +"/mnt/storage/games/GOG/Game2/x64/ffx_fsr2_api_x64.dll" + ] * 100 + + def normalize_paths(): + normalized = [] + for path_str in paths: + # Normalize path separators and case + path = Path(path_str) + normalized.append(str(path.as_posix())) + return len(normalized) + + result = benchmark(normalize_paths) + assert result == len(paths) + + +def test_file_size_calculation(benchmark): + """Benchmark file size calculations""" + file_sizes = [ + 1024 * 500, # 500 KB + 1024 * 1024 * 2, # 2 MB + 1024 * 1024 * 50, # 50 MB + 1024 * 100, # 100 KB + ] * 200 + + def format_file_sizes(): + formatted = [] + for size in file_sizes: + # Convert to human-readable format + if size < 1024: + formatted.append(f"{size} B") + elif size < 1024 * 1024: + formatted.append(f"{size / 1024:.2f} KB") + else: + formatted.append(f"{size / (1024 * 1024):.2f} MB") + return len(formatted) + + result = benchmark(format_file_sizes) + assert result == len(file_sizes) + + +def test_dll_type_mapping(benchmark): + """Benchmark DLL type identification""" + dll_names = [ + "nvngx_dlss.dll", "nvngx_dlssg.dll", "nvngx_dlssd.dll", + "libxess.dll", "ffx_fsr2_api_x64.dll", "ffx_fsr2_api_dx12_x64.dll", + "amd_fidelityfx_dx12.dll", "unknown.dll" + ] * 150 + + dll_type_map = { + 'nvngx_dlss.dll': 'DLSS', + 'nvngx_dlssg.dll': 'DLSS Frame Generation', + 'nvngx_dlssd.dll': 'DLSS Ray Reconstruction', + 'libxess.dll': 'XeSS', + 'ffx_fsr2_api_x64.dll': 'FSR 2', + 'ffx_fsr2_api_dx12_x64.dll': 'FSR 2', + 'amd_fidelityfx_dx12.dll': 'FidelityFX', + } + + def identify_dll_types(): + types = [] + for dll_name in dll_names: + dll_type= dll_type_map.get(dll_name.lower(), 'Unknown') + types.append(dll_type) + return len(types) + + result = benchmark(identify_dll_types) + assert result == len(dll_names) + + +def test_list_filtering_performance(benchmark): + """Benchmark filtering large lists""" + # Simulate filtering a large list of detected files + all_files = [] + for i in range(1000): + all_files.extend([ + f"game{i}.exe", + f"nvngx_dlss_{i}.dll", + f"config{i}.ini", + f"libxess_{i}.dll", + f"texture{i}.dds" + ]) + + def filter_dll_files(): + dll_files = [f for f in all_files if f.endswith('.dll')] + return len(dll_files) + + result = benchmark(filter_dll_files) + assert result > 0 + + +def test_dictionary_lookup_performance(benchmark): + """Benchmark dictionary lookups for launcher paths""" + launcher_paths = { + 'steam': 'C:\\Program Files\\Steam', + 'epic': 'C:\\Program Files\\Epic Games', + 'gog': 'C:\\GOG Games', + 'ubisoft': 'C:\\Program Files\\Ubisoft', + 'ea': 'C:\\Program Files\\EA Games', + 'xbox': 'C:\\Program Files\\Xbox Games', + 'battlenet': 'C:\\Program Files\\Battle.net' + } + + queries = ['steam', 'epic', 'gog', 'unknown', 'ubisoft', 'ea'] * 200 + + def lookup_paths(): + found = [] + for launcher in queries: + path = launcher_paths.get(launcher) + if path: + found.append(path) + return len(found) + + result = benchmark(lookup_paths) + assert result > 0 diff --git a/requirements.txt b/requirements.txt index 8d076dcf..35ea85b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ aiosqlite>=0.22.0 aiofiles>=25.1.0 scandir-rs>=2.9.3; platform_system == "Windows" winloop>=0.1.7; platform_system == "Windows" +pytest-codspeed>=4.2.0