From 0219071502a94f4a6ddc2fd546ff2c99f9f534d5 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 29 Dec 2015 13:13:08 -0500 Subject: [PATCH] Add New Relic instrumentation to SequenceModule. The goal of this is to capture more detailed usage and performance information around the kind of course content we have out there. Having this information will allow us to more easily query to see how sequence size and specific block types affect front end performance. --- common/lib/xmodule/xmodule/seq_module.py | 81 +++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index e74ba6dc03..2c125231bb 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -3,7 +3,7 @@ xModule implementation of a learning sequence """ # pylint: disable=abstract-method - +import collections import json import logging from pkg_resources import resource_string @@ -13,6 +13,7 @@ from lxml import etree from xblock.core import XBlock from xblock.fields import Integer, Scope, Boolean, String from xblock.fragment import Fragment +import newrelic.agent from .exceptions import NotFoundError from .fields import Date @@ -191,6 +192,11 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): context["username"] = self.runtime.service(self, "user").get_current_user().opt_attrs['edx-platform.username'] display_names = [self.get_parent().display_name or '', self.display_name or ''] + + # We do this up here because proctored exam functionality could bypass + # rendering after this section. + self._capture_basic_metrics() + # Is this sequential part of a timed or proctored exam? if self.is_time_limited: view_html = self._time_limited_student_view(context) @@ -201,7 +207,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): fragment.add_content(view_html) return fragment - for child in self.get_display_items(): + display_items = self.get_display_items() + for child in display_items: is_bookmarked = bookmarks_service.is_bookmarked(usage_key=child.scope_ids.usage_id) context["bookmarked"] = is_bookmarked @@ -238,8 +245,78 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): fragment.add_content(self.system.render_template("seq_module.html", params)) + self._capture_full_seq_item_metrics(display_items) + self._capture_current_unit_metrics(display_items) + + # Get all descendant XBlock types and counts return fragment + def _locations_in_subtree(self, node): + """ + The usage keys for all descendants of an XBlock/XModule as a flat list. + + Includes the location of the node passed in. + """ + stack = [node] + locations = [] + + while stack: + curr = stack.pop() + locations.append(curr.location) + if curr.has_children: + stack.extend(curr.get_children()) + + return locations + + def _capture_basic_metrics(self): + """ + Capture basic information about this sequence in New Relic. + """ + newrelic.agent.add_custom_parameter('seq.block_id', unicode(self.location)) + newrelic.agent.add_custom_parameter('seq.display_name', self.display_name or '') + newrelic.agent.add_custom_parameter('seq.position', self.position) + newrelic.agent.add_custom_parameter('seq.is_time_limited', self.is_time_limited) + + def _capture_full_seq_item_metrics(self, display_items): + """ + Capture information about the number and types of XBlock content in + the sequence as a whole. We send this information to New Relic so that + we can do better performance analysis of courseware. + """ + # Basic count of the number of Units (a.k.a. VerticalBlocks) we have in + # this learning sequence + newrelic.agent.add_custom_parameter('seq.num_units', len(display_items)) + + # Count of all modules (leaf nodes) in this sequence (e.g. videos, + # problems, etc.) The units (verticals) themselves are not counted. + all_item_keys = self._locations_in_subtree(self) + newrelic.agent.add_custom_parameter('seq.num_items', len(all_item_keys)) + + # Count of all modules by block_type (e.g. "video": 2, "discussion": 4) + block_counts = collections.Counter(usage_key.block_type for usage_key in all_item_keys) + for block_type, count in block_counts.items(): + newrelic.agent.add_custom_parameter('seq.block_counts.{}'.format(block_type), count) + + def _capture_current_unit_metrics(self, display_items): + """ + Capture information about the current selected Unit within the Sequence. + """ + # Positions are stored with indexing starting at 1. If we get into a + # weird state where the saved position is out of bounds (e.g. the + # content was changed), avoid going into any details about this unit. + if 1 <= self.position <= len(display_items): + # Basic info about the Unit... + current = display_items[self.position - 1] + newrelic.agent.add_custom_parameter('seq.current.block_id', unicode(current.location)) + newrelic.agent.add_custom_parameter('seq.current.display_name', current.display_name or '') + + # Examining all items inside the Unit (or split_test, conditional, etc.) + child_locs = self._locations_in_subtree(current) + newrelic.agent.add_custom_parameter('seq.current.num_items', len(child_locs)) + curr_block_counts = collections.Counter(usage_key.block_type for usage_key in child_locs) + for block_type, count in curr_block_counts.items(): + newrelic.agent.add_custom_parameter('seq.current.block_counts.{}'.format(block_type), count) + def _time_limited_student_view(self, context): """ Delegated rendering of a student view when in a time