Skip to content

Commit

Permalink
Merge pull request #2671 from RaiderIO/development
Browse files Browse the repository at this point in the history
Release v11.9.2 - Further optimizations to API calls
  • Loading branch information
Wotuu authored Jan 22, 2025
2 parents 95a7edc + ba3b339 commit 6ef21d1
Show file tree
Hide file tree
Showing 14 changed files with 154 additions and 40 deletions.
2 changes: 1 addition & 1 deletion .env.docker.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ APP_NAME="Keystone.guru"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_LOG_DISCORD_WEBHOOK=
APP_URL=http://localhost:8008
APP_TYPE=local
LOG_CHANNEL=daily
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=db
Expand Down
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ APP_NAME="Keystone.guru"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_LOG_DISCORD_WEBHOOK=
APP_URL=http://localhost:8008
APP_TYPE=local
LOG_CHANNEL=daily
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
Expand Down
22 changes: 22 additions & 0 deletions app/Http/Controllers/SiteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace App\Http\Controllers;

use App\Http\Models\Request\CombatLog\Route\CombatLogRouteRequestModel;
use App\Logic\Utils\Stopwatch;
use App\Models\DungeonRoute\DungeonRoute;
use App\Models\GameServerRegion;
use App\Models\Release;
use App\Models\Season;
use App\Models\User;
use App\Service\CombatLog\CombatLogRouteDungeonRouteServiceInterface;
use App\Service\DungeonRoute\CoverageServiceInterface;
use App\Service\DungeonRoute\DiscoverServiceInterface;
use App\Service\Expansion\ExpansionService;
Expand Down Expand Up @@ -59,6 +62,25 @@ public function index(CoverageServiceInterface $coverageService, SeasonService $
}
}

/**
* @return RedirectResponse|Redirector
*/
public function benchmark(
Request $request,
CombatLogRouteDungeonRouteServiceInterface $combatLogRouteDungeonRouteService
): View {
$validated = json_decode(file_get_contents(app()->basePath('tmp/combatlog.json')), true);

Stopwatch::start('SiteController::benchmark');
$result = $combatLogRouteDungeonRouteService->correctCombatLogRoute(
CombatLogRouteRequestModel::createFromArray($validated)
);
Stopwatch::pause('SiteController::benchmark');

// dump('hey');
return view('misc.credits');
}

/**
* @return RedirectResponse|Redirector
*/
Expand Down
64 changes: 41 additions & 23 deletions app/Models/Dungeon.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ class Dungeon extends CacheModel implements MappingModelInterface, TracksPageVie

public $timestamps = false;

private $activeSeasonCache = null;

/**
* https://stackoverflow.com/a/34485411/771270
*/
Expand Down Expand Up @@ -340,9 +342,14 @@ public function getFacadeFloor(): ?Floor
* Get the season that is active for this dungeon right now (preferring upcoming seasons if current and next season overlap)
* @TODO business logic should not be in a model - move to service
*/
public function getActiveSeason(SeasonServiceInterface $seasonService): ?Season
public function getActiveSeason(SeasonServiceInterface $seasonService, bool $useCache = true): ?Season
{
return $seasonService->getUpcomingSeasonForDungeon($this) ??
if ($useCache && $this->activeSeasonCache !== null) {
return $this->activeSeasonCache;
}

return $this->activeSeasonCache =
$seasonService->getUpcomingSeasonForDungeon($this) ??
$seasonService->getMostRecentSeasonForDungeon($this) ??
// Timewalking fallback
$seasonService->getCurrentSeason($this->expansion);
Expand Down Expand Up @@ -404,24 +411,25 @@ public function getInUseNpcs(): Collection
{
return Npc::select('npcs.*')
->leftJoin('npc_enemy_forces', 'npcs.id', 'npc_enemy_forces.npc_id')
->where(fn(Builder $builder) => $builder->where('npcs.dungeon_id', $this->id)
->orWhere('npcs.dungeon_id', -1))
->where(function (Builder $builder) {
$builder->where(function (Builder $builder) {
$builder->where(fn(Builder $builder) => $builder->where('npcs.dungeon_id', $this->id)
->orWhere('npcs.dungeon_id', -1)
)->where(function (Builder $builder) {
// Enemy forces may be not set, that means that we assume 0. They MAY be missing entirely for bosses
// or for other exceptions listed below
$builder->where('npc_enemy_forces.mapping_version_id', $this->currentMappingVersion->id)
->whereNotNull('npc_enemy_forces.enemy_forces');
})->orWhereIn('npcs.classification_id', [
NpcClassification::ALL[NpcClassification::NPC_CLASSIFICATION_BOSS],
NpcClassification::ALL[NpcClassification::NPC_CLASSIFICATION_FINAL_BOSS],
NpcClassification::ALL[NpcClassification::NPC_CLASSIFICATION_RARE],
])->orWhereIn('npcs.id', [
// Neltharion's Lair:
->orWhereNull('npc_enemy_forces.id');
});
})
->when($this->key === self::DUNGEON_NELTHARIONS_LAIR, function (Builder $builder) {
$builder->orWhereIn('npcs.id', [
// Burning Geodes are in the mapping but give 0 enemy forces.
// They're in the mapping because they're dangerous af
101437,

]);
})
->when($this->key === self::DUNGEON_THE_NECROTIC_WAKE, function (Builder $builder) {
$builder->orWhereIn('npcs.id', [
// Necrotic Wake:
// Brittlebone Warrior is in the mapping but gives 0 enemy forces.
163122,
Expand All @@ -435,13 +443,17 @@ public function getInUseNpcs(): Collection
163622,
// Rotspew Leftovers
163623,

// Halls of Infusion:
]);
})
->when($this->key === self::DUNGEON_HALLS_OF_INFUSION, function (Builder $builder) {
$builder->orWhereIn('npcs.id', [
// Aqua Ragers are in the mapping but give 0 enemy forces - so would be excluded.
// They're in the mapping because they are a significant drain on time and excluding them would raise questions about why they're gone
190407,

// Brackenhide Hollow:
]);
})
->when($this->key === self::DUNGEON_BRACKENHIDE_HOLLOW, function (Builder $builder) {
$builder->orWhereIn('npcs.id', [
// Witherlings that are a significant nuisance to be included in the mapping. They give 0 enemy forces.
194273,
// Rotfang Hyena are part of Gutshot boss but, they are part of the mapping. They give 0 enemy forces.
Expand All @@ -452,8 +464,10 @@ public function getInUseNpcs(): Collection
194469,
// Gutstabbers give 0 enemy forces but are in the mapping regardless
197857,

// Nokhud Offensive:
]);
})
->when($this->key === self::DUNGEON_THE_NOKHUD_OFFENSIVE, function (Builder $builder) {
$builder->orWhereIn('npcs.id', [
// War Ohuna gives 0 enemy forces but is in the mapping regardless
192803,
// Stormsurge Totem gives 0 enemy forces but is in the mapping regardless
Expand All @@ -462,14 +476,18 @@ public function getInUseNpcs(): Collection
194895,
// Primal Gust gives 0 enemy forces but is in the mapping regardless
195579,

// Dawn of the Infinite:
]);
})
->when(in_array($this->key, [self::DUNGEON_DAWN_OF_THE_INFINITE_GALAKRONDS_FALL, self::DUNGEON_DAWN_OF_THE_INFINITE_MUROZONDS_RISE]), function (Builder $builder) {
$builder->orWhereIn('npcs.id', [
// Temporal Deviation gives 0 enemy forces but is in the mapping regardless
206063,
// Iridikron's Creation
204918,

// City of Threads:
]);
})
->when($this->key === self::DUNGEON_CITY_OF_THREADS, function (Builder $builder) {
$builder->orWhereIn('npcs.id', [
// Eye of the Queen gives 0 enemy forces but is in the mapping regardless
220003,
]);
Expand Down
5 changes: 5 additions & 0 deletions app/Providers/KeystoneGuruServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ public function boot(
return;
}

// API requests don't need to load this in at all! We don't use the view cache since there's no view
if (Str::startsWith(request()->getRequestUri(), ['/api', '/benchmark'])) {
return;
}

session_set_cookie_params([
'secure' => true,
'httponly' => false,
Expand Down
4 changes: 2 additions & 2 deletions app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ protected function configureRateLimiting(): void
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 60)->by($this->userKey($request));
});
RateLimiter::for('create-user', function (Request $request) {
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 10)->by($this->userKey($request));
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 50)->by($this->userKey($request));
});

// Heavy GET requests
Expand Down Expand Up @@ -109,7 +109,7 @@ private function configureApiRateLimiting(): void
return $this->noLimitForExemptionsApi($request) ?? Limit::perMinute(self::RATE_LIMIT_OVERRIDE_PER_MINUTE_API ?? 120)->by($this->userKey($request));
});
RateLimiter::for('api-combatlog-correct-event', function (Request $request) {
return $this->noLimitForExemptionsApi($request) ?? Limit::perMinute(self::RATE_LIMIT_OVERRIDE_PER_MINUTE_API ?? 240)->by($this->userKey($request));
return $this->noLimitForExemptionsApi($request) ?? Limit::perMinute(self::RATE_LIMIT_OVERRIDE_PER_MINUTE_API ?? 500)->by($this->userKey($request));
});
RateLimiter::for('api-create-dungeonroute-thumbnail', function (Request $request) {
return $this->noLimitForExemptionsApi($request) ?? Limit::perMinute(self::RATE_LIMIT_OVERRIDE_PER_MINUTE_API ?? 30)->by($this->userKey($request));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
use App;
use App\Http\Models\Request\CombatLog\Route\CombatLogRouteNpcRequestModel;
use App\Http\Models\Request\CombatLog\Route\CombatLogRouteRequestModel;
use App\Http\Models\Request\CombatLog\Route\CombatLogRouteSpellRequestModel;
use App\Logic\Utils\Stopwatch;
use App\Models\DungeonRoute\DungeonRoute;
use App\Models\Floor\Floor;
use App\Models\Spell\Spell;
use App\Repositories\Interfaces\AffixGroup\AffixGroupRepositoryInterface;
use App\Repositories\Interfaces\DungeonRoute\DungeonRouteAffixGroupRepositoryInterface;
use App\Repositories\Interfaces\DungeonRoute\DungeonRouteRepositoryInterface;
Expand All @@ -20,6 +23,7 @@
use App\Service\Coordinates\CoordinatesServiceInterface;
use App\Service\Season\SeasonServiceInterface;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Random\RandomException;

/**
Expand All @@ -31,6 +35,9 @@ class CombatLogRouteDungeonRouteBuilder extends DungeonRouteBuilder
{
private CombatLogRouteDungeonRouteBuilderLoggingInterface $log;

/** @var Collection<int> */
protected Collection $validSpellIds;

/**
* @throws DungeonNotSupportedException
* @throws RandomException
Expand Down Expand Up @@ -62,6 +69,10 @@ public function __construct(
$log
);

$this->validSpellIds = Spell::whereIn('id',
$combatLogRoute->spells->map(fn(CombatLogRouteSpellRequestModel $spell) => $spell->spellId)
)->get()->keyBy('id');

/** @var CombatLogRouteDungeonRouteBuilderLoggingInterface $log */
$this->log = $log;
}
Expand Down Expand Up @@ -114,12 +125,21 @@ private function buildKillZones(): void
// return $event;
// }));

$floorCache = collect();
$firstEngagedAt = null;
foreach ($npcEngagedAndDiedEvents as $event) {
/** @var $event array{type: string, timestamp: Carbon, npc: CombatLogRouteNpcRequestModel} */
$realUiMapId = Floor::UI_MAP_ID_MAPPING[$event['npc']->coord->uiMapId] ?? $event['npc']->coord->uiMapId;
if ($this->currentFloor === null || $realUiMapId !== $this->currentFloor->ui_map_id) {
$newFloor = Floor::findByUiMapId($event['npc']->coord->uiMapId, $this->dungeonRoute->dungeon_id);
$newFloor = $floorCache->get($event['npc']->coord->uiMapId);

if ($newFloor === null) {
$floorCache->put(
$event['npc']->coord->uiMapId,
$newFloor = Floor::findByUiMapId($event['npc']->coord->uiMapId, $this->dungeonRoute->dungeon_id)
);
}

if ($newFloor === null) {
// Floor not found = we stay on the current floor
$this->log->buildKillZonesFloorNotFound($this->currentFloor?->id, $event['npc']->coord->uiMapId, $this->dungeonRoute->dungeon_id);
Expand Down Expand Up @@ -231,15 +251,15 @@ private function determineSpellsCastBetween(ActivePull $activePull, ?Carbon $las
foreach ($this->combatLogRoute->spells as $spell) {
if ($lastDiedAt !== null) {
if ($spell->getCastAt()->between($firstEngagedAt, $lastDiedAt)) {
if ($this->validSpellIds->search($spell->spellId) === false) {
if (!$this->validSpellIds->has($spell->spellId)) {
$this->log->determineSpellsCastBetweenInvalidSpellIdBetween($spell->spellId);

continue;
}
$activePull->addSpell($spell->spellId);
}
} else if ($spell->getCastAt()->isAfter($firstEngagedAt)) {
if ($this->validSpellIds->search($spell->spellId) === false) {
if (!$this->validSpellIds->has($spell->spellId)) {
$this->log->determineSpellsCastBetweenInvalidSpellIdAfter($spell->spellId);

continue;
Expand Down
5 changes: 0 additions & 5 deletions app/Service/CombatLog/Builders/DungeonRouteBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use App\Models\KillZone\KillZone;
use App\Models\KillZone\KillZoneEnemy;
use App\Models\Npc\NpcClassification;
use App\Models\Spell\Spell;
use App\Repositories\Interfaces\DungeonRoute\DungeonRouteRepositoryInterface;
use App\Repositories\Interfaces\KillZone\KillZoneEnemyRepositoryInterface;
use App\Repositories\Interfaces\KillZone\KillZoneRepositoryInterface;
Expand Down Expand Up @@ -60,9 +59,6 @@ abstract class DungeonRouteBuilder
/** @var Collection<int> */
protected Collection $validNpcIds;

/** @var Collection<int> */
protected Collection $validSpellIds;

private int $killZoneIndex = 1;

/** @var Collection<KillZone> */
Expand Down Expand Up @@ -94,7 +90,6 @@ public function __construct(
// #1818 Filter out any NPC ids that are invalid
$this->validNpcIds = $this->dungeonRoute->dungeon->getInUseNpcIds();

$this->validSpellIds = Spell::all('id')->pluck(['id']);
$this->activePullCollection = new ActivePullCollection();

// This allows me to set the killZones in buildFinished, so that existing relations are still preserved
Expand Down
2 changes: 1 addition & 1 deletion config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@

'log' => env('APP_LOG', 'single'),

'log_level' => env('APP_LOG_LEVEL', 'debug'),
'log_level' => env('LOG_LEVEL', 'debug'),

/*
|--------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('npc_enemy_forces', function (Blueprint $table) {
$table->index('npc_id');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('npc_enemy_forces', function (Blueprint $table) {
$table->dropIndex('npc_id');
});
}
};
25 changes: 25 additions & 0 deletions database/seeders/releases/v11.9.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"id": 268,
"release_changelog_id": 275,
"version": "v11.9.2",
"title": "Further optimizations to API calls",
"backup_db": 1,
"silent": 1,
"spotlight": 0,
"released": 0,
"created_at": "2025-01-22T11:31:41+00:00",
"updated_at": "2025-01-22T11:31:41+00:00",
"changelog": {
"id": 275,
"release_id": 268,
"description": null,
"changes": [
{
"release_changelog_id": 275,
"release_changelog_category_id": 12,
"ticket_id": 2668,
"change": "API endpoints are now quicker to load."
}
]
}
}
Loading

0 comments on commit 6ef21d1

Please sign in to comment.