Skip to content
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

Merged

Conversation

shangxiao
Copy link
Contributor

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)
Copy link
Contributor Author

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.

Copy link
Member

@charettes charettes Oct 5, 2023

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

  1. Avoid eager model rendering for the last migration that needs to be unapplied
  2. Make state_forwards and state cloning within Migration.unapply for each operation really cheap for this particular migration
  3. Cause O=len(last_migration.operations) model rendering from scratch for each database_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?

Copy link
Contributor Author

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.

@shangxiao
Copy link
Contributor Author

@jarshwah, hi. Interested in checking whether this patch speeds up reverse migrations for you?

@shangxiao shangxiao force-pushed the ticket_34841_reverse_migration_optimisation branch from 1a576e8 to cbf760e Compare October 5, 2023 12:54
@@ -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
Copy link
Contributor Author

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? 🤔

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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 ...

Copy link
Contributor Author

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 😅

@felixxm
Copy link
Member

felixxm commented Nov 20, 2023

@shangxiao Do you have time to keep working on this?

@shangxiao
Copy link
Contributor Author

@shangxiao Do you have time to keep working on this?

Yep 👍

@felixxm
Copy link
Member

felixxm commented Mar 22, 2024

@charettes Do you think this is an acceptable improvement?

@felixxm felixxm self-assigned this Mar 22, 2024
@@ -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.
Copy link
Contributor Author

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.

@felixxm felixxm force-pushed the ticket_34841_reverse_migration_optimisation branch from 026f01a to b85ab16 Compare March 22, 2024 20:07
Copy link
Member

@felixxm felixxm left a 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.

@charettes
Copy link
Member

charettes commented Mar 22, 2024

@felixxm no objections from me, this should only cause a slowdown on custom migrations.Operation subclasses out there that use state.apps in their state_forwards as none of the core ones do anymore so I think this is a good tradeoff.

@felixxm felixxm force-pushed the ticket_34841_reverse_migration_optimisation branch from b85ab16 to b6e2b83 Compare March 22, 2024 20:30
@felixxm felixxm merged commit b6e2b83 into django:main Mar 22, 2024
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants