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
Fixed #34841 -- Avoided rendering apps on state still requiring mutation #17266
Fixed #34841 -- Avoided rendering apps on state still requiring mutation #17266
Conversation
django/db/migrations/executor.py
Outdated
if "apps" not in state.__dict__: | ||
state.apps # Render all -- performance critical | ||
# The state before this migration | ||
states[migration] = state | ||
# The old state keeps as-is, we continue with the new state | ||
state = migration.mutate_state(state, preserve=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may no longer need to set preserve=True
here since state is cloned already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is adequate, but my migration understanding is rusty.
For migrations that are planed to be unapplied (migration in migrations_to_run
) we do want their rendered state to be used below when calling self.unapply_migration
as it will inevitably result in database_forwards
being called for each operation and the latter still relies on .state
to perform schema changes (see ticket-29898).
Is the speedup you're experiencing due to the fact that the state_forwards
calls performed by Migration.unapply
for each of its operations are now cheaper because the apps are not invalidated?
From what I understand the changes you are proposing do
- Avoid eager model rendering for the last migration that needs to be unapplied
- Make
state_forwards
and state cloning withinMigration.unapply
for each operation really cheap for this particular migration - Cause
O=len(last_migration.operations)
model rendering from scratch for eachdatabase_backwards
call
I think this change might make unapplying a single migration with a single operation faster at the expense of slowing down other scenarios. Does that add up with your testing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@charettes Ah that explains why we render apps on state 👍
The slowdown was only caused by the block that mutates solely for post-migration state:
# Generate the post migration state by starting from the state before
# the last migration is unapplied and mutating it to include all the
# remaining applied migrations.
last_unapplied_migration = plan[-1][0]
state = states[last_unapplied_migration]
del state.apps
for index, (migration, _) in enumerate(full_plan):
if migration == last_unapplied_migration:
for migration, _ in full_plan[index:]:
if migration in applied_migrations:
migration.mutate_state(state, preserve=False)
break
As you pointed out, we should indeed leave it rendered for this:
for migration, _ in plan:
self.unapply_migration(states[migration], migration, fake=fake)
applied_migrations.remove(migration)
(I confirmed this by when testing reversing to zero
: my patch showed a significant slow down as it's my largest migration.)
So rethinking this, the changes should be isolated to that post-migration mutating block. I think this is more appropriate:
--- a/django/db/migrations/executor.py
+++ b/django/db/migrations/executor.py
@@ -224,6 +224,7 @@ class MigrationExecutor:
# remaining applied migrations.
last_unapplied_migration = plan[-1][0]
state = states[last_unapplied_migration]
+ del state.apps
for index, (migration, _) in enumerate(full_plan):
if migration == last_unapplied_migration:
for migration, _ in full_plan[index:]:
^ testing that change fixes the issue of reversing the last migration and doesn't affect reversing to zero
.
@jarshwah, hi. Interested in checking whether this patch speeds up reverse migrations for you? |
1a576e8
to
cbf760e
Compare
@@ -224,6 +224,7 @@ def _migrate_all_backwards(self, plan, full_plan, fake): | |||
# remaining applied migrations. | |||
last_unapplied_migration = plan[-1][0] | |||
state = states[last_unapplied_migration] | |||
del state.apps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This speeds up mutation - but do we need to re-render state.apps
after mutation is finished for the benefit of any post-migrate signal? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, there's no way around complete model rendering here due to post_migrate
eager rendering of models
I could see how disposing of rendered models like you did you help here though. What about using state.bulk_update()
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming you meant state.apps.bulk_update()
? Unfortunately still 20s to unapply only the final migration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Results of del state.apps
on my project:
|-------------------------+--------+----------------|
| | Normal | del state.apps |
|-------------------------+--------+----------------|
| Unapply final migration | 20.5s | 3.7s |
| Unapply all migrations | 177.8s | 175.08s |
|-------------------------+--------+----------------|
FYI this project is making use of django-pghistory so there's constant removal & re-addition of triggers used to track changes on tables. This may be affecting things: not only because of db operations but it also stays in the " Rendering model states..." state for quite a while.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a comment to explain why this is helpful?
# Delete apps to avoid ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep I have to go back and remember why that was myself 😅
@shangxiao Do you have time to keep working on this? |
Yep 👍 |
@charettes Do you think this is an acceptable improvement? |
django/db/migrations/executor.py
Outdated
@@ -224,12 +224,15 @@ def _migrate_all_backwards(self, plan, full_plan, fake): | |||
# remaining applied migrations. | |||
last_unapplied_migration = plan[-1][0] | |||
state = states[last_unapplied_migration] | |||
# Avoid mutating state with apps rendered as it's an expensive operation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want a better comment I'd need to dig into the state mutation to remember what specifically is expensive.
026f01a
to
b85ab16
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shangxiao Thanks 👍
I think it's worth trying.
@felixxm no objections from me, this should only cause a slowdown on custom |
b85ab16
to
b6e2b83
Compare
https://code.djangoproject.com/ticket/34841