diff --git a/database/factories/ActionlogFactory.php b/database/factories/ActionlogFactory.php index edf9955198..ba3e469b4a 100644 --- a/database/factories/ActionlogFactory.php +++ b/database/factories/ActionlogFactory.php @@ -40,11 +40,15 @@ class ActionlogFactory extends Factory $target = User::inRandomOrder()->first(); $asset = Asset::inRandomOrder()->RTD()->first(); + // Fall back to the asset's default (rtd) location when the target + // user has no location set. Mirrors the fallback in + // App\Console\Commands\SyncAssetLocations so we don't seed assets + // with a null location_id. $asset->update( [ 'assigned_to' => $target->id, 'assigned_type' => User::class, - 'location_id' => $target->location_id, + 'location_id' => $target->location_id ?? $asset->rtd_location_id, ] ); diff --git a/database/factories/AssetFactory.php b/database/factories/AssetFactory.php index fab04b687c..246722bad4 100644 --- a/database/factories/AssetFactory.php +++ b/database/factories/AssetFactory.php @@ -60,6 +60,23 @@ class AssetFactory extends Factory $asset->asset_eol_date = $this->faker->boolean(5) ? CarbonImmutable::parse($asset->purchase_date)->addMonths(rand(0, 20))->format('Y-m-d') : CarbonImmutable::parse($asset->purchase_date)->addMonths($asset->model?->eol ?? rand(12, 60))->format('Y-m-d'); + + // Set location_id to match the asset's current allocation. Mirrors + // the four cases in App\Console\Commands\SyncAssetLocations so a + // fresh db:seed doesn't need to run that command as post-processing + // (which allocated ~130 MB scanning every asset). Any state that + // pre-sets location_id (e.g., a caller passes it explicitly) wins. + if ($asset->location_id !== null) { + return; + } + + $asset->location_id = match (true) { + $asset->assigned_to === null => $asset->rtd_location_id, + $asset->assigned_type === User::class => User::find($asset->assigned_to)?->location_id ?? $asset->rtd_location_id, + $asset->assigned_type === Location::class => $asset->assigned_to, + $asset->assigned_type === Asset::class => Asset::find($asset->assigned_to)?->location_id ?? $asset->rtd_location_id, + default => $asset->rtd_location_id, + }; }); } diff --git a/database/seeders/ActionlogSeeder.php b/database/seeders/ActionlogSeeder.php index 3903980007..eaabb09b72 100644 --- a/database/seeders/ActionlogSeeder.php +++ b/database/seeders/ActionlogSeeder.php @@ -6,10 +6,13 @@ use App\Models\Actionlog; use App\Models\Asset; use App\Models\Location; use App\Models\User; +use Database\Seeders\Concerns\ReportsMemory; use Illuminate\Database\Seeder; class ActionlogSeeder extends Seeder { + use ReportsMemory; + public function run() { Actionlog::truncate(); @@ -24,19 +27,30 @@ class ActionlogSeeder extends Seeder $admin = User::where('permissions->superuser', '1')->first() ?? User::factory()->firstAdmin()->create(); + $this->reportMemory('ActionlogSeeder start'); + + memory_reset_peak_usage(); Actionlog::factory() ->count(300) ->assetCheckoutToUser() ->create(['created_by' => $admin->id]); + gc_collect_cycles(); + $this->reportMemory('ActionlogSeeder after 300 assetCheckoutToUser'); + memory_reset_peak_usage(); Actionlog::factory() ->count(100) ->assetCheckoutToLocation() ->create(['created_by' => $admin->id]); + gc_collect_cycles(); + $this->reportMemory('ActionlogSeeder after 100 assetCheckoutToLocation'); + memory_reset_peak_usage(); Actionlog::factory() ->count(20) ->licenseCheckoutToUser() ->create(['created_by' => $admin->id]); + gc_collect_cycles(); + $this->reportMemory('ActionlogSeeder after 20 licenseCheckoutToUser'); } } diff --git a/database/seeders/AssetSeeder.php b/database/seeders/AssetSeeder.php index d6d167d4f4..03ed2c1c16 100644 --- a/database/seeders/AssetSeeder.php +++ b/database/seeders/AssetSeeder.php @@ -6,6 +6,7 @@ use App\Models\Asset; use App\Models\Location; use App\Models\Supplier; use App\Models\User; +use Database\Seeders\Concerns\ReportsMemory; use Illuminate\Database\Eloquent\Factories\Sequence; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; @@ -14,6 +15,8 @@ use Illuminate\Support\Facades\Storage; class AssetSeeder extends Seeder { + use ReportsMemory; + private $admin; private $locationIds; @@ -31,7 +34,19 @@ class AssetSeeder extends Seeder $this->locationIds = Location::all()->pluck('id'); $this->supplierIds = Supplier::all()->pluck('id'); - Asset::factory()->count(2000)->laptopMbp()->state(new Sequence($this->getState()))->create(); + $this->reportMemory('AssetSeeder start'); + + // Chunk the big laptopMbp batch so we don't hold 2000 Asset models + // (plus 2000 Actionlog observer side-effects) in memory at once, which + // was pushing the demo servers into swap during full re-seeds. + memory_reset_peak_usage(); + for ($i = 0; $i < 10; $i++) { + Asset::factory()->count(200)->laptopMbp()->state(new Sequence($this->getState()))->create(); + gc_collect_cycles(); + } + $this->reportMemory('AssetSeeder after laptopMbp chunked batch (2000 total)'); + + memory_reset_peak_usage(); Asset::factory()->count(50)->laptopMbpPending()->state(new Sequence($this->getState()))->create(); Asset::factory()->count(50)->laptopMbpArchived()->state(new Sequence($this->getState()))->create(); Asset::factory()->count(50)->laptopAir()->state(new Sequence($this->getState()))->create(); @@ -63,6 +78,8 @@ class AssetSeeder extends Seeder } DB::table('checkout_requests')->truncate(); + + $this->reportMemory('AssetSeeder end (all factory batches complete)'); } private function ensureLocationsSeeded() diff --git a/database/seeders/Concerns/ReportsMemory.php b/database/seeders/Concerns/ReportsMemory.php new file mode 100644 index 0000000000..680f49b27d --- /dev/null +++ b/database/seeders/Concerns/ReportsMemory.php @@ -0,0 +1,22 @@ +command is + * null (e.g., when the seeder is invoked outside of an Artisan command). + */ +trait ReportsMemory +{ + protected function reportMemory(string $label): void + { + $peakMb = number_format(memory_get_peak_usage(true) / 1024 / 1024, 1); + $currentMb = number_format(memory_get_usage(true) / 1024 / 1024, 1); + $this->command?->info("[mem] {$label}: peak {$peakMb} MB, current {$currentMb} MB"); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index a107cb814e..5d369e4b60 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -3,14 +3,15 @@ namespace Database\Seeders; use App\Models\Setting; +use Database\Seeders\Concerns\ReportsMemory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Seeder; -use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Log; class DatabaseSeeder extends Seeder { + use ReportsMemory; + /** * Run the database seeds. * @@ -21,6 +22,8 @@ class DatabaseSeeder extends Seeder Model::unguard(); DB::statement('SET FOREIGN_KEY_CHECKS=0'); + $this->reportMemory('DatabaseSeeder start'); + // Only create default settings if they do not exist in the db. if (! Setting::first()) { // factory(Setting::class)->create(); @@ -28,33 +31,56 @@ class DatabaseSeeder extends Seeder } $this->call(CompanySeeder::class); + $this->reportMemory('after CompanySeeder'); $this->call(CategorySeeder::class); + $this->reportMemory('after CategorySeeder'); $this->call(LocationSeeder::class); + $this->reportMemory('after LocationSeeder'); $this->call(DepartmentSeeder::class); + $this->reportMemory('after DepartmentSeeder'); $this->call(UserSeeder::class); + $this->reportMemory('after UserSeeder'); $this->call(DepreciationSeeder::class); + $this->reportMemory('after DepreciationSeeder (1st)'); $this->call(ManufacturerSeeder::class); + $this->reportMemory('after ManufacturerSeeder'); $this->call(SupplierSeeder::class); + $this->reportMemory('after SupplierSeeder'); $this->call(AssetModelSeeder::class); + $this->reportMemory('after AssetModelSeeder'); $this->call(DepreciationSeeder::class); + $this->reportMemory('after DepreciationSeeder (2nd)'); $this->call(StatuslabelSeeder::class); + $this->reportMemory('after StatuslabelSeeder'); $this->call(AccessorySeeder::class); + $this->reportMemory('after AccessorySeeder'); $this->call(CustomFieldSeeder::class); + $this->reportMemory('after CustomFieldSeeder'); $this->call(AssetSeeder::class); + $this->reportMemory('after AssetSeeder'); $this->call(LicenseSeeder::class); + $this->reportMemory('after LicenseSeeder'); $this->call(ComponentSeeder::class); + $this->reportMemory('after ComponentSeeder'); $this->call(ConsumableSeeder::class); + $this->reportMemory('after ConsumableSeeder'); $this->call(ActionlogSeeder::class); + $this->reportMemory('after ActionlogSeeder'); $this->call(MaintenanceSeeder::class); + $this->reportMemory('after MaintenanceSeeder'); - Artisan::call('snipeit:sync-asset-locations', ['--output' => 'all']); - $output = Artisan::output(); - Log::info($output); + // snipeit:sync-asset-locations used to run here to backfill location_id + // on seeded assets. AssetFactory::configure() now sets location_id at + // make-time based on the assignment state, so post-seed sync is + // redundant. The command remains available as a manual maintenance + // tool for production databases that need drift correction. Model::reguard(); DB::statement('SET FOREIGN_KEY_CHECKS=1'); DB::table('imports')->truncate(); DB::table('requested_assets')->truncate(); + + $this->reportMemory('DatabaseSeeder end'); } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index 46eb6260f8..5a016c1ae5 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -5,6 +5,7 @@ namespace Database\Seeders; use App\Models\Company; use App\Models\Department; use App\Models\User; +use Database\Seeders\Concerns\ReportsMemory; use Illuminate\Database\Eloquent\Factories\Sequence; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Log; @@ -12,6 +13,8 @@ use Illuminate\Support\Facades\Storage; class UserSeeder extends Seeder { + use ReportsMemory; + /** * Run the database seeds. * @@ -71,37 +74,57 @@ class UserSeeder extends Seeder // ~30% (600) no company // ~50% (1000) one company // ~20% (400) two or three companies + // + // Chunked so we don't hold 1000-2000 User models in memory at once + // (each ->each() / foreach that follows a big ->create() otherwise + // materializes the whole collection). Reduced demo-server memory + // pressure that was pushing seeding into swap. + $chunk = 200; - User::factory()->count(600)->viewAssets() - ->withoutCompany() - ->state(new Sequence(fn ($sequence) => [ - 'department_id' => $departmentIds->random(), - ])) - ->create(); + $departmentState = fn () => new Sequence(fn ($sequence) => [ + 'department_id' => $departmentIds->random(), + ]); - User::factory()->count(1000)->viewAssets() - ->withoutCompany() - ->state(new Sequence(fn ($sequence) => [ - 'department_id' => $departmentIds->random(), - ])) - ->create() - ->each(function (User $user) use ($companyIds) { - $user->companies()->sync([$companyIds->random()]); - $user->syncLegacyCompanyIdMirror(); - }); + $this->reportMemory('UserSeeder start of regular-user batches'); - $multiCompanyUsers = User::factory()->count(400)->viewAssets() - ->withoutCompany() - ->state(new Sequence(fn ($sequence) => [ - 'department_id' => $departmentIds->random(), - ])) - ->create(); - - foreach ($multiCompanyUsers as $user) { - $ids = $companyIds->random(min(rand(2, 3), $companyIds->count()))->toArray(); - $user->companies()->sync($ids); - $user->syncLegacyCompanyIdMirror(); + memory_reset_peak_usage(); + for ($i = 0; $i < 600 / $chunk; $i++) { + User::factory()->count($chunk)->viewAssets() + ->withoutCompany() + ->state($departmentState()) + ->create(); + gc_collect_cycles(); } + $this->reportMemory('UserSeeder after 600 no-company users (chunked)'); + + memory_reset_peak_usage(); + for ($i = 0; $i < 1000 / $chunk; $i++) { + User::factory()->count($chunk)->viewAssets() + ->withoutCompany() + ->state($departmentState()) + ->create() + ->each(function (User $user) use ($companyIds) { + $user->companies()->sync([$companyIds->random()]); + $user->syncLegacyCompanyIdMirror(); + }); + gc_collect_cycles(); + } + $this->reportMemory('UserSeeder after 1000 one-company users (chunked)'); + + memory_reset_peak_usage(); + for ($i = 0; $i < 400 / $chunk; $i++) { + User::factory()->count($chunk)->viewAssets() + ->withoutCompany() + ->state($departmentState()) + ->create() + ->each(function (User $user) use ($companyIds) { + $ids = $companyIds->random(min(rand(2, 3), $companyIds->count()))->toArray(); + $user->companies()->sync($ids); + $user->syncLegacyCompanyIdMirror(); + }); + gc_collect_cycles(); + } + $this->reportMemory('UserSeeder after 400 multi-company users (chunked)'); $src = public_path('/img/demo/avatars/'); $dst = 'avatars'.'/'; @@ -138,5 +161,6 @@ class UserSeeder extends Seeder $file_number++; } + $this->reportMemory('UserSeeder end (all users + avatars complete)'); } }