F
Filament15mo ago
Shavik

->fillForm is very slow in tests

I'm working on writing some feature/integration tests a simple Filament App. It tracks 'people'. I have the following test that should test that a user can be created. My problem is the ->fillForm method is very slow. It takes around 20 seconds to run. This code look scarier than it should as I've broken many things down to time them.
it('can create person', function () {
$personData = Person::factory()->make();
$personData->load('physicalAddress');

$addressData = Address::factory()->type(AddressType::PHYSICAL)->make();
$formData = array_merge($personData->toArray(), ['physicalAddress' => $addressData->toArray()]);
if ($personData->height_inches !== null) {
$formData['feet'] = (int)floor($personData->height_inches / 12);
$formData['inches'] = $personData->height_inches % 12;
}
dump($formData);

dump('performing livewire request.');
$livewire = livewire(PersonResource\Pages\CreatePerson::class);

DB::connection()->enableQueryLog();
$start = microtime(true);
$livewire->fillForm($formData);
dump('fillForm: ' . round((microtime(true) - $start), 2) . " seconds");


$start = microtime(true);
$livewire->call('create');
dump('call: ' . round((microtime(true) - $start), 2) . " seconds");

$start = microtime(true);
$livewire->assertHasNoFormErrors();
dump('assertHasNoFormErrors: ' . round((microtime(true) - $start), 2) . " seconds");

$queries = DB::getRawQueryLog();
dump($queries);

dump('checking database...');
$this->assertDatabaseHas(Person::class, $personData->withoutRelations()->toArray());
$addressData->setAppends([]);
$this->assertDatabaseHas(Address::class, $addressData->toArray());
});
it('can create person', function () {
$personData = Person::factory()->make();
$personData->load('physicalAddress');

$addressData = Address::factory()->type(AddressType::PHYSICAL)->make();
$formData = array_merge($personData->toArray(), ['physicalAddress' => $addressData->toArray()]);
if ($personData->height_inches !== null) {
$formData['feet'] = (int)floor($personData->height_inches / 12);
$formData['inches'] = $personData->height_inches % 12;
}
dump($formData);

dump('performing livewire request.');
$livewire = livewire(PersonResource\Pages\CreatePerson::class);

DB::connection()->enableQueryLog();
$start = microtime(true);
$livewire->fillForm($formData);
dump('fillForm: ' . round((microtime(true) - $start), 2) . " seconds");


$start = microtime(true);
$livewire->call('create');
dump('call: ' . round((microtime(true) - $start), 2) . " seconds");

$start = microtime(true);
$livewire->assertHasNoFormErrors();
dump('assertHasNoFormErrors: ' . round((microtime(true) - $start), 2) . " seconds");

$queries = DB::getRawQueryLog();
dump($queries);

dump('checking database...');
$this->assertDatabaseHas(Person::class, $personData->withoutRelations()->toArray());
$addressData->setAppends([]);
$this->assertDatabaseHas(Address::class, $addressData->toArray());
});
The output is the following:
"performing livewire request." // tests/Feature/PersonTest.php:43
"fillForm: 20.66 seconds" // tests/Feature/PersonTest.php:54
"call: 0.44 seconds" // tests/Feature/PersonTest.php:59
"assertHasNoFormErrors: 0.4 seconds" // tests/Feature/PersonTest.php:63
array:5 [ // tests/Feature/PersonTest.php:66
0 => array:2 [
"raw_query" => "insert into "people" ("first_name", "middle_name", "last_name", "maternal_last_name", "gender", "birthday", "place_of_birth", "weight_pounds", "eye_color", "hair_color", "email", "home_phone", "cell_phone", "marketing_comms_enabled", "update_comms_enabled", "user_id", "updated_at", "created_at") values ('Zoey', null, 'Rath', 'McLaughlin', 'female', '1926-07-06', 'Corkeryfurt', 152, null, null, '[email protected]', '(914) 319-7774', '+1-316-757-2317', 0, 0, 1, '2023-09-18 22:19:37', '2023-09-18 22:19:37')"
"time" => 0.09
]
1 => array:2 [
"raw_query" => "insert into "addresses" ("type", "street", "suite", "city", "state", "zip", "updated_at", "created_at") values ('physical', '700 Gustave Light Suite 860', null, 'Port Devon', 'MO', '87688', '2023-09-18 22:19:37', '2023-09-18 22:19:37')"
"time" => 0.07
]
2 => array:2 [
"raw_query" => "update "people" set "physical_address_id" = 1, "updated_at" = '2023-09-18 22:19:37' where "id" = 1"
"time" => 0.04
]
3 => array:2 [
"raw_query" => "insert into "addresses" ("type", "street", "suite", "city", "state", "zip", "updated_at", "created_at") values ('mailing', null, null, null, null, null, '2023-09-18 22:19:37', '2023-09-18 22:19:37')"
"time" => 0.04
]
4 => array:2 [
"raw_query" => "update "people" set "mailing_address_id" = 2, "updated_at" = '2023-09-18 22:19:37' where "id" = 1"
"time" => 0.03
]
]
"checking database..." // tests/Feature/PersonTest.php:68

PASS Tests\Feature\PersonTest
"performing livewire request." // tests/Feature/PersonTest.php:43
"fillForm: 20.66 seconds" // tests/Feature/PersonTest.php:54
"call: 0.44 seconds" // tests/Feature/PersonTest.php:59
"assertHasNoFormErrors: 0.4 seconds" // tests/Feature/PersonTest.php:63
array:5 [ // tests/Feature/PersonTest.php:66
0 => array:2 [
"raw_query" => "insert into "people" ("first_name", "middle_name", "last_name", "maternal_last_name", "gender", "birthday", "place_of_birth", "weight_pounds", "eye_color", "hair_color", "email", "home_phone", "cell_phone", "marketing_comms_enabled", "update_comms_enabled", "user_id", "updated_at", "created_at") values ('Zoey', null, 'Rath', 'McLaughlin', 'female', '1926-07-06', 'Corkeryfurt', 152, null, null, '[email protected]', '(914) 319-7774', '+1-316-757-2317', 0, 0, 1, '2023-09-18 22:19:37', '2023-09-18 22:19:37')"
"time" => 0.09
]
1 => array:2 [
"raw_query" => "insert into "addresses" ("type", "street", "suite", "city", "state", "zip", "updated_at", "created_at") values ('physical', '700 Gustave Light Suite 860', null, 'Port Devon', 'MO', '87688', '2023-09-18 22:19:37', '2023-09-18 22:19:37')"
"time" => 0.07
]
2 => array:2 [
"raw_query" => "update "people" set "physical_address_id" = 1, "updated_at" = '2023-09-18 22:19:37' where "id" = 1"
"time" => 0.04
]
3 => array:2 [
"raw_query" => "insert into "addresses" ("type", "street", "suite", "city", "state", "zip", "updated_at", "created_at") values ('mailing', null, null, null, null, null, '2023-09-18 22:19:37', '2023-09-18 22:19:37')"
"time" => 0.04
]
4 => array:2 [
"raw_query" => "update "people" set "mailing_address_id" = 2, "updated_at" = '2023-09-18 22:19:37' where "id" = 1"
"time" => 0.03
]
]
"checking database..." // tests/Feature/PersonTest.php:68

PASS Tests\Feature\PersonTest
As you can see, there are no slow queries and I've isolated the slowness to the ->fillForm method. I can't see to figure out why that would be slowing things down to badly. Any help would be greatly appreciated!
17 Replies
cheesegrits
cheesegrits15mo ago
I'm assuming the real form doesn't have any obvious performance issues? Is there anything "unusual' in your form schema?
Shavik
ShavikOP15mo ago
Correct, the form on the website isn't slow. It's a 4 'page' wizard on the create page. The 'edit' page is 'all in one'
awcodes
awcodes15mo ago
Something feels off here. Why would the test be slower if it isn’t actually rendering anything. Weird.
Shavik
ShavikOP15mo ago
Here is the full output and the full test file. https://gist.github.com/chrisreedio/fc6b4ff875a061645d4fb8606b12c459
Gist
Output
GitHub Gist: instantly share code, notes, and snippets.
Shavik
ShavikOP15mo ago
it can create person takes like 23 seconds. I have tried both PostgresSQL and SQLite before I did the SQL query logging and realized none of the queries were the problem, They all execute quickly, no matter the database. The only thing I can think that would be different is that when developing, I run nginx w/ php-fpm. I'm really new to livewire tests so unsure how those actually execute. I know the built in artisan serve has quite poor performance with livewire based sites (in my experience) so was unsure if this was contributing but surely not 20ish seconds to fill a form?
awcodes
awcodes15mo ago
Is the address making an actual api call externally? Only thing thing that really seems out of place, not that it’s the problem is eager loading an address then creating a factory model for address after that. But I’m not sure of the difference, if there even is one.
Shavik
ShavikOP15mo ago
I don't expect you to read all this but this is my CreatePerson.php https://gist.github.com/chrisreedio/ddcf8aac65311921a91895582283603f It's just a wizard, with steps.
Gist
CreatePerson.php
GitHub Gist: instantly share code, notes, and snippets.
Shavik
ShavikOP15mo ago
Schema::create('people', function (Blueprint $table) {
$table->id();
// TODO - add 'user_id/tenant_id' column
$table->foreignIdFor(User::class)->constrained()->nullOnDelete();

$table->string('first_name');
$table->string('middle_name')->nullable();
$table->string('last_name');
$table->string('maternal_last_name')->nullable();
// $table->string('full_name')->virtualAs('first_name || \' \' || last_name');

// Demographics
$table->string('gender')->nullable();
$table->date('birthday')->nullable();
$table->string('place_of_birth')->nullable();
$table->integer('height_inches')->nullable();
$table->integer('weight_pounds')->nullable();
$table->string('eye_color')->nullable();
$table->string('hair_color')->nullable();

// Communications
$table->string('email')->nullable();
$table->string('home_phone')->nullable();
$table->string('cell_phone')->nullable();
$table->boolean('marketing_comms_enabled')->default(false);
$table->boolean('update_comms_enabled')->default(true);

// Relationships
$table->foreignIdFor(Address::class, 'physical_address_id')->nullable()->constrained('addresses')->nullOnDelete();
$table->foreignIdFor(Address::class, 'mailing_address_id')->nullable()->constrained('addresses')->nullOnDelete();

$table->softDeletes();
$table->timestamps();
});
Schema::create('people', function (Blueprint $table) {
$table->id();
// TODO - add 'user_id/tenant_id' column
$table->foreignIdFor(User::class)->constrained()->nullOnDelete();

$table->string('first_name');
$table->string('middle_name')->nullable();
$table->string('last_name');
$table->string('maternal_last_name')->nullable();
// $table->string('full_name')->virtualAs('first_name || \' \' || last_name');

// Demographics
$table->string('gender')->nullable();
$table->date('birthday')->nullable();
$table->string('place_of_birth')->nullable();
$table->integer('height_inches')->nullable();
$table->integer('weight_pounds')->nullable();
$table->string('eye_color')->nullable();
$table->string('hair_color')->nullable();

// Communications
$table->string('email')->nullable();
$table->string('home_phone')->nullable();
$table->string('cell_phone')->nullable();
$table->boolean('marketing_comms_enabled')->default(false);
$table->boolean('update_comms_enabled')->default(true);

// Relationships
$table->foreignIdFor(Address::class, 'physical_address_id')->nullable()->constrained('addresses')->nullOnDelete();
$table->foreignIdFor(Address::class, 'mailing_address_id')->nullable()->constrained('addresses')->nullOnDelete();

$table->softDeletes();
$table->timestamps();
});
The people migration nothing too fancy
awcodes
awcodes15mo ago
That all seems ok to me. Just doesn’t make sense for the test to be slower than actual use case. Unless it’s hitting an actual api that could be slow.
Shavik
ShavikOP15mo ago
I don't believe I have any of that in here. I did import the maps plugin but I have since removed it and it isn't used currently on any of the forms.
it('can create person', function () {
$personData = Person::factory()->make();
$personData->load('physicalAddress');

$addressData = Address::factory()->type(AddressType::PHYSICAL)->make();
$formData = array_merge($personData->toArray(), ['physicalAddress' => $addressData->toArray()]);
if ($personData->height_inches !== null) {
$formData['feet'] = (int)floor($personData->height_inches / 12);
$formData['inches'] = $personData->height_inches % 12;
}

livewire(PersonResource\Pages\CreatePerson::class)
->fillForm($formData)
->call('create')
->assertHasNoFormErrors();

$this->assertDatabaseHas(Person::class, $personData->withoutRelations()->toArray());
$addressData->setAppends([]);
$this->assertDatabaseHas(Address::class, $addressData->toArray());
});
it('can create person', function () {
$personData = Person::factory()->make();
$personData->load('physicalAddress');

$addressData = Address::factory()->type(AddressType::PHYSICAL)->make();
$formData = array_merge($personData->toArray(), ['physicalAddress' => $addressData->toArray()]);
if ($personData->height_inches !== null) {
$formData['feet'] = (int)floor($personData->height_inches / 12);
$formData['inches'] = $personData->height_inches % 12;
}

livewire(PersonResource\Pages\CreatePerson::class)
->fillForm($formData)
->call('create')
->assertHasNoFormErrors();

$this->assertDatabaseHas(Person::class, $personData->withoutRelations()->toArray());
$addressData->setAppends([]);
$this->assertDatabaseHas(Address::class, $addressData->toArray());
});
Here is a minimal version of the test that still has the ~20 second lag on the fillForm I had broken it out as I wasn't sure which of the calls in that group was causing it There is also probably a better way to handle the feet/inches but I store total inches in the db and the form provides a feet AND inches input box
awcodes
awcodes15mo ago
What is the difference in physicalAddress and the Address model.?
Shavik
ShavikOP15mo ago
Well there is an address model. I imported my addresses from another project there were attached to various things via morphs. I didn't want to use a relationship manager on this project for the addresses so I attached the addresses to the people via a belongsTo on the person model. So the person has a physicalAddress relation and a mailingAddress relation (both address models/table)
awcodes
awcodes15mo ago
Not saying it’s wrong. Just trying to understand the eager load that happens before the factory, if that makes sense.
Shavik
ShavikOP15mo ago
Ah I see what you're asking. I think that can be removed. I had left that in from before when the Person factory generated an address. I had stripped that out to see if that was causing the issue early on.
cheesegrits
cheesegrits15mo ago
Yeah, this is odd. The fillForm() method really doesn't do anything special, it just updates the underlying Livewire properties. If you use xdebug, you could step in to fillForm and see if there's a specific property that is causing the delay. If not, without starting to add debug code to the core, probably do it the old fashioned way. Remove everything from the schema, start with an empty schema, then add stuff back till it goes south. @shavik any luck with this?
Shavik
ShavikOP15mo ago
I got distracted with something else but I'd like to dust off xdebug later and see if I can figure it out. The huge delay is quite frustrating. Like waiting for CI/CD but locally!
cheesegrits
cheesegrits15mo ago
Yeah, it'd be reaally frustrating. Something weird going on, because it should take a handful of milliseconds.
Want results from more Discord servers?
Add your server