How to Test Performance

Load Testing

Server Side

Legacy Tests - https://github.com/edx/load-tests (internal-only repo)

Current Tests - https://github.com/edx/edx-load-tests 

Framework - https://github.com/locustio/locust(used currently in edx-load-tests. We also have a fork, and some legacy JMeter scripts)

How to Run Load Tests

Client Side (Browser)

Framework - http://www.sitespeed.io/ (Install via npm)

Useful add-in - https://github.com/edx/edx-sitespeed (for logged-in sessions)

Network throttling tools: Network Link Conditioner, tc (ubuntu native), wondersharper (which does not work on virtual machines), etc


Monitoring in Environments

New Relic


Profiling

PyInstrument

PyInstrument can be helpful because of the way it writes output data as a call tree. More info here:

https://github.com/joerick/pyinstrument

Example of a profile using pyinstrument, writing output to a file:

from pyinstrument import Profiler
profiler = Profiler(use_signal=False)
profiler.start()
# Do stuff here
profiler.stop()
 
# write profile data to file
with open('my.log', 'w') as f:
  f.write(profiler.output_text())

SnakeViz

Here are the steps to profile your python code with cProfile and then view the results using SnakeViz.

    1. export VAGRANT_X11=1
    2. vagrant ssh
    3. First time only: Install SnakeViz.  Please update this document with the steps you used if you do it.
    4. sudo su edxapp
    5. Add the following to the code that you want to profile


      @contextmanager
      def collect_profile(file_prefix):
          """
          Context manager to collect profile information.
          """
          import cProfile
          import uuid
          profiler = cProfile.Profile()
          profiler.enable()
          try:
              yield
          finally:
              profiler.disable()
              profiler.dump_stats("{0}_{1}.profile".format(file_prefix, uuid.uuid4()))
      
      with collect_profile:
          <python_code_to_profile>

      OR, if you want to profile an entire function, you can use this decorator:

      def collect_profile_func(file_prefix, enabled=False):
          """
          Method decorator for collecting profile.
          """
          import functools
      
          def _outer(func):
              """
              Outer function decorator.
              """
              @functools.wraps(func)
              def _inner(self, *args, **kwargs):
                  """
                  Inner wrapper function.
                  """
                  if enabled:
                      with collect_profile(file_prefix):
                          return func(self, *args, **kwargs)
                  else:
                      return func(self, *args, **kwargs)
              return _inner
          return _outer
      
      
      @contextmanager
      def collect_profile(file_prefix):
          """
          Context manager to collect profile information.
          """
          import cProfile
          import uuid
          profiler = cProfile.Profile()
          profiler.enable()
          try:
              yield
          finally:
              profiler.disable()
              profiler.dump_stats("{0}_{1}.profile".format(file_prefix, uuid.uuid4()))
    6. Then use SnakeViz by issuing:

      1. snakeviz DIRECTORY/FILE_NAME.profile

Tips

When profiling locally, disable features that would affect performance numbers.  For instance, the following can be set in the a local [lms/cms]/envs/private.py file:

from .common import INSTALLED_APPS, MIDDLEWARE_CLASSES, FEATURES
def tuple_without(source_tuple, exclusion_list):
    """Return new tuple excluding any entries in the exclusion list. Needed because tuples
    are immutable. Order preserved."""
    return tuple([i for i in source_tuple if i not in exclusion_list])
INSTALLED_APPS = tuple_without(INSTALLED_APPS, ['debug_toolbar', 'debug_toolbar_mongo'])
MIDDLEWARE_CLASSES = tuple_without(MIDDLEWARE_CLASSES, [
    'django_comment_client.utils.QueryCountDebugMiddleware',
    'debug_toolbar.middleware.DebugToolbarMiddleware',
])
DEBUG_TOOLBAR_MONGO_STACKTRACES = False

import contracts
contracts.disable_all()