Several optimizations for improving vertical rendering performance.
This commit is contained in:
@@ -4,8 +4,12 @@ API methods related to xblock state.
|
||||
|
||||
|
||||
from xblock_django.models import XBlockConfiguration, XBlockStudioConfiguration
|
||||
from openedx.core.lib.cache_utils import CacheInvalidationManager
|
||||
|
||||
cacher = CacheInvalidationManager(model=XBlockConfiguration)
|
||||
|
||||
|
||||
@cacher
|
||||
def deprecated_xblocks():
|
||||
"""
|
||||
Return the QuerySet of deprecated XBlock types. Note that this method is independent of
|
||||
@@ -14,6 +18,7 @@ def deprecated_xblocks():
|
||||
return XBlockConfiguration.objects.current_set().filter(deprecated=True)
|
||||
|
||||
|
||||
@cacher
|
||||
def disabled_xblocks():
|
||||
"""
|
||||
Return the QuerySet of disabled XBlock types (which should not render in the LMS).
|
||||
|
||||
@@ -6,10 +6,8 @@ Models.
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class XBlockConfiguration(ConfigurationModel):
|
||||
"""
|
||||
XBlock configuration used by both LMS and Studio, and not specific to a particular template.
|
||||
@@ -35,7 +33,6 @@ class XBlockConfiguration(ConfigurationModel):
|
||||
).format(self.name, self.enabled, self.deprecated)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class XBlockStudioConfigurationFlag(ConfigurationModel):
|
||||
"""
|
||||
Enables site-wide Studio configuration for XBlocks.
|
||||
@@ -52,7 +49,6 @@ class XBlockStudioConfigurationFlag(ConfigurationModel):
|
||||
return "XBlockStudioConfigurationFlag(enabled={})".format(self.enabled)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class XBlockStudioConfiguration(ConfigurationModel):
|
||||
"""
|
||||
Studio editing configuration for a specific XBlock/template combination.
|
||||
|
||||
@@ -260,8 +260,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
|
||||
NUM_PROBLEMS = 20
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 10, 176),
|
||||
(ModuleStoreEnum.Type.split, 4, 174),
|
||||
(ModuleStoreEnum.Type.mongo, 10, 172),
|
||||
(ModuleStoreEnum.Type.split, 4, 170),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
|
||||
@@ -1492,8 +1492,8 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
self.assertContains(resp, u"Download Your Certificate")
|
||||
|
||||
@ddt.data(
|
||||
(True, 54),
|
||||
(False, 53)
|
||||
(True, 53),
|
||||
(False, 52)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_progress_queries_paced_courses(self, self_paced, query_count):
|
||||
@@ -1506,8 +1506,8 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
|
||||
@ddt.data(
|
||||
(False, 62, 41),
|
||||
(True, 53, 36)
|
||||
(False, 61, 40),
|
||||
(True, 52, 35)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_progress_queries(self, enable_waffle, initial, subsequent):
|
||||
|
||||
@@ -35,6 +35,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_GET, require_http_methods, require_POST
|
||||
from django.views.generic import View
|
||||
from edx_django_utils.monitoring import set_custom_metrics_for_course_key
|
||||
from edxnotes.helpers import is_feature_enabled
|
||||
from ipware.ip import get_ip
|
||||
from markupsafe import escape
|
||||
from opaque_keys import InvalidKeyError
|
||||
@@ -1607,6 +1608,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
|
||||
'disable_footer': True,
|
||||
'disable_window_wrap': True,
|
||||
'enable_completion_on_view_service': enable_completion_on_view_service,
|
||||
'edx_notes_enabled': is_feature_enabled(course, request.user),
|
||||
'staff_access': bool(request.user.has_perm(VIEW_XQA_INTERFACE, course)),
|
||||
'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
|
||||
}
|
||||
|
||||
@@ -100,21 +100,21 @@ class TestCourseGradeFactory(GradeTestBase):
|
||||
with self.assertNumQueries(3), mock_get_score(1, 2):
|
||||
_assert_read(expected_pass=False, expected_percent=0) # start off with grade of 0
|
||||
|
||||
num_queries = 45
|
||||
num_queries = 44
|
||||
with self.assertNumQueries(num_queries), mock_get_score(1, 2):
|
||||
grade_factory.update(self.request.user, self.course, force_update_subsections=True)
|
||||
|
||||
with self.assertNumQueries(3):
|
||||
_assert_read(expected_pass=True, expected_percent=0.5) # updated to grade of .5
|
||||
|
||||
num_queries = 7
|
||||
num_queries = 6
|
||||
with self.assertNumQueries(num_queries), mock_get_score(1, 4):
|
||||
grade_factory.update(self.request.user, self.course, force_update_subsections=False)
|
||||
|
||||
with self.assertNumQueries(3):
|
||||
_assert_read(expected_pass=True, expected_percent=0.5) # NOT updated to grade of .25
|
||||
|
||||
num_queries = 24
|
||||
num_queries = 23
|
||||
with self.assertNumQueries(num_queries), mock_get_score(2, 2):
|
||||
grade_factory.update(self.request.user, self.course, force_update_subsections=True)
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
|
||||
|
||||
RequestCache.clear_all_namespaces()
|
||||
|
||||
expected_query_count = 50
|
||||
expected_query_count = 45
|
||||
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
|
||||
with check_mongo_calls(mongo_count):
|
||||
with self.assertNumQueries(expected_query_count):
|
||||
@@ -2260,7 +2260,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
'failed': 3,
|
||||
'skipped': 2
|
||||
}
|
||||
with self.assertNumQueries(146):
|
||||
with self.assertNumQueries(141):
|
||||
self.assertCertificatesGenerated(task_input, expected_results)
|
||||
|
||||
expected_results = {
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled
|
||||
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
|
||||
%>
|
||||
<%def name="course_name()">
|
||||
<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %>
|
||||
@@ -36,7 +36,7 @@ ${static.get_page_title_breadcrumbs(course_name())}
|
||||
<%static:css group='style-course-vendor'/>
|
||||
<%static:css group='style-course'/>
|
||||
## Utility: Notes
|
||||
% if is_edxnotes_enabled(course, request.user):
|
||||
% if edx_notes_enabled:
|
||||
<%static:css group='style-student-notes'/>
|
||||
% endif
|
||||
|
||||
@@ -92,10 +92,10 @@ ${HTML(fragment.foot_html())}
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
% if course.show_calculator or is_edxnotes_enabled(course, request.user):
|
||||
% if course.show_calculator or edx_notes_enabled:
|
||||
<nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}" aria-label="${_('Course Utilities')}">
|
||||
## Utility: Notes
|
||||
% if is_edxnotes_enabled(course, request.user):
|
||||
% if edx_notes_enabled:
|
||||
<%include file="/edxnotes/toggle_notes.html" args="course=course"/>
|
||||
% endif
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ def get_bookmarks(user, course_key=None, fields=None, serialized=True):
|
||||
else:
|
||||
bookmarks_queryset = bookmarks_queryset.select_related('user')
|
||||
|
||||
bookmarks_queryset = bookmarks_queryset.order_by('-created')
|
||||
bookmarks_queryset = bookmarks_queryset.order_by('-id')
|
||||
else:
|
||||
bookmarks_queryset = Bookmark.objects.none()
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.conf import settings
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.db.models.fields import BooleanField, DateTimeField, DecimalField, FloatField, IntegerField, TextField
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.db.utils import IntegrityError
|
||||
from django.template import defaultfilters
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
@@ -27,6 +28,7 @@ from lms.djangoapps.discussion import django_comment_client
|
||||
from openedx.core.djangoapps.catalog.models import CatalogIntegration
|
||||
from openedx.core.djangoapps.lang_pref.api import get_closest_released_language
|
||||
from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||
from openedx.core.lib.cache_utils import request_cached, RequestCache
|
||||
from static_replace.models import AssetBaseUrlConfig
|
||||
from xmodule import block_metadata_utils, course_metadata_utils
|
||||
from xmodule.course_module import DEFAULT_START_DATE, CourseDescriptor
|
||||
@@ -319,6 +321,7 @@ class CourseOverview(TimeStampedModel):
|
||||
return modulestore().has_course(course_id)
|
||||
|
||||
@classmethod
|
||||
@request_cached('course_overview')
|
||||
def get_from_id(cls, course_id):
|
||||
"""
|
||||
Load a CourseOverview object for a given course ID.
|
||||
@@ -1024,3 +1027,16 @@ class SimulateCoursePublishConfig(ConfigurationModel):
|
||||
|
||||
def __str__(self):
|
||||
return six.text_type(self.arguments)
|
||||
|
||||
|
||||
def _invalidate_overview_cache(**kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Invalidate the course overview request cache.
|
||||
"""
|
||||
RequestCache('course_overview').clear()
|
||||
|
||||
|
||||
post_save.connect(_invalidate_overview_cache, sender=CourseOverview)
|
||||
post_save.connect(_invalidate_overview_cache, sender=CourseOverviewImageConfig)
|
||||
post_delete.connect(_invalidate_overview_cache, sender=CourseOverview)
|
||||
post_delete.connect(_invalidate_overview_cache, sender=CourseOverviewImageConfig)
|
||||
|
||||
@@ -22,6 +22,7 @@ from opaque_keys.edx.django.models import CourseKeyField
|
||||
# create an alias in "user_api".
|
||||
|
||||
from openedx.core.djangolib.model_mixins import DeletableByUserValue
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
# pylint: disable=unused-import
|
||||
from student.models import (
|
||||
PendingEmailChange,
|
||||
@@ -54,6 +55,7 @@ class UserPreference(models.Model):
|
||||
unique_together = ("user", "key")
|
||||
|
||||
@staticmethod
|
||||
@request_cached()
|
||||
def get_all_preferences(user):
|
||||
"""
|
||||
Gets all preferences for a given user
|
||||
|
||||
@@ -10,8 +10,10 @@ import zlib
|
||||
|
||||
import six
|
||||
import wrapt
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.utils.encoding import force_text
|
||||
from edx_django_utils.cache import RequestCache
|
||||
|
||||
from edx_django_utils.cache import RequestCache, TieredCache
|
||||
from six import iteritems
|
||||
from six.moves import cPickle as pickle
|
||||
from six.moves import map
|
||||
@@ -151,6 +153,57 @@ class process_cached(object): # pylint: disable=invalid-name
|
||||
return functools.partial(self.__call__, obj)
|
||||
|
||||
|
||||
class CacheInvalidationManager:
|
||||
"""
|
||||
This class provides a decorator for simple functions, which can handle invalidation.
|
||||
|
||||
To use, instantiate with a namespace or django model class:
|
||||
`manager = CacheInvalidationManager(model=User)`
|
||||
|
||||
Then use it as a decorator on functions with no arguments
|
||||
`@manager
|
||||
def get_system_user():
|
||||
...
|
||||
`
|
||||
When the User model is saved or deleted, all cache keys used by
|
||||
the decorator will be cleared.
|
||||
"""
|
||||
|
||||
def __init__(self, namespace=None, model=None, cache_time=86400):
|
||||
if model:
|
||||
post_save.connect(self.invalidate, sender=model)
|
||||
post_delete.connect(self.invalidate, sender=model)
|
||||
namespace = str(model)
|
||||
self.namespace = namespace
|
||||
self.cache_time = cache_time
|
||||
self.keys = set()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def invalidate(self, **kwargs):
|
||||
"""
|
||||
Invalidate all keys tracked by the manager.
|
||||
"""
|
||||
for key in self.keys:
|
||||
TieredCache.delete_all_tiers(key)
|
||||
|
||||
def __call__(self, func):
|
||||
"""
|
||||
Decorator for functions with no arguments.
|
||||
"""
|
||||
cache_key = '{}.{}.{}'.format(self.namespace, func.__module__, func.__name__)
|
||||
self.keys.add(cache_key)
|
||||
|
||||
@functools.wraps(func)
|
||||
def decorator(*args, **kwargs): # pylint: disable=unused-argument,missing-docstring
|
||||
result = TieredCache.get_cached_response(cache_key)
|
||||
if result.is_found:
|
||||
return result.value
|
||||
result = func()
|
||||
TieredCache.set_all_tiers(cache_key, result, self.cache_time)
|
||||
return result
|
||||
return decorator
|
||||
|
||||
|
||||
def zpickle(data):
|
||||
"""Given any data structure, returns a zlib compressed pickled serialization."""
|
||||
return zlib.compress(pickle.dumps(data, 4)) # Keep this constant as we upgrade from python 2 to 3.
|
||||
|
||||
@@ -218,7 +218,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
|
||||
|
||||
# Fetch the view and verify the query counts
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
with self.assertNumQueries(74, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(73, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_home_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
@@ -134,7 +134,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
|
||||
|
||||
# Fetch the view and verify that the query counts haven't changed
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
with self.assertNumQueries(51, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(50, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_updates_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
Reference in New Issue
Block a user