Commit c930e17f by Nimisha Asthagiri

Merge pull request #4534 from edx/nimisha/import-perf-unit-test-STUD-1994

Unit test for import performance STUD-1994
parents f6b278e7 cd33165a
...@@ -200,10 +200,10 @@ class TestCourseListing(ModuleStoreTestCase): ...@@ -200,10 +200,10 @@ class TestCourseListing(ModuleStoreTestCase):
# Now count the db queries # Now count the db queries
store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
with check_mongo_calls(store.collection, USER_COURSES_COUNT): with check_mongo_calls(store, USER_COURSES_COUNT):
courses_list = _accessible_courses_list_from_groups(self.request) courses_list = _accessible_courses_list_from_groups(self.request)
with check_mongo_calls(store.collection, 1): with check_mongo_calls(store, 1):
courses_list = _accessible_courses_list(self.request) courses_list = _accessible_courses_list(self.request)
def test_get_course_list_with_same_course_id(self): def test_get_course_list_with_same_course_id(self):
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# pylint: disable=E1101 # pylint: disable=E1101
# pylint: disable=protected-access
""" """
Tests for import_from_xml using the mongo modulestore. Tests for import_from_xml using the mongo modulestore.
""" """
...@@ -10,8 +11,10 @@ from django.conf import settings ...@@ -10,8 +11,10 @@ from django.conf import settings
import copy import copy
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore.tests.factories import check_exact_number_of_calls, check_number_of_calls
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
...@@ -149,6 +152,21 @@ class ContentStoreImportTest(ModuleStoreTestCase): ...@@ -149,6 +152,21 @@ class ContentStoreImportTest(ModuleStoreTestCase):
print "course tabs = {0}".format(course.tabs) print "course tabs = {0}".format(course.tabs)
self.assertEqual(course.tabs[2]['name'], 'Syllabus') self.assertEqual(course.tabs[2]['name'], 'Syllabus')
def test_import_performance_mongo(self):
store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
# we try to refresh the inheritance tree for each update_item in the import
with check_exact_number_of_calls(store, store.refresh_cached_metadata_inheritance_tree, 46):
# the post-publish step loads each item in the subtree, which calls _get_cached_metadata_inheritance_tree
with check_exact_number_of_calls(store, store._get_cached_metadata_inheritance_tree, 22):
# with bulk-edit in progress, the inheritance tree should be recomputed only at the end of the import
# NOTE: On Jenkins, with memcache enabled, the number of calls here is only 1.
# Locally, without memcache, the number of calls is actually 2 (once more during the publish step)
with check_number_of_calls(store, store._compute_metadata_inheritance_tree, 2):
self.load_test_import_course()
def test_rewrite_reference_list(self): def test_rewrite_reference_list(self):
module_store = modulestore() module_store = modulestore()
target_course_id = SlashSeparatedCourseKey('testX', 'conditional_copy', 'copy_run') target_course_id = SlashSeparatedCourseKey('testX', 'conditional_copy', 'copy_run')
......
...@@ -9,7 +9,7 @@ from xblock.core import XBlock ...@@ -9,7 +9,7 @@ from xblock.core import XBlock
from xmodule.tabs import StaticTab from xmodule.tabs import StaticTab
from decorator import contextmanager from decorator import contextmanager
from mock import Mock, patch from mock import Mock, patch
from nose.tools import assert_less_equal from nose.tools import assert_less_equal, assert_greater_equal
class Dummy(object): class Dummy(object):
...@@ -220,30 +220,57 @@ class ItemFactory(XModuleFactory): ...@@ -220,30 +220,57 @@ class ItemFactory(XModuleFactory):
@contextmanager @contextmanager
def check_mongo_calls(mongo_store, max_finds=0, max_sends=None): def check_exact_number_of_calls(object_with_method, method, num_calls, method_name=None):
""" """
Instruments the given store to count the number of calls to find (incl find_one) and the number Instruments the given method on the given object to verify the number of calls to the
of calls to send_message which is for insert, update, and remove (if you provide max_sends). At the method is exactly equal to 'num_calls'.
end of the with statement, it compares the counts to the max_finds and max_sends using a simple """
assertLessEqual. with check_number_of_calls(object_with_method, method, num_calls, num_calls, method_name):
yield
:param mongo_store: the MongoModulestore or subclass to watch
:param max_finds: the maximum number of find calls to allow @contextmanager
:param max_sends: If none, don't instrument the send calls. If non-none, count and compare to def check_number_of_calls(object_with_method, method, maximum_calls, minimum_calls=1, method_name=None):
the given int value. """
Instruments the given method on the given object to verify the number of calls to the method is
less than or equal to the expected maximum_calls and greater than or equal to the expected minimum_calls.
""" """
method_wrap = Mock(wraps=method)
wrap_patch = patch.object(object_with_method, method_name or method.__name__, method_wrap)
try: try:
find_wrap = Mock(wraps=mongo_store.collection.find)
wrap_patch = patch.object(mongo_store.collection, 'find', find_wrap)
wrap_patch.start() wrap_patch.start()
if max_sends:
sends_wrap = Mock(wraps=mongo_store.database.connection._send_message)
sends_patch = patch.object(mongo_store.database.connection, '_send_message', sends_wrap)
sends_patch.start()
yield yield
finally: finally:
wrap_patch.stop() wrap_patch.stop()
if max_sends:
sends_patch.stop() # verify the counter actually worked by ensuring we have counted greater than (or equal to) the minimum calls
assert_less_equal(sends_wrap.call_count, max_sends) assert_greater_equal(method_wrap.call_count, minimum_calls)
assert_less_equal(find_wrap.call_count, max_finds)
# now verify the number of actual calls is less than (or equal to) the expected maximum
assert_less_equal(method_wrap.call_count, maximum_calls)
@contextmanager
def check_mongo_calls(mongo_store, num_finds=0, num_sends=None):
"""
Instruments the given store to count the number of calls to find (incl find_one) and the number
of calls to send_message which is for insert, update, and remove (if you provide num_sends). At the
end of the with statement, it compares the counts to the num_finds and num_sends.
:param mongo_store: the MongoModulestore or subclass to watch
:param num_finds: the exact number of find calls expected
:param num_sends: If none, don't instrument the send calls. If non-none, count and compare to
the given int value.
"""
with check_exact_number_of_calls(mongo_store.collection, mongo_store.collection.find, num_finds):
if num_sends:
with check_exact_number_of_calls(
mongo_store.database.connection,
mongo_store.database.connection._send_message, # pylint: disable=protected-access
num_sends,
):
yield
else:
yield
...@@ -19,23 +19,23 @@ class TestPublish(SplitWMongoCourseBoostrapper): ...@@ -19,23 +19,23 @@ class TestPublish(SplitWMongoCourseBoostrapper):
# There are 12 created items and 7 parent updates # There are 12 created items and 7 parent updates
# create course: finds: 1 to verify uniqueness, 1 to find parents # create course: finds: 1 to verify uniqueness, 1 to find parents
# sends: 1 to create course, 1 to create overview # sends: 1 to create course, 1 to create overview
with check_mongo_calls(self.draft_mongo, 6, 2): with check_mongo_calls(self.draft_mongo, 5, 2):
super(TestPublish, self)._create_course(split=False) # 2 inserts (course and overview) super(TestPublish, self)._create_course(split=False) # 2 inserts (course and overview)
# with bulk will delay all inheritance computations which won't be added into the mongo_calls # with bulk will delay all inheritance computations which won't be added into the mongo_calls
with self.draft_mongo.bulk_write_operations(self.old_course_key): with self.draft_mongo.bulk_write_operations(self.old_course_key):
# finds: 1 for parent to add child, 1 for parent to update edit info # finds: 1 for parent to add child
# sends: 1 for insert, 2 for parent (add child, change edit info) # sends: 1 for insert, 1 for parent (add child)
with check_mongo_calls(self.draft_mongo, 5, 3): with check_mongo_calls(self.draft_mongo, 1, 2):
self._create_item('chapter', 'Chapter1', {}, {'display_name': 'Chapter 1'}, 'course', 'runid', split=False) self._create_item('chapter', 'Chapter1', {}, {'display_name': 'Chapter 1'}, 'course', 'runid', split=False)
with check_mongo_calls(self.draft_mongo, 5, 3): with check_mongo_calls(self.draft_mongo, 2, 2):
self._create_item('chapter', 'Chapter2', {}, {'display_name': 'Chapter 2'}, 'course', 'runid', split=False) self._create_item('chapter', 'Chapter2', {}, {'display_name': 'Chapter 2'}, 'course', 'runid', split=False)
# update info propagation is 2 levels. create looks for draft and then published and then creates # update info propagation is 2 levels. create looks for draft and then published and then creates
with check_mongo_calls(self.draft_mongo, 16, 8): with check_mongo_calls(self.draft_mongo, 8, 6):
self._create_item('vertical', 'Vert1', {}, {'display_name': 'Vertical 1'}, 'chapter', 'Chapter1', split=False) self._create_item('vertical', 'Vert1', {}, {'display_name': 'Vertical 1'}, 'chapter', 'Chapter1', split=False)
self._create_item('vertical', 'Vert2', {}, {'display_name': 'Vertical 2'}, 'chapter', 'Chapter1', split=False) self._create_item('vertical', 'Vert2', {}, {'display_name': 'Vertical 2'}, 'chapter', 'Chapter1', split=False)
with check_mongo_calls(self.draft_mongo, 36, 36): with check_mongo_calls(self.draft_mongo, 20, 12):
self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1', split=False) self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1', split=False)
self._create_item( self._create_item(
'discussion', 'Discussion1', 'discussion', 'Discussion1',
...@@ -62,7 +62,8 @@ class TestPublish(SplitWMongoCourseBoostrapper): ...@@ -62,7 +62,8 @@ class TestPublish(SplitWMongoCourseBoostrapper):
'vertical', 'Vert2', 'vertical', 'Vert2',
split=False split=False
) )
with check_mongo_calls(self.draft_mongo, 2, 2):
with check_mongo_calls(self.draft_mongo, 0, 2):
# 2 finds b/c looking for non-existent parents # 2 finds b/c looking for non-existent parents
self._create_item('static_tab', 'staticuno', "<p>tab</p>", {'display_name': 'Tab uno'}, None, None, split=False) self._create_item('static_tab', 'staticuno', "<p>tab</p>", {'display_name': 'Tab uno'}, None, None, split=False)
self._create_item('course_info', 'updates', "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None, split=False) self._create_item('course_info', 'updates', "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None, split=False)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment