Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BattleEngine in Rust for better memory efficiency with large battles #517

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bd2f010
Add Rust support to Dockerfile
lanedirt Jan 5, 2025
9ec32f9
Update Dockerfile to make Rust available for www user
lanedirt Jan 8, 2025
362e8e8
Add rust test module
lanedirt Jan 8, 2025
26fa66f
Make compile.sh executable during entrypoint
lanedirt Jan 8, 2025
a081050
Fix entrypoint cargo command
lanedirt Jan 8, 2025
e6f7067
Move unit test to feature test as it requires a db connection
lanedirt Jan 8, 2025
958704f
Update PlanetServiceTest.php
lanedirt Jan 8, 2025
43b1a76
Update run-tests-sqlite.yml
lanedirt Jan 8, 2025
d2418e9
Add basic Rust Battle Engine implementation
lanedirt Jan 9, 2025
835b61e
Add WIP rust battle engine basic test
lanedirt Jan 9, 2025
373dc6d
Add rust workspace and rust debug binary
lanedirt Jan 10, 2025
c82ed19
Refactor Rust battle engine input params
lanedirt Jan 13, 2025
1e2c178
Add cleanup round scaffolding to rust battle engine
lanedirt Jan 14, 2025
3bc0113
Update rust battle engine with basic test passing
lanedirt Jan 16, 2025
153c2bd
Refactor battle engines to abstract class to contain generic logic
lanedirt Jan 16, 2025
07ff341
Rename abstract test file so it doesn't get detected as test
lanedirt Jan 16, 2025
2c2180c
Convert all output params of Rust battle engine
lanedirt Jan 16, 2025
872fcc7
Improve rust battle engine fixing more tests
lanedirt Jan 16, 2025
daba1c7
Implement rapidfire to Rust battle engine
lanedirt Jan 16, 2025
0d9e626
Add battle engine performance test artisan command
lanedirt Jan 17, 2025
2c6e769
Update battle engine test to add comparison table output
lanedirt Jan 17, 2025
f7dea5e
Refactor battle engine performance test and add Rust memory tracking
lanedirt Jan 17, 2025
722603f
Improve battle engine performance test to show true peak memory
lanedirt Jan 17, 2025
71d98a7
Fix user delete issue with battle reports
lanedirt Jan 17, 2025
06cc583
Update rust to return memory used in kb
lanedirt Jan 17, 2025
7cdb73b
Refactor
lanedirt Jan 17, 2025
8203c4a
Make migration compatible with sqlite
lanedirt Jan 17, 2025
f17b2db
Cleanup battle engine performance test
lanedirt Jan 17, 2025
f12d895
Optimize Rust battle engine field types to reduce memory usage
lanedirt Jan 17, 2025
c9aede5
Refactor Rust battle engine
lanedirt Jan 17, 2025
74fd1dd
Refactor Rust battle engine for readability
lanedirt Jan 18, 2025
e79dbe4
Refactor rust battle engine input param to hashmap to prevent requiri…
lanedirt Jan 18, 2025
8147a08
Improve Rust documentation
lanedirt Jan 18, 2025
ab23a16
Update comments
lanedirt Jan 18, 2025
b9f65f5
Add server setting option to choose battle engine rust/php
lanedirt Jan 18, 2025
241243e
Compile rust libs on container startup
lanedirt Jan 18, 2025
2089437
Fix issues with GitHub Actions runners using Rust libs
lanedirt Jan 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/run-tests-docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@ jobs:
run: |
sed -i '/USER www/s/^/#/' ./Dockerfile
- name: Set Permissions
run: sudo chmod -R 777 /var/www
run: |
sudo chmod -R 777 /var/www
- name: Copy .env
run: sudo cp .env.example .env
- name: Set up Docker Compose
run: |
# Build the images and start the services
docker compose -f docker-compose.yml up -d
- name: Wait 10 seconds
run: sleep 10
- name: Wait for application to be ready up to 60 seconds
run: |
# Wait for the application to be ready (max 60 seconds)
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/run-tests-sqlite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ jobs:
with:
php-version: '8.3'
- uses: actions/checkout@v3

# Install Rust and Cargo
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true

# Compile Rust modules
- name: Compile Rust modules
run: |
chmod +x ./rust/compile.sh
./rust/compile.sh

# Laravel setup steps
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Composer update
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ yarn-error.log
.vscode
.sql
.codebuddy

# Rust compilation dirs
rust/*/target
# Compiled rust apps (rust modules are recompiled during container start)
storage/rust-libs/*.so
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ RUN apt-get update && apt-get install -y \
git \
curl

# Install PHP FFI development files required to interface with Rust for BattleEngine
RUN apt-get update && apt-get install -y \
pkg-config \
libffi-dev
RUN docker-php-ext-install ffi

# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

Expand Down Expand Up @@ -71,5 +77,10 @@ RUN chmod +x /usr/local/bin/entrypoint && \
# Switch to www user
USER www

# Setup Rust/Cargo for www user
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y && \
. "$HOME/.cargo/env" && \
echo 'source $HOME/.cargo/env' >> ~/.bashrc

# Run entrypoint
CMD ["/usr/local/bin/entrypoint"]
210 changes: 210 additions & 0 deletions app/Console/Commands/Tests/TestBattleEnginePerformance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php

namespace OGame\Console\Commands\Tests;

use Illuminate\Support\Carbon;
use OGame\GameMissions\BattleEngine\Models\BattleResult;
use OGame\GameMissions\BattleEngine\BattleEngine;
use OGame\GameMissions\BattleEngine\PhpBattleEngine;
use OGame\GameMissions\BattleEngine\RustBattleEngine;
use OGame\GameObjects\Models\Units\UnitCollection;
use OGame\Services\ObjectService;
use OGame\Models\Resources;
use OGame\Services\SettingsService;
use InvalidArgumentException;
use Exception;

/**
* This command is used to test the performance of a specific battle engine with specified fleets.
*
* Use like this to test PHP
* ---
* php artisan test:battle-engine-performance php --fleet='{"attacker": {"light_fighter": 1667}, "defender": {"rocket_launcher": 1667}}'
* ---
*
* Use like this to test Rust
* ---
* php artisan test:battle-engine-performance rust --fleet='{"attacker": {"light_fighter": 1667}, "defender": {"rocket_launcher": 1667}}'
* ---
*/
class TestBattleEnginePerformance extends TestCommand
{
protected $signature = 'test:battle-engine-performance
{engine : The battle engine to test (php/rust)}
{--fleet= : JSON string defining attacker and defender fleets}';
protected $description = 'Test battle engine performance with specified fleets';

protected string $email = '[email protected]';
private float $startTime;

/**
* Main entry point for the command.
*/
public function handle(): int
{
// Check for fleet option
if (!$this->option('fleet') || !$this->parseFleets($this->option('fleet'))) {
$this->error('Specify valid --fleet option in JSON format like this: --fleet=\'{"attacker": {"light_fighter": 1667}, "defender": {"rocket_launcher": 1667}}\'');
return 1;
}

// Set up the test environment
parent::setup();

$fleets = $this->parseFleets($this->option('fleet'));
$engine = $this->argument('engine');

if (!in_array($engine, ['php', 'rust'])) {
$this->error('Invalid engine specified. Use "php" or "rust"');
return 1;
}

return $this->runSingleEngineTest($engine, $fleets);
}

/**
* Run a single engine test with specified fleets.
*
* @param string $engine The engine to test.
* @param array<string, UnitCollection> $fleets The fleets to test.
* @return int The exit code.
*/
private function runSingleEngineTest(string $engine, array $fleets): int
{
// Set static time
Carbon::setTestNow(Carbon::create(2024, 1, 1, 0, 0, 0));

// Add resources and tech levels
$this->currentPlanetService->addResources(new Resources(1000000, 1000000, 1000000, 0));
$this->playerService->setResearchLevel('weapon_technology', 10);
$this->playerService->setResearchLevel('shielding_technology', 10);
$this->playerService->setResearchLevel('armor_technology', 10);

// Set up defender planet with provided units
foreach ($fleets['defender']->units as $unit) {
$this->currentPlanetService->addUnit($unit->unitObject->machine_name, $unit->amount);
}

// Force garbage collection before starting measurements
gc_collect_cycles();

// Start tracking metrics
$this->startTime = microtime(true);

// Create attacker fleet
$attackerFleet = $fleets['attacker'];
$this->info("\nAttacker (" . number_format($attackerFleet->getAmount()) . ") and defender (" . number_format($this->currentPlanetService->getShipUnits()->getAmount()) . " + " . number_format($this->currentPlanetService->getDefenseUnits()->getAmount()) . ") fleet created");

// Run battle simulation
$battleEngine = $this->createBattleEngine($engine, $attackerFleet);
$this->info("\nBattle engine starting simulation...");

$battleResult = $battleEngine->simulateBattle();
$this->info("--> Battle engine finished simulation...");

// Calculate and display metrics
$this->displayMetrics($engine, $battleResult);

return 0;
}

/**
* Create a battle engine instance.
*
* @param string $engine The engine to test.
* @param UnitCollection $attackerFleet The attacker fleet.
* @return BattleEngine The battle engine instance.
*/
private function createBattleEngine(string $engine, UnitCollection $attackerFleet): BattleEngine
{
// Resolve settings service.
$settingsService = resolve(SettingsService::class);

return $engine === 'php'
? new PhpBattleEngine($attackerFleet, $this->playerService, $this->currentPlanetService, $settingsService)
: new RustBattleEngine($attackerFleet, $this->playerService, $this->currentPlanetService, $settingsService);
}

/**
* Display the battle metrics.
*
* @param string $engine The engine used for the battle.
* @param BattleResult $battleResult The battle result.
*/
private function displayMetrics(string $engine, BattleResult $battleResult): void
{
// Force garbage collection before final measurements
gc_collect_cycles();

$endTime = microtime(true);
$peakMemoryDuringExecution = memory_get_peak_usage(true);

$executionTime = ($endTime - $this->startTime) * 1000; // Convert to milliseconds
$peakMemoryUsage = $peakMemoryDuringExecution / 1024 / 1024; // Convert bytes to MB

$this->info("\n========================================================");
$this->info("Battle Statistics:");
$this->info("========================================================");
$this->info("Attacker initial fleet size: " . number_format($battleResult->attackerUnitsStart->getAmount()));
$this->info("Defender initial fleet size: " . number_format($battleResult->defenderUnitsStart->getAmount()));
$this->info("Number of rounds: " . number_format(count($battleResult->rounds)));
$this->info("Attacker final fleet size: " . number_format($battleResult->attackerUnitsResult->getAmount()));
$this->info("Defender final fleet size: " . number_format($battleResult->defenderUnitsResult->getAmount()));

$this->info("\n========================================================");
$this->info("Battle Engine Performance Metrics:");
$this->info("========================================================");
$this->info("Execution time: " . number_format($executionTime, 2) . "ms");

$this->info(string: "Peak PHP memory usage: " . number_format($peakMemoryUsage, 2) . "MB");

if ($engine === 'rust') {
$this->info("Note: Rust memory usage can't be measured reliably from PHP. Debug Rust app manually to get indication of Rust memory usage.");
}

$this->info("\n");
}

/**
* Parse the fleet JSON string into an array of fleets.
*
* @param string $fleetJson The fleet JSON string.
* @return array<string, UnitCollection>|null The fleets.
*/
private function parseFleets(string $fleetJson): ?array
{
try {
$fleets = json_decode($fleetJson, true, 512, JSON_THROW_ON_ERROR);

if (!isset($fleets['attacker']) || !isset($fleets['defender'])) {
throw new InvalidArgumentException('Fleet JSON must contain both "attacker" and "defender" arrays');
}

return [
'attacker' => $this->createUnitCollection($fleets['attacker']),
'defender' => $this->createUnitCollection($fleets['defender'])
];
} catch (Exception $e) {
$this->error('Invalid fleet JSON: ' . $e->getMessage());
return null;
}
}

/**
* Create a unit collection from an array of units.
*
* @param array<string, int> $units The units to create the fleet from.
* @return UnitCollection The created fleet.
*/
private function createUnitCollection(array $units): UnitCollection
{
$fleet = new UnitCollection();

foreach ($units as $unitType => $amount) {
$unit = ObjectService::getUnitObjectByMachineName($unitType);
$fleet->addUnit($unit, $amount);
}

return $fleet;
}
}
8 changes: 1 addition & 7 deletions app/Console/Commands/Tests/TestCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,7 @@ protected function setup(): void
// Delete user if it already exists.
$user = User::where('email', '=', $this->email)->first();
if ($user) {
try {
$playerServiceFactory->make($user->id)->delete();
} catch (Exception $e) {
$this->error("Failed to delete existing user: " . $e->getMessage());
// Try to delete the user directly from the database.
$user->delete();
}
$playerServiceFactory->make($user->id)->delete();
}

// Create a test user
Expand Down
Loading
Loading