diff --git a/.gitignore b/.gitignore
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5202b53ad2514a0f3407971bb5500c37973ab11a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,6 @@
+bins
+deps
+libs
+objs
+*.pyc
+
diff --git a/src/core/iomgr/pollset.h b/src/core/iomgr/pollset.h
index 7374a4ec13126468c1cb2fb6748c32bf03ada405..36d80d5c2975272928f798d389b2069e4dc1193f 100644
--- a/src/core/iomgr/pollset.h
+++ b/src/core/iomgr/pollset.h
@@ -35,6 +35,7 @@
 #define __GRPC_INTERNAL_IOMGR_POLLSET_H_
 
 #include <grpc/support/port_platform.h>
+#include <grpc/support/time.h>
 
 /* A grpc_pollset is a set of file descriptors that a higher level item is
    interested in. For example:
diff --git a/tools/run_tests/jobset.py b/tools/run_tests/jobset.py
index 2ab95ef97c7ff2200ae60e997ba2f488e498c752..17fb1d6924033e95af1fa9145a9187e0c6237545 100755
--- a/tools/run_tests/jobset.py
+++ b/tools/run_tests/jobset.py
@@ -39,6 +39,36 @@ _RUNNING = object()
 _KILLED = object()
 
 
+_COLORS = {
+    'red': 31,
+    'green': 32,
+    'yellow': 33,
+    }
+
+
+_BEGINNING_OF_LINE = '\x1b[0G'
+_CLEAR_LINE = '\x1b[2K'
+
+
+_TAG_COLOR = {
+    'FAILED': 'red',
+    'PASSED': 'green',
+    'START': 'yellow',
+    'WAITING': 'yellow',
+    }
+
+
+def message(tag, message, explanatory_text=None):
+  sys.stdout.write('%s%s\x1b[%dm%s\x1b[0m: %s%s' % (
+      _BEGINNING_OF_LINE,
+      _CLEAR_LINE,
+      _COLORS[_TAG_COLOR[tag]],
+      tag,
+      message,
+      '\n%s\n' % explanatory_text if explanatory_text is not None else ''))
+  sys.stdout.flush()
+
+
 class Job(object):
   """Manages one job."""
 
@@ -49,9 +79,7 @@ class Job(object):
                                      stderr=subprocess.STDOUT,
                                      stdout=self._tempfile)
     self._state = _RUNNING
-    sys.stdout.write('\x1b[0G\x1b[2K\x1b[33mSTART\x1b[0m: %s' %
-                     self._cmdline)
-    sys.stdout.flush()
+    message('START', self._cmdline)
 
   def state(self):
     """Poll current state of the job. Prints messages at completion."""
@@ -60,16 +88,10 @@ class Job(object):
         self._state = _FAILURE
         self._tempfile.seek(0)
         stdout = self._tempfile.read()
-        sys.stdout.write('\x1b[0G\x1b[2K\x1b[31mFAILED\x1b[0m: %s'
-                         ' [ret=%d]\n'
-                         '%s\n' % (
-                             self._cmdline, self._process.returncode, stdout))
-        sys.stdout.flush()
+        message('FAILED', '%s [ret=%d]' % (self._cmdline, self._process.returncode), stdout)
       else:
         self._state = _SUCCESS
-        sys.stdout.write('\x1b[0G\x1b[2K\x1b[32mPASSED\x1b[0m: %s' %
-                         self._cmdline)
-        sys.stdout.flush()
+        message('PASSED', '%s' % self._cmdline)
     return self._state
 
   def kill(self):
@@ -86,6 +108,7 @@ class Jobset(object):
     self._check_cancelled = check_cancelled
     self._cancelled = False
     self._failures = 0
+    self._completed = 0
     self._maxjobs = maxjobs
 
   def start(self, cmdline):
@@ -107,8 +130,11 @@ class Jobset(object):
         if st == _FAILURE: self._failures += 1
         dead.add(job)
       for job in dead:
+        self._completed += 1
         self._running.remove(job)
-      if not dead: return
+      if dead: return
+      message('WAITING', '%d jobs running, %d complete' % (
+          len(self._running), self._completed))
       time.sleep(0.1)
 
   def cancelled(self):
diff --git a/tools/run_tests/run_tests.py b/tools/run_tests/run_tests.py
index 00831d2d7462e648ec4b7b751b22907d29ca63c1..6ab80d90453e5893bacadbf21a6944faecee0f16 100755
--- a/tools/run_tests/run_tests.py
+++ b/tools/run_tests/run_tests.py
@@ -11,16 +11,44 @@ import time
 import jobset
 import watch_dirs
 
-# flags required for make for each configuration
-_CONFIGS = ['dbg', 'opt', 'tsan', 'msan', 'asan']
+# SimpleConfig: just compile with CONFIG=config, and run the binary to test
+class SimpleConfig(object):
+  def __init__(self, config):
+    self.build_config = config
+
+  def run_command(self, binary):
+    return [binary]
+
+
+# ValgrindConfig: compile with some CONFIG=config, but use valgrind to run
+class ValgrindConfig(object):
+  def __init__(self, config):
+    self.build_config = config
+
+  def run_command(self, binary):
+    return ['valgrind', binary]
+
+
+# different configurations we can run under
+_CONFIGS = {
+  'dbg': SimpleConfig('dbg'),
+  'opt': SimpleConfig('opt'),
+  'tsan': SimpleConfig('tsan'),
+  'msan': SimpleConfig('msan'),
+  'asan': SimpleConfig('asan'),
+  'valgrind': ValgrindConfig('dbg'),
+  }
+
+
+_DEFAULT = ['dbg', 'opt']
 _MAKE_TEST_TARGETS = ['buildtests_c', 'buildtests_cxx']
 
 # parse command line
 argp = argparse.ArgumentParser(description='Run grpc tests.')
 argp.add_argument('-c', '--config',
-                  choices=['all'] + _CONFIGS,
+                  choices=['all'] + sorted(_CONFIGS.keys()),
                   nargs='+',
-                  default=['all'])
+                  default=_DEFAULT)
 argp.add_argument('-t', '--test-filter', nargs='*', default=['*'])
 argp.add_argument('-n', '--runs_per_test', default=1, type=int)
 argp.add_argument('-f', '--forever',
@@ -30,10 +58,11 @@ argp.add_argument('-f', '--forever',
 args = argp.parse_args()
 
 # grab config
-configs = [cfg
-           for cfg in itertools.chain.from_iterable(
-               _CONFIGS if x == 'all' else [x]
-               for x in args.config)]
+run_configs = set(_CONFIGS[cfg]
+                  for cfg in itertools.chain.from_iterable(
+                      _CONFIGS.iterkeys() if x == 'all' else [x]
+                      for x in args.config))
+build_configs = set(cfg.build_config for cfg in run_configs)
 filters = args.test_filter
 runs_per_test = args.runs_per_test
 forever = args.forever
@@ -47,19 +76,20 @@ def _build_and_run(check_cancelled):
         '-j', '%d' % (multiprocessing.cpu_count() + 1),
         target,
         'CONFIG=%s' % cfg]
-       for cfg in configs
+       for cfg in build_configs
        for target in _MAKE_TEST_TARGETS),
       check_cancelled, maxjobs=1):
     sys.exit(1)
 
   # run all the tests
-  jobset.run(([x]
-              for x in itertools.chain.from_iterable(
-                  itertools.chain.from_iterable(itertools.repeat(
-                      glob.glob('bins/%s/%s_test' % (config, filt)),
-                      runs_per_test))
-                  for config in configs
-                  for filt in filters)), check_cancelled)
+  jobset.run((
+      config.run_command(x)
+      for config in run_configs
+      for filt in filters
+      for x in itertools.chain.from_iterable(itertools.repeat(
+          glob.glob('bins/%s/%s_test' % (
+              config.build_config, filt)),
+          runs_per_test))), check_cancelled)
 
 
 if forever: