Commit f0f7a538 by Nimisha Asthagiri

Optimize Subsection computation

parent 6aaf442d
...@@ -28,6 +28,7 @@ def get_course_blocks( ...@@ -28,6 +28,7 @@ def get_course_blocks(
user, user,
starting_block_usage_key, starting_block_usage_key,
transformers=None, transformers=None,
collected_block_structure=None,
): ):
""" """
A higher order function implemented on top of the A higher order function implemented on top of the
...@@ -45,6 +46,11 @@ def get_course_blocks( ...@@ -45,6 +46,11 @@ def get_course_blocks(
transformers whose transform methods are to be called. transformers whose transform methods are to be called.
If None, COURSE_BLOCK_ACCESS_TRANSFORMERS is used. If None, COURSE_BLOCK_ACCESS_TRANSFORMERS is used.
collected_block_structure (BlockStructureBlockData) - A
block structure retrieved from a prior call to
BlockStructureManager.get_collected. Can be optionally
provided if already available, for optimization.
Returns: Returns:
BlockStructureBlockData - A transformed block structure, BlockStructureBlockData - A transformed block structure,
starting at starting_block_usage_key, that has undergone the starting at starting_block_usage_key, that has undergone the
...@@ -61,4 +67,5 @@ def get_course_blocks( ...@@ -61,4 +67,5 @@ def get_course_blocks(
return get_block_structure_manager(starting_block_usage_key.course_key).get_transformed( return get_block_structure_manager(starting_block_usage_key.course_key).get_transformed(
transformers, transformers,
starting_block_usage_key, starting_block_usage_key,
collected_block_structure,
) )
...@@ -125,7 +125,8 @@ class CourseGrade(object): ...@@ -125,7 +125,8 @@ class CourseGrade(object):
subsection_grades.append( subsection_grades.append(
subsection_grade_factory.create( subsection_grade_factory.create(
self.course_structure[subsection_key], self.course_structure[subsection_key],
self.course_structure, self.course self.course_structure,
self.course,
) )
) )
......
...@@ -4,9 +4,6 @@ SubsectionGrade Class ...@@ -4,9 +4,6 @@ SubsectionGrade Class
from collections import OrderedDict from collections import OrderedDict
from lazy import lazy from lazy import lazy
from django.conf import settings
from course_blocks.api import get_course_blocks
from courseware.model_data import ScoresClient from courseware.model_data import ScoresClient
from lms.djangoapps.grades.scores import get_score, possibly_scored from lms.djangoapps.grades.scores import get_score, possibly_scored
from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade
...@@ -173,20 +170,17 @@ class SubsectionGradeFactory(object): ...@@ -173,20 +170,17 @@ class SubsectionGradeFactory(object):
self._compute_and_save_grade(subsection, course_structure, course) self._compute_and_save_grade(subsection, course_structure, course)
) )
def update(self, usage_key, course_key): def update(self, usage_key, course_structure, course):
""" """
Updates the SubsectionGrade object for the student and subsection Updates the SubsectionGrade object for the student and subsection
identified by the given usage key. identified by the given usage key.
""" """
from courseware.courses import get_course_by_id # avoids circular import with courseware.py
course = get_course_by_id(course_key, depth=0)
# save ourselves the extra queries if the course does not use subsection grades # save ourselves the extra queries if the course does not use subsection grades
if not PersistentGradesEnabledFlag.feature_enabled(course.id): if not PersistentGradesEnabledFlag.feature_enabled(course.id):
return return
course_structure = get_course_blocks(self.student, usage_key)
subsection = course_structure[usage_key]
self._prefetch_scores(course_structure, course) self._prefetch_scores(course_structure, course)
subsection = course_structure[usage_key]
return self._compute_and_save_grade(subsection, course_structure, course) return self._compute_and_save_grade(subsection, course_structure, course)
def _compute_and_save_grade(self, subsection, course_structure, course): def _compute_and_save_grade(self, subsection, course_structure, course):
......
...@@ -5,6 +5,8 @@ from logging import getLogger ...@@ -5,6 +5,8 @@ from logging import getLogger
from django.dispatch import receiver from django.dispatch import receiver
from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.courseware.courses import get_course_by_id
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache
...@@ -95,33 +97,25 @@ def recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=u ...@@ -95,33 +97,25 @@ def recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=u
- course_id: Unicode string representing the course - course_id: Unicode string representing the course
- usage_id: Unicode string indicating the courseware instance - usage_id: Unicode string indicating the courseware instance
""" """
try: student = kwargs['user']
course_id = kwargs['course_id'] course_key = CourseLocator.from_string(kwargs['course_id'])
usage_id = kwargs['usage_id']
student = kwargs['user']
except KeyError:
log.exception(
u"Failed to process SCORE_CHANGED signal, some arguments were missing."
"user: %s, course_id: %s, usage_id: %s.",
kwargs.get('user', None),
kwargs.get('course_id', None),
kwargs.get('usage_id', None),
)
return
course_key = CourseLocator.from_string(course_id)
if not PersistentGradesEnabledFlag.feature_enabled(course_key): if not PersistentGradesEnabledFlag.feature_enabled(course_key):
return return
usage_key = UsageKey.from_string(usage_id).replace(course_key=course_key) scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key)
block_structure = get_course_in_cache(course_key) collected_block_structure = get_course_in_cache(course_key)
course = get_course_by_id(course_key, depth=0)
subsections_to_update = block_structure.get_transformer_block_field( subsections_to_update = collected_block_structure.get_transformer_block_field(
usage_key, scored_block_usage_key,
GradesTransformer, GradesTransformer,
'subsections', 'subsections',
set() set()
) )
for subsection_usage_key in subsections_to_update:
for subsection in subsections_to_update: transformed_subsection_structure = get_course_blocks(
SubsectionGradeFactory(student).update(subsection, course_key) student,
subsection_usage_key,
collected_block_structure=collected_block_structure,
)
SubsectionGradeFactory(student).update(subsection_usage_key, transformed_subsection_structure, course)
...@@ -3,10 +3,11 @@ Tests for the score change signals defined in the courseware models module. ...@@ -3,10 +3,11 @@ Tests for the score change signals defined in the courseware models module.
""" """
import ddt import ddt
from unittest import skip
from django.test import TestCase from django.test import TestCase
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag, CoursePersistentGradesFlag
from mock import patch, MagicMock from mock import patch, MagicMock
from unittest import skip
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
...@@ -177,7 +178,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase): ...@@ -177,7 +178,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
def setUp(self): def setUp(self):
super(ScoreChangedUpdatesSubsectionGradeTest, self).setUp() super(ScoreChangedUpdatesSubsectionGradeTest, self).setUp()
self.user = UserFactory() self.user = UserFactory()
PersistentGradesEnabledFlag.objects.create(enabled=True) PersistentGradesEnabledFlag.objects.create(enabled_for_all_courses=True, enabled=True)
def set_up_course(self, enable_subsection_grades=True): def set_up_course(self, enable_subsection_grades=True):
""" """
...@@ -189,7 +190,8 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase): ...@@ -189,7 +190,8 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
name='course', name='course',
run='run', run='run',
) )
CoursePersistentGradesFlag.objects.create(course_id=self.course.id, enabled=enable_subsection_grades) if not enable_subsection_grades:
PersistentGradesEnabledFlag.objects.create(enabled=False)
self.chapter = ItemFactory.create(parent=self.course, category="chapter", display_name="Chapter") self.chapter = ItemFactory.create(parent=self.course, category="chapter", display_name="Chapter")
self.sequential = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Open Sequential") self.sequential = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Open Sequential")
...@@ -211,23 +213,36 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase): ...@@ -211,23 +213,36 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
def test_subsection_grade_updated_on_signal(self, default_store): def test_subsection_grade_updated_on_signal(self, default_store):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course() self.set_up_course()
with check_mongo_calls(2) and self.assertNumQueries(19): self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
with check_mongo_calls(2) and self.assertNumQueries(15):
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs) recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
def test_single_call_to_create_block_structure(self):
self.set_up_course()
self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
with patch(
'openedx.core.lib.block_structure.factory.BlockStructureFactory.create_from_cache',
return_value=None,
) as mock_block_structure_create:
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
self.assertEquals(mock_block_structure_create.call_count, 1)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_query_count_does_not_change_with_more_problems(self, default_store): def test_query_count_does_not_change_with_more_problems(self, default_store):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course() self.set_up_course()
self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2') ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2')
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3') ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3')
with check_mongo_calls(2) and self.assertNumQueries(19): with check_mongo_calls(2) and self.assertNumQueries(15):
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs) recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_subsection_grades_not_enabled_on_course(self, default_store): def test_subsection_grades_not_enabled_on_course(self, default_store):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course(enable_subsection_grades=False) self.set_up_course(enable_subsection_grades=False)
with check_mongo_calls(2) and self.assertNumQueries(2): self.assertFalse(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
with check_mongo_calls(2) and self.assertNumQueries(0):
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs) recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
@skip("Pending completion of TNL-5089") @skip("Pending completion of TNL-5089")
...@@ -242,21 +257,13 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase): ...@@ -242,21 +257,13 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
PersistentGradesEnabledFlag.objects.create(enabled=feature_flag) PersistentGradesEnabledFlag.objects.create(enabled=feature_flag)
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course() self.set_up_course()
with check_mongo_calls(0) and self.assertNumQueries(19 if feature_flag else 1): with check_mongo_calls(0) and self.assertNumQueries(15 if feature_flag else 1):
SCORE_CHANGED.send(sender=None, **self.score_changed_kwargs) SCORE_CHANGED.send(sender=None, **self.score_changed_kwargs)
@ddt.data( @ddt.data('user', 'course_id', 'usage_id')
('points_possible', 2, 19), def test_missing_kwargs(self, kwarg):
('points_earned', 2, 19),
('user', 0, 0),
('course_id', 0, 0),
('usage_id', 0, 0),
)
@ddt.unpack
def test_missing_kwargs(self, kwarg, expected_mongo_calls, expected_sql_calls):
self.set_up_course() self.set_up_course()
self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
del self.score_changed_kwargs[kwarg] del self.score_changed_kwargs[kwarg]
with patch('lms.djangoapps.grades.signals.handlers.log') as log_mock: with self.assertRaises(KeyError):
with check_mongo_calls(expected_mongo_calls) and self.assertNumQueries(expected_sql_calls): recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
self.assertEqual(log_mock.exception.called, kwarg not in ['points_possible', 'points_earned'])
...@@ -8,6 +8,7 @@ The following internal data structures are implemented: ...@@ -8,6 +8,7 @@ The following internal data structures are implemented:
_BlockRelations - Data structure for a single block's relations. _BlockRelations - Data structure for a single block's relations.
_BlockData - Data structure for a single block's data. _BlockData - Data structure for a single block's data.
""" """
from copy import deepcopy
from functools import partial from functools import partial
from logging import getLogger from logging import getLogger
...@@ -413,6 +414,19 @@ class BlockStructureBlockData(BlockStructure): ...@@ -413,6 +414,19 @@ class BlockStructureBlockData(BlockStructure):
# Map of a transformer's name to its non-block-specific data. # Map of a transformer's name to its non-block-specific data.
self.transformer_data = TransformerDataMap() self.transformer_data = TransformerDataMap()
def copy(self):
"""
Returns a new instance of BlockStructureBlockData with a
deep-copy of this instance's contents.
"""
from .factory import BlockStructureFactory
return BlockStructureFactory.create_new(
self.root_block_usage_key,
deepcopy(self._block_relations),
deepcopy(self.transformer_data),
deepcopy(self._block_data_map),
)
def iteritems(self): def iteritems(self):
""" """
Returns iterator of (UsageKey, BlockData) pairs for all Returns iterator of (UsageKey, BlockData) pairs for all
......
...@@ -6,7 +6,8 @@ from logging import getLogger ...@@ -6,7 +6,8 @@ from logging import getLogger
from openedx.core.lib.cache_utils import zpickle, zunpickle from openedx.core.lib.cache_utils import zpickle, zunpickle
from .block_structure import BlockStructureModulestoreData, BlockStructureBlockData from .block_structure import BlockStructureBlockData
from .factory import BlockStructureFactory
logger = getLogger(__name__) # pylint: disable=C0103 logger = getLogger(__name__) # pylint: disable=C0103
...@@ -97,12 +98,12 @@ class BlockStructureCache(object): ...@@ -97,12 +98,12 @@ class BlockStructureCache(object):
# Deserialize and construct the block structure. # Deserialize and construct the block structure.
block_relations, transformer_data, block_data_map = zunpickle(zp_data_from_cache) block_relations, transformer_data, block_data_map = zunpickle(zp_data_from_cache)
block_structure = BlockStructureModulestoreData(root_block_usage_key) return BlockStructureFactory.create_new(
block_structure._block_relations = block_relations root_block_usage_key,
block_structure.transformer_data = transformer_data block_relations,
block_structure._block_data_map = block_data_map transformer_data,
block_data_map,
return block_structure )
def delete(self, root_block_usage_key): def delete(self, root_block_usage_key):
""" """
......
""" """
Module for factory class for BlockStructure objects. Module for factory class for BlockStructure objects.
""" """
from .block_structure import BlockStructureModulestoreData from .block_structure import BlockStructureModulestoreData, BlockStructureBlockData
class BlockStructureFactory(object): class BlockStructureFactory(object):
...@@ -82,3 +82,14 @@ class BlockStructureFactory(object): ...@@ -82,3 +82,14 @@ class BlockStructureFactory(object):
NoneType - If the root_block_usage_key is not found in the cache. NoneType - If the root_block_usage_key is not found in the cache.
""" """
return block_structure_cache.get(root_block_usage_key) return block_structure_cache.get(root_block_usage_key)
@classmethod
def create_new(cls, root_block_usage_key, block_relations, transformer_data, block_data_map):
"""
Returns a new block structure for given the arguments.
"""
block_structure = BlockStructureBlockData(root_block_usage_key)
block_structure._block_relations = block_relations # pylint: disable=protected-access
block_structure.transformer_data = transformer_data
block_structure._block_data_map = block_data_map # pylint: disable=protected-access
return block_structure
...@@ -33,7 +33,7 @@ class BlockStructureManager(object): ...@@ -33,7 +33,7 @@ class BlockStructureManager(object):
self.modulestore = modulestore self.modulestore = modulestore
self.block_structure_cache = BlockStructureCache(cache) self.block_structure_cache = BlockStructureCache(cache)
def get_transformed(self, transformers, starting_block_usage_key=None): def get_transformed(self, transformers, starting_block_usage_key=None, collected_block_structure=None):
""" """
Returns the transformed Block Structure for the root_block_usage_key, Returns the transformed Block Structure for the root_block_usage_key,
starting at starting_block_usage_key, getting block data from the cache starting at starting_block_usage_key, getting block data from the cache
...@@ -50,11 +50,17 @@ class BlockStructureManager(object): ...@@ -50,11 +50,17 @@ class BlockStructureManager(object):
in the block structure that is to be transformed. in the block structure that is to be transformed.
If None, root_block_usage_key is used. If None, root_block_usage_key is used.
collected_block_structure (BlockStructureBlockData) - A
block structure retrieved from a prior call to
get_collected. Can be optionally provided if already available,
for optimization.
Returns: Returns:
BlockStructureBlockData - A transformed block structure, BlockStructureBlockData - A transformed block structure,
starting at starting_block_usage_key. starting at starting_block_usage_key.
""" """
block_structure = self.get_collected() block_structure = collected_block_structure.copy() if collected_block_structure else self.get_collected()
if starting_block_usage_key: if starting_block_usage_key:
# Override the root_block_usage_key so traversals start at the # Override the root_block_usage_key so traversals start at the
# requested location. The rest of the structure will be pruned # requested location. The rest of the structure will be pruned
......
...@@ -221,3 +221,48 @@ class TestBlockStructureData(TestCase, ChildrenMapTestMixin): ...@@ -221,3 +221,48 @@ class TestBlockStructureData(TestCase, ChildrenMapTestMixin):
block_structure = self.create_block_structure(ChildrenMapTestMixin.LINEAR_CHILDREN_MAP) block_structure = self.create_block_structure(ChildrenMapTestMixin.LINEAR_CHILDREN_MAP)
block_structure.remove_block_traversal(lambda block: block == 2) block_structure.remove_block_traversal(lambda block: block == 2)
self.assert_block_structure(block_structure, [[1], [], [], []], missing_blocks=[2]) self.assert_block_structure(block_structure, [[1], [], [], []], missing_blocks=[2])
def test_copy(self):
def _set_value(structure, value):
"""
Sets a test transformer block field to the given value in the given structure.
"""
structure.set_transformer_block_field(1, 'transformer', 'test_key', value)
def _get_value(structure):
"""
Returns the value of the test transformer block field in the given structure.
"""
return structure[1].transformer_data['transformer'].test_key
# create block structure and verify blocks pre-exist
block_structure = self.create_block_structure(ChildrenMapTestMixin.LINEAR_CHILDREN_MAP)
self.assert_block_structure(block_structure, [[1], [2], [3], []])
_set_value(block_structure, 'original_value')
# create a new copy of the structure and verify they are equivalent
new_copy = block_structure.copy()
self.assertEquals(block_structure.root_block_usage_key, new_copy.root_block_usage_key)
for block in block_structure:
self.assertIn(block, new_copy)
self.assertEquals(block_structure.get_parents(block), new_copy.get_parents(block))
self.assertEquals(block_structure.get_children(block), new_copy.get_children(block))
self.assertEquals(_get_value(block_structure), _get_value(new_copy))
# verify edits to original block structure do not affect the copy
block_structure.remove_block(2, keep_descendants=True)
self.assert_block_structure(block_structure, [[1], [3], [], []], missing_blocks=[2])
self.assert_block_structure(new_copy, [[1], [2], [3], []])
_set_value(block_structure, 'edit1')
self.assertEquals(_get_value(block_structure), 'edit1')
self.assertEquals(_get_value(new_copy), 'original_value')
# verify edits to copy do not affect the original
new_copy.remove_block(3, keep_descendants=True)
self.assert_block_structure(block_structure, [[1], [3], [], []], missing_blocks=[2])
self.assert_block_structure(new_copy, [[1], [2], [], []], missing_blocks=[3])
_set_value(new_copy, 'edit2')
self.assertEquals(_get_value(block_structure), 'edit1')
self.assertEquals(_get_value(new_copy), 'edit2')
...@@ -54,3 +54,15 @@ class TestBlockStructureFactory(TestCase, ChildrenMapTestMixin): ...@@ -54,3 +54,15 @@ class TestBlockStructureFactory(TestCase, ChildrenMapTestMixin):
block_structure_cache=cache, block_structure_cache=cache,
) )
) )
def test_new(self):
block_structure = BlockStructureFactory.create_from_modulestore(
root_block_usage_key=0, modulestore=self.modulestore
)
new_structure = BlockStructureFactory.create_new(
block_structure.root_block_usage_key,
block_structure._block_relations, # pylint: disable=protected-access
block_structure.transformer_data,
block_structure._block_data_map, # pylint: disable=protected-access
)
self.assert_block_structure(new_structure, self.children_map)
...@@ -139,6 +139,24 @@ class TestBlockStructureManager(TestCase, ChildrenMapTestMixin): ...@@ -139,6 +139,24 @@ class TestBlockStructureManager(TestCase, ChildrenMapTestMixin):
TestTransformer1.assert_collected(block_structure) TestTransformer1.assert_collected(block_structure)
TestTransformer1.assert_transformed(block_structure) TestTransformer1.assert_transformed(block_structure)
def test_get_transformed_with_collected(self):
with mock_registered_transformers(self.registered_transformers):
collected_block_structure = self.bs_manager.get_collected()
# using the same collected block structure,
# transform at different starting blocks
for (starting_block, expected_structure, expected_missing_blocks) in [
(0, [[1, 2], [3, 4], [], [], []], []),
(1, [[], [3, 4], [], [], []], [0, 2]),
(2, [[], [], [], [], []], [0, 1, 3, 4]),
]:
block_structure = self.bs_manager.get_transformed(
self.transformers,
starting_block_usage_key=starting_block,
collected_block_structure=collected_block_structure,
)
self.assert_block_structure(block_structure, expected_structure, missing_blocks=expected_missing_blocks)
def test_get_transformed_with_nonexistent_starting_block(self): def test_get_transformed_with_nonexistent_starting_block(self):
with mock_registered_transformers(self.registered_transformers): with mock_registered_transformers(self.registered_transformers):
with self.assertRaises(UsageKeyNotInBlockStructure): with self.assertRaises(UsageKeyNotInBlockStructure):
......
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