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

Improve performance of page tree #1088

Merged
merged 11 commits into from Jan 19, 2022
Merged

Conversation

timobrembeck
Copy link
Member

@timobrembeck timobrembeck commented Jan 5, 2022

Short description

Improve performance of page tree

Proposed changes

  • Replace django-mptt by django-treebeard
  • Improve performance of page tree
    • Prefetch page translations in page tree and page API
    • Prefetch language tree together with page tree
    • Use cached_property in Page models
    • Optimize calculation of translations states
    • Cache tree structure in loops

Resolved issues

This is part of #886 and #943 and leads to various performance improvements
Fixes: #642
Fixes: #748

@timobrembeck timobrembeck requested a review from a team as a code owner January 5, 2022 15:43
Copy link
Member

@svenseeberg svenseeberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In template integreat_cms/cms/templates/pages/page_tree_node.html, error at line 118

throws a 'NoneType' object has no attribute 'slug'

Full trace:
Jan 05 20:21:18 ERROR django.request - Internal Server Error: /nurnberg/pages/de/
Traceback (most recent call last):
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "integreat-cms-django/integreat_cms/cms/decorators.py", line 114, in wrap
    return function(request, *args, **kwargs)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "integreat-cms-django/integreat_cms/cms/views/pages/page_tree_view.py", line 148, in get
    return render(
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/shortcuts.py", line 19, in render
    content = loader.render_to_string(template_name, context, request, using=using)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/loader.py", line 62, in render_to_string
    return template.render(context, request)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/backends/django.py", line 61, in render
    return self.template.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 170, in render
    return self._render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/test/utils.py", line 100, in instrumented_test_render
    return self.nodelist.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/loader_tags.py", line 150, in render
    return compiled_parent._render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/test/utils.py", line 100, in instrumented_test_render
    return self.nodelist.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/loader_tags.py", line 150, in render
    return compiled_parent._render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/test/utils.py", line 100, in instrumented_test_render
    return self.nodelist.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/loader_tags.py", line 62, in render
    result = block.nodelist.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/loader_tags.py", line 62, in render
    result = block.nodelist.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/defaulttags.py", line 312, in render
    return nodelist.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django_mptt-0.13.4-py3.9.egg/mptt/templatetags/mptt_tags.py", line 313, in render
    bits = [self._render_node(context, node) for node in roots]
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django_mptt-0.13.4-py3.9.egg/mptt/templatetags/mptt_tags.py", line 313, in <listcomp>
    bits = [self._render_node(context, node) for node in roots]
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django_mptt-0.13.4-py3.9.egg/mptt/templatetags/mptt_tags.py", line 303, in _render_node
    bits.append(self._render_node(context, child))
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django_mptt-0.13.4-py3.9.egg/mptt/templatetags/mptt_tags.py", line 306, in _render_node
    rendered = self.template_nodes.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/loader_tags.py", line 195, in render
    return template.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 172, in render
    return self._render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/test/utils.py", line 100, in instrumented_test_render
    return self.nodelist.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/defaulttags.py", line 312, in render
    return nodelist.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 988, in render
    output = self.filter_expression.resolve(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 671, in resolve
    obj = self.var.resolve(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 796, in resolve
    value = self._resolve_lookup(context)
  File "integreat-cms-django/.venv/lib64/python3.9/site-packages/django/template/base.py", line 858, in _resolve_lookup
    current = current()
  File "integreat-cms-django/integreat_cms/cms/models/pages/abstract_base_page_translation.py", line 112, in get_absolute_url
    return "/" + self.permalink
  File "integreat-cms-django/integreat_cms/cms/models/pages/page_translation.py", line 100, in permalink
    self.ancestor_path,
  File "integreat-cms-django/integreat_cms/cms/models/pages/page_translation.py", line 78, in ancestor_path
    [
  File "integreat-cms-django/integreat_cms/cms/models/pages/page_translation.py", line 81, in <listcomp>
    else ancestor.default_translation.slug
AttributeError: 'NoneType' object has no attribute 'slug'

@timobrembeck timobrembeck marked this pull request as draft January 7, 2022 18:46
@timobrembeck timobrembeck force-pushed the refactoring/page-performance branch 2 times, most recently from fa07116 to cc5a3dc Compare January 13, 2022 03:54
@timobrembeck timobrembeck changed the title Prefetch page translations in page tree and page API Improve performance of page tree Jan 13, 2022
@timobrembeck timobrembeck marked this pull request as ready for review January 13, 2022 04:14
@timobrembeck timobrembeck requested a review from a team January 13, 2022 04:14
@timobrembeck
Copy link
Member Author

timobrembeck commented Jan 13, 2022

Well, this rabbit hole was deeper than I anticipated, and I'm still not completely happy with the current solution, but I think it's ready to be tested now. As usual, I'm sorry about the huge diff 😒

@timobrembeck timobrembeck force-pushed the refactoring/page-performance branch 2 times, most recently from 583f746 to cde167a Compare January 16, 2022 21:43
@timobrembeck
Copy link
Member Author

I did a little bit of benchmarking of the loading times in milliseconds on my local machine (so no valid absolute numbers, but maybe interesting for a qualitative comparison):

API pages endpoint:

# Pages develop develop + Redis refactoring refactoring + Redis
16 5740.4 290.6 212.8 48.2
128 45582.0 4995.0 332.1 84.5
512 326515.0 36276.0 872.5 246.5
1024 💣 💣 2051.3 409.4
2048 💣 💣 8696.5 1047.2

Page TreeView:

# Pages develop develop + Redis refactoring refactoring + Redis
16 19036.9 961.7 521.3 156.3
128 198160.0 14479.0 885.3 435.6
512 1010765.0 146369.0 2529.8 1482.5
1024 💣 💣 5351.7 2843.1
2048 💣 💣 12970.0 5784.9

You can reproduce similar results by using the new ./dev-tool/duplicate_pages.sh which does what the name suggests 😉
So with e.g. 5 iterations, you can go from the default set of 16 pages to 512 pages.

@svenseeberg
Copy link
Member

The numbers sound very promising :)

@svenseeberg
Copy link
Member

Does it make sense to only do a rough review of this PR and then merge it to develop and send all devs bug hunting on our test system?

@timobrembeck
Copy link
Member Author

Does it make sense to only do a rough review of this PR and then merge it to develop and send all devs bug hunting on our test system?

Probably, yes... there will definitely be a few bugs hidden in these lines, but I don't expect anyone to do a full code review on this...

@timobrembeck
Copy link
Member Author

Speaking of which, I found another small bug in the imprint form, but should be fixed now. Maybe it's a good time to write more automated tests to check form submissions etc...

Copy link
Contributor

@melegiul melegiul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this great job, loading of the page tree with the new test data looks much better now
I tested the drag and drop feature at the page tree and the language tree and I found an exception (IndexError: list index out of range - POST /augsburg/pages/de/23/move/22/last-child) after some first valid moves, which might be related to the new treebeard package

Traceback

Traceback (most recent call last):
  File "/home/melegiul/dev/integreat-cms/.venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/home/melegiul/dev/integreat-cms/.venv/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/melegiul/dev/integreat-cms/.venv/lib/python3.9/site-packages/django/views/decorators/http.py", line 40, in inner
    return func(request, *args, **kwargs)
  File "/home/melegiul/dev/integreat-cms/.venv/lib/python3.9/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/home/melegiul/dev/integreat-cms/integreat_cms/cms/decorators.py", line 112, in wrap
    return function(request, *args, **kwargs)
  File "/home/melegiul/dev/integreat-cms/.venv/lib/python3.9/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/home/melegiul/dev/integreat-cms/integreat_cms/cms/views/pages/page_actions.py", line 547, in move_page
    page.save()
  File "/home/melegiul/dev/integreat-cms/integreat_cms/cms/models/abstract_tree_node.py", line 106, in save
    self.parent = self.get_parent()
  File "/home/melegiul/dev/integreat-cms/.venv/lib/python3.9/site-packages/treebeard/ns_tree.py", line 661, in get_parent
    self._cached_parent_obj = self.get_ancestors().reverse()[0]
  File "/home/melegiul/dev/integreat-cms/.venv/lib/python3.9/site-packages/django/db/models/query.py", line 318, in __getitem__
    return qs._result_cache[0]

@timobrembeck
Copy link
Member Author

Thank you for this great job, loading of the page tree with the new test data looks much better now I tested the drag and drop feature at the page tree and the language tree and I found an exception (IndexError: list index out of range - POST /augsburg/pages/de/23/move/22/last-child) after some first valid moves, which might be related to the new treebeard package

@melegiul Thanks a lot for the report, I could reproduce the issue locally and hopefully fix it. Could you test again?

Copy link
Contributor

@melegiul melegiul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot, drag n drop is working as expexcted again for pages and the language tree
But I found a problem with PDF export feature

integreat_cms/cms/templates/pages/page_pdf.html Outdated Show resolved Hide resolved
Copy link
Contributor

@melegiul melegiul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, now the PDF export works again, thank you
I found one more bug about page filtering, the filter result do only contain root level pages
AFAICT this bug is on the develop branch already, so I will open a new ticket for that

@timobrembeck timobrembeck force-pushed the refactoring/page-performance branch 2 times, most recently from 6bf7649 to 1d7ee93 Compare January 18, 2022 17:34
For two reasons:
    1. django-mptt is unmaintained
    2. django-mptt doesn't allow prefetching translations in page tree
- Prefetch page translations in content lists and API
- Prefetch language tree together with region
- Use cached_property in content models
Pass the get-parameter "debug" to any JSON view to get the result in HTML including the debug toolbar
- Add current region to request object in custom middleware
- Remove context processor which added the region to the template context

This prevents duplicated region queries in views and templates
- Add initial migration file
- Unignore migrations directory
- Add CircleCI check whether migrations are missing
- Add more pages to the test data fixture
- Add dev tool to generate more test data locally
- Reflect changes in the expected outputs of the API tests
@timobrembeck timobrembeck merged commit 89ed88a into develop Jan 19, 2022
@timobrembeck timobrembeck deleted the refactoring/page-performance branch January 19, 2022 16:15
@timobrembeck timobrembeck mentioned this pull request Feb 2, 2022
16 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Define roles in migration instead of fixture Commit v1 of database schema
3 participants