diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 23b24eaad9..e623930c59 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -1481,7 +1481,7 @@ class Asset extends Depreciable public function scopePending($query) { // Pluck IDs then whereIn — do NOT replace with whereHas. whereHas generates a correlated EXISTS per row and causes severe slowdowns in withCount contexts. - $ids = Statuslabel::where('deployable', 0)->where('pending', 1)->where('archived', 0)->whereNull('deleted_at')->pluck('id'); + $ids = Statuslabel::idsFor('pending'); return $query->whereIn('assets.status_id', $ids->isEmpty() ? [0] : $ids); } @@ -1534,7 +1534,7 @@ class Asset extends Depreciable public function scopeRTD($query) { // Pluck IDs then whereIn — do NOT replace with whereHas. whereHas generates a correlated EXISTS per row and causes severe slowdowns in withCount contexts. - $ids = Statuslabel::where('deployable', 1)->where('pending', 0)->where('archived', 0)->whereNull('deleted_at')->pluck('id'); + $ids = Statuslabel::idsFor('deployable'); return $query->whereNull('assets.assigned_to') ->whereIn('assets.status_id', $ids->isEmpty() ? [0] : $ids); @@ -1549,7 +1549,7 @@ class Asset extends Depreciable public function scopeUndeployable($query) { // Pluck IDs then whereIn — do NOT replace with whereHas. whereHas generates a correlated EXISTS per row and causes severe slowdowns in withCount contexts. - $ids = Statuslabel::where('deployable', 0)->where('pending', 0)->where('archived', 0)->whereNull('deleted_at')->pluck('id'); + $ids = Statuslabel::idsFor('undeployable'); return $query->whereIn('assets.status_id', $ids->isEmpty() ? [0] : $ids); } @@ -1563,7 +1563,7 @@ class Asset extends Depreciable public function scopeNotArchived($query) { // Pluck IDs then whereIn — do NOT replace with whereHas. whereHas generates a correlated EXISTS per row and causes severe slowdowns in withCount contexts. - $ids = Statuslabel::where('archived', 0)->whereNull('deleted_at')->pluck('id'); + $ids = Statuslabel::idsFor('not_archived'); return $query->whereIn('assets.status_id', $ids->isEmpty() ? [0] : $ids); } @@ -1730,9 +1730,7 @@ class Asset extends Depreciable { // Pluck IDs then whereIn — do NOT replace with whereHas. whereHas generates a correlated EXISTS per row and causes severe slowdowns in withCount contexts. if (Setting::getSettings()->show_archived_in_list != 1) { - $validStatusIds = Statuslabel::where('archived', 0) - ->whereNull('deleted_at') - ->pluck('id'); + $validStatusIds = Statuslabel::idsFor('not_archived'); return $query->whereIn('assets.status_id', $validStatusIds->isEmpty() ? [0] : $validStatusIds); } @@ -1749,7 +1747,7 @@ class Asset extends Depreciable public function scopeArchived($query) { // Pluck IDs then whereIn — do NOT replace with whereHas. whereHas generates a correlated EXISTS per row and causes severe slowdowns in withCount contexts. - $ids = Statuslabel::where('deployable', 0)->where('pending', 0)->where('archived', 1)->whereNull('deleted_at')->pluck('id'); + $ids = Statuslabel::idsFor('archived'); return $query->whereIn('assets.status_id', $ids->isEmpty() ? [0] : $ids); } diff --git a/app/Models/AssetModel.php b/app/Models/AssetModel.php index 2bd5792cae..31d1474e86 100755 --- a/app/Models/AssetModel.php +++ b/app/Models/AssetModel.php @@ -160,11 +160,17 @@ class AssetModel extends SnipeModel public function percentRemaining() { - if ($this->availableAssets()->count() == 0) { + // Cache the available count locally — the old code called + // availableAssets()->count() twice, which under the API transformer + // loop showed up as a duplicated `select count(*) … assets where + // model_id = ?` for every model in the response (visible in any + // /api/v1/models?category_id=… query log). + $available = $this->availableAssets()->count(); + if ($available === 0) { return 0; } - return $this->availableAssets()->count() / $this->assets()->count() * 100; + return $available / $this->assets()->count() * 100; } /** diff --git a/app/Models/Statuslabel.php b/app/Models/Statuslabel.php index c0a30711f4..d5fedc4dce 100755 --- a/app/Models/Statuslabel.php +++ b/app/Models/Statuslabel.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Query\Builder; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Gate; use Watson\Validating\ValidatingTrait; @@ -47,6 +48,51 @@ class Statuslabel extends SnipeModel use Searchable; + /** + * Per-request memo of the ID lists used by Asset's status-driven scopes + * (RTD / Pending / Undeployable / Archived / NotArchived). Each scope + * fired a separate `SELECT id FROM status_labels WHERE …` query on every + * call before this was introduced, which under withCount + transformer + * loops (see AssetModelsTransformer) added dozens of redundant queries + * per page or API hit. + * + * Cleared on save/delete via model events below, and reset between tests + * in InitializesSettings (alongside Setting::$_cache). + */ + protected static array $statusIdCache = []; + + /** + * Return the cached set of status_label IDs matching the requested + * "kind" — one of: deployable, pending, undeployable, archived, + * not_archived. Each call after the first in a single request reads + * from memory. + */ + public static function idsFor(string $type): Collection + { + return self::$statusIdCache[$type] ??= match ($type) { + 'deployable' => self::where('deployable', 1)->where('pending', 0)->where('archived', 0)->whereNull('deleted_at')->pluck('id'), + 'pending' => self::where('deployable', 0)->where('pending', 1)->where('archived', 0)->whereNull('deleted_at')->pluck('id'), + 'undeployable' => self::where('deployable', 0)->where('pending', 0)->where('archived', 0)->whereNull('deleted_at')->pluck('id'), + 'archived' => self::where('deployable', 0)->where('pending', 0)->where('archived', 1)->whereNull('deleted_at')->pluck('id'), + 'not_archived' => self::where('archived', 0)->whereNull('deleted_at')->pluck('id'), + default => throw new \InvalidArgumentException('Unknown status type: '.$type), + }; + } + + public static function clearIdCache(): void + { + self::$statusIdCache = []; + } + + protected static function booted(): void + { + // Any mutation to a status label invalidates the cached ID lists. + $invalidate = fn () => self::clearIdCache(); + static::saved($invalidate); + static::deleted($invalidate); + static::restored($invalidate); + } + /** * The attributes that should be included when searching the model. * diff --git a/resources/views/blade/box/index.blade.php b/resources/views/blade/box/index.blade.php index f0e1976920..8be64fb1e0 100644 --- a/resources/views/blade/box/index.blade.php +++ b/resources/views/blade/box/index.blade.php @@ -31,9 +31,10 @@ @endif - @if (($slot) && (!$slot->isEmpty())) - {{ $slot }} - @endif + {{-- Render slot unconditionally — ComponentSlot::isEmpty() + materializes the slot to inspect it, doubling every DB call + inside. An empty slot renders nothing visible. --}} + {{ $slot }} diff --git a/resources/views/blade/tabs/index.blade.php b/resources/views/blade/tabs/index.blade.php index 77591a7a0b..a40ec24cde 100644 --- a/resources/views/blade/tabs/index.blade.php +++ b/resources/views/blade/tabs/index.blade.php @@ -6,16 +6,17 @@
@endif - @if ((isset($content)) && (!$content->isEmpty())) + {{-- Render slots unconditionally — ComponentSlot::isEmpty() + materializes the slot to inspect it, doubling every DB call + inside (asset/model counts, presenter dataTableLayout, etc.). + `isset($content)` is fine because it's a named slot check that + doesn't render anything. --}} + @isset($content) {{ $content }} - @endif + @endisset - @if (($slot) && (!$slot->isEmpty())) - {{ $slot }} - @endif + {{ $slot }} diff --git a/resources/views/categories/view.blade.php b/resources/views/categories/view.blade.php index a4a4687d48..5e0c03d1c1 100644 --- a/resources/views/categories/view.blade.php +++ b/resources/views/categories/view.blade.php @@ -20,15 +20,22 @@