Merge branch 'master' into 8079-api-client-auth-uuid
authorradhika <radhika@curoverse.com>
Mon, 15 Feb 2016 13:40:54 +0000 (08:40 -0500)
committerradhika <radhika@curoverse.com>
Mon, 15 Feb 2016 13:40:54 +0000 (08:40 -0500)
backports/python-gflags/fpm-info.sh [new file with mode: 0644]
sdk/cli/bin/crunch-job
sdk/python/setup.py
tools/crunchstat-summary/crunchstat_summary/chartjs.py
tools/crunchstat-summary/crunchstat_summary/reader.py
tools/crunchstat-summary/crunchstat_summary/summarizer.py
tools/crunchstat-summary/tests/logfile_20151204190335.txt.gz.report
tools/crunchstat-summary/tests/logfile_20151210063411.txt.gz.report
tools/crunchstat-summary/tests/logfile_20151210063439.txt.gz.report
tools/crunchstat-summary/tests/test_examples.py

diff --git a/backports/python-gflags/fpm-info.sh b/backports/python-gflags/fpm-info.sh
new file mode 100644 (file)
index 0000000..67a989e
--- /dev/null
@@ -0,0 +1 @@
+fpm_args+=(-v 2.0)
index 4e5b0826b6bceabf326b07b65660a5ec4c93f946..ae210a6f447e42d69ecd9302f414866bb4da6e23 100755 (executable)
@@ -432,7 +432,7 @@ fi
   if ($docker_pid == 0)
   {
     srun (["srun", "--nodelist=" . join(',', @node)],
-          ["/bin/sh", "-ec", $docker_install_script]);
+          ["/bin/bash", "-o", "pipefail", "-ec", $docker_install_script]);
     exit ($?);
   }
   while (1)
@@ -443,8 +443,8 @@ fi
   }
   if ($? != 0)
   {
-    croak("Installing Docker image from $docker_locator exited "
-          .exit_status_s($?));
+    Log(undef, "Installing Docker image from $docker_locator exited " . exit_status_s($?));
+    exit(EX_RETRY_UNLOCKED);
   }
 
   # Determine whether this version of Docker supports memory+swap limits.
index 98b7361e92e94a5da39fdfe446f92546020e0ab1..b6518f95a17495851c72985af0e8b16a97e3f1b6 100644 (file)
@@ -43,7 +43,7 @@ setup(name='arvados-python-client',
           'google-api-python-client',
           'httplib2',
           'pycurl >=7.19.5.1, <7.21.5',
-          'python-gflags',
+          'python-gflags<3.0',
           'ws4py'
       ],
       test_suite='tests',
index fb30041889c726e4cc14d93a5aaeb7b4e1fbbbd9..03e45e601d39eaa3803f2e75e62c9b6c3721e4f1 100644 (file)
@@ -2,6 +2,7 @@ from __future__ import print_function
 
 import cgi
 import json
+import math
 import pkg_resources
 
 from crunchstat_summary import logger
@@ -35,12 +36,25 @@ class ChartJS(object):
             }
             for s in self.summarizers]
 
+    def _axisY(self, tasks, stat):
+        ymax = 1
+        for task in tasks.itervalues():
+            for pt in task.series[stat]:
+                ymax = max(ymax, pt[1])
+        ytick = math.exp((1+math.floor(math.log(ymax, 2)))*math.log(2))/4
+        return {
+            'gridColor': '#cccccc',
+            'gridThickness': 1,
+            'interval': ytick,
+            'minimum': 0,
+            'maximum': ymax,
+            'valueFormatString': "''",
+        }
+
     def charts(self, label, tasks):
         return [
             {
-                'axisY': {
-                    'minimum': 0,
-                },
+                'axisY': self._axisY(tasks=tasks, stat=stat),
                 'data': [
                     {
                         'type': 'line',
index 049b48f0636089a5354e16550fa5daa9877b9227..2b6ebce16dc55ff5ad63af14d0924c4cd245a545 100644 (file)
@@ -17,6 +17,10 @@ class CollectionReader(object):
                 "collection {} has {} files; need exactly one".format(
                     collection_id, len(filenames)))
         self._reader = collection.open(filenames[0])
+        self._label = "{}/{}".format(collection_id, filenames[0])
+
+    def __str__(self):
+        return self._label
 
     def __iter__(self):
         return iter(self._reader)
@@ -27,33 +31,39 @@ class LiveLogReader(object):
 
     def __init__(self, job_uuid):
         logger.debug('load stderr events for job %s', job_uuid)
-        self._filters = [
-            ['object_uuid', '=', job_uuid],
-            ['event_type', '=', 'stderr']]
-        self._label = job_uuid
+        self.job_uuid = job_uuid
+
+    def __str__(self):
+        return self.job_uuid
 
     def _get_all_pages(self):
         got = 0
         last_id = 0
-        while True:
-            page = arvados.api().logs().index(
-                limit=1000,
-                order=['id asc'],
-                filters=self._filters + [['id','>',str(last_id)]],
-            ).execute(num_retries=2)
-            got += len(page['items'])
-            logger.debug(
-                '%s: received %d of %d log events',
-                self._label, got,
-                got + page['items_available'] - len(page['items']))
-            for i in page['items']:
-                for line in i['properties']['text'].split('\n'):
-                    self._queue.put(line+'\n')
-                last_id = i['id']
-            if (len(page['items']) == 0 or
-                len(page['items']) >= page['items_available']):
-                break
-        self._queue.put(self.EOF)
+        filters = [
+            ['object_uuid', '=', self.job_uuid],
+            ['event_type', '=', 'stderr']]
+        try:
+            while True:
+                page = arvados.api().logs().index(
+                    limit=1000,
+                    order=['id asc'],
+                    filters=filters + [['id','>',str(last_id)]],
+                    select=['id', 'properties'],
+                ).execute(num_retries=2)
+                got += len(page['items'])
+                logger.debug(
+                    '%s: received %d of %d log events',
+                    self.job_uuid, got,
+                    got + page['items_available'] - len(page['items']))
+                for i in page['items']:
+                    for line in i['properties']['text'].split('\n'):
+                        self._queue.put(line+'\n')
+                    last_id = i['id']
+                if (len(page['items']) == 0 or
+                    len(page['items']) >= page['items_available']):
+                    break
+        finally:
+            self._queue.put(self.EOF)
 
     def __iter__(self):
         self._queue = Queue.Queue()
@@ -65,5 +75,6 @@ class LiveLogReader(object):
     def next(self):
         line = self._queue.get()
         if line is self.EOF:
+            self._thread.join()
             raise StopIteration
         return line
index 48bec6a9339c0740021b43735797e58a8e6a5756..d058c232ac3c4ff177e11aa29a3590a8be4a833a 100644 (file)
@@ -10,6 +10,7 @@ import itertools
 import math
 import re
 import sys
+import threading
 
 from arvados.api import OrderedJsonModel
 from crunchstat_summary import logger
@@ -37,8 +38,7 @@ class Summarizer(object):
 
         # stats_max: {category: {stat: val}}
         self.stats_max = collections.defaultdict(
-            functools.partial(collections.defaultdict,
-                              lambda: float('-Inf')))
+            functools.partial(collections.defaultdict, lambda: 0))
         # task_stats: {task_id: {category: {stat: val}}}
         self.task_stats = collections.defaultdict(
             functools.partial(collections.defaultdict, dict))
@@ -52,10 +52,10 @@ class Summarizer(object):
         # constructor will overwrite this with something useful.
         self.existing_constraints = {}
 
-        logger.debug("%s: logdata %s", self.label, repr(logdata))
+        logger.debug("%s: logdata %s", self.label, logdata)
 
     def run(self):
-        logger.debug("%s: parsing log data", self.label)
+        logger.debug("%s: parsing logdata %s", self.label, self._logdata)
         for line in self._logdata:
             m = re.search(r'^\S+ \S+ \d+ (?P<seq>\d+) job_task (?P<task_uuid>\S+)$', line)
             if m:
@@ -65,7 +65,7 @@ class Summarizer(object):
                 logger.debug('%s: seq %d is task %s', self.label, seq, uuid)
                 continue
 
-            m = re.search(r'^\S+ \S+ \d+ (?P<seq>\d+) success in (?P<elapsed>\d+) seconds', line)
+            m = re.search(r'^\S+ \S+ \d+ (?P<seq>\d+) (success in|failure \(#., permanent\) after) (?P<elapsed>\d+) seconds', line)
             if m:
                 task_id = self.seq_to_uuid[int(m.group('seq'))]
                 elapsed = int(m.group('elapsed'))
@@ -87,6 +87,7 @@ class Summarizer(object):
                 child_summarizer.stats_max = self.stats_max
                 child_summarizer.task_stats = self.task_stats
                 child_summarizer.tasks = self.tasks
+                child_summarizer.starttime = self.starttime
                 child_summarizer.run()
                 logger.debug('%s: done %s', self.label, uuid)
                 continue
@@ -160,11 +161,11 @@ class Summarizer(object):
                             val = val / this_interval_s
                             if stat in ['user+sys__rate', 'tx+rx__rate']:
                                 task.series[category, stat].append(
-                                    (timestamp - task.starttime, val))
+                                    (timestamp - self.starttime, val))
                     else:
                         if stat in ['rss']:
                             task.series[category, stat].append(
-                                (timestamp - task.starttime, val))
+                                (timestamp - self.starttime, val))
                         self.task_stats[task_id][category][stat] = val
                     if val > self.stats_max[category][stat]:
                         self.stats_max[category][stat] = val
@@ -196,6 +197,8 @@ class Summarizer(object):
         return label
 
     def text_report(self):
+        if not self.tasks:
+            return "(no report generated)\n"
         return "\n".join(itertools.chain(
             self._text_report_gen(),
             self._recommend_gen())) + "\n"
@@ -225,17 +228,30 @@ class Summarizer(object):
                  lambda x: x * 100),
                 ('Overall CPU usage: {}%',
                  self.job_tot['cpu']['user+sys'] /
-                 self.job_tot['time']['elapsed'],
+                 self.job_tot['time']['elapsed']
+                 if self.job_tot['time']['elapsed'] > 0 else 0,
                  lambda x: x * 100),
                 ('Max memory used by a single task: {}GB',
                  self.stats_max['mem']['rss'],
                  lambda x: x / 1e9),
                 ('Max network traffic in a single task: {}GB',
-                 self.stats_max['net:eth0']['tx+rx'],
+                 self.stats_max['net:eth0']['tx+rx'] +
+                 self.stats_max['net:keep0']['tx+rx'],
                  lambda x: x / 1e9),
                 ('Max network speed in a single interval: {}MB/s',
-                 self.stats_max['net:eth0']['tx+rx__rate'],
-                 lambda x: x / 1e6)):
+                 self.stats_max['net:eth0']['tx+rx__rate'] +
+                 self.stats_max['net:keep0']['tx+rx__rate'],
+                 lambda x: x / 1e6),
+                ('Keep cache miss rate {}%',
+                 (float(self.job_tot['keepcache']['miss']) /
+                 float(self.job_tot['keepcalls']['get']))
+                 if self.job_tot['keepcalls']['get'] > 0 else 0,
+                 lambda x: x * 100.0),
+                ('Keep cache utilization {}%',
+                 (float(self.job_tot['blkio:0:0']['read']) /
+                 float(self.job_tot['net:keep0']['rx']))
+                 if self.job_tot['net:keep0']['rx'] > 0 else 0,
+                 lambda x: x * 100.0)):
             format_string, val, transform = args
             if val == float('-Inf'):
                 continue
@@ -246,7 +262,8 @@ class Summarizer(object):
     def _recommend_gen(self):
         return itertools.chain(
             self._recommend_cpu(),
-            self._recommend_ram())
+            self._recommend_ram(),
+            self._recommend_keep_cache())
 
     def _recommend_cpu(self):
         """Recommend asking for 4 cores if max CPU usage was 333%"""
@@ -255,7 +272,7 @@ class Summarizer(object):
         if cpu_max_rate == float('-Inf'):
             logger.warning('%s: no CPU usage data', self.label)
             return
-        used_cores = int(math.ceil(cpu_max_rate))
+        used_cores = max(1, int(math.ceil(cpu_max_rate)))
         asked_cores = self.existing_constraints.get('min_cores_per_node')
         if asked_cores is None or used_cores < asked_cores:
             yield (
@@ -318,6 +335,23 @@ class Summarizer(object):
                 int(used_mib),
                 int(math.ceil(nearlygibs(used_mib))*AVAILABLE_RAM_RATIO*1024))
 
+    def _recommend_keep_cache(self):
+        """Recommend increasing keep cache if miss rate is above 0.2%"""
+        if self.job_tot['keepcalls']['get'] == 0:
+            return
+        miss_rate = float(self.job_tot['keepcache']['miss']) / float(self.job_tot['keepcalls']['get']) * 100.0
+        asked_mib = self.existing_constraints.get('keep_cache_mb_per_task', 256)
+
+        if miss_rate > 0.2:
+            yield (
+                '#!! {} Keep cache miss rate was {:.2f}% -- '
+                'try runtime_constraints "keep_cache_mb_per_task":{}'
+            ).format(
+                self.label,
+                miss_rate,
+                asked_mib*2)
+
+
     def _format(self, val):
         """Return a string representation of a stat.
 
@@ -342,10 +376,16 @@ class JobSummarizer(Summarizer):
             self.job = arv.jobs().get(uuid=job).execute()
         else:
             self.job = job
-        if self.job['log']:
-            rdr = crunchstat_summary.reader.CollectionReader(self.job['log'])
-            label = self.job['uuid']
-        else:
+        rdr = None
+        if self.job.get('log'):
+            try:
+                rdr = crunchstat_summary.reader.CollectionReader(self.job['log'])
+            except arvados.errors.NotFoundError as e:
+                logger.warning("Trying event logs after failing to read "
+                               "log collection %s: %s", self.job['log'], e)
+            else:
+                label = self.job['uuid']
+        if rdr is None:
             rdr = crunchstat_summary.reader.LiveLogReader(self.job['uuid'])
             label = self.job['uuid'] + ' (partial)'
         super(JobSummarizer, self).__init__(rdr, **kwargs)
@@ -363,21 +403,24 @@ class PipelineSummarizer(object):
             if 'job' not in component:
                 logger.warning(
                     "%s: skipping component with no job assigned", cname)
-            elif component['job'].get('log') is None:
-                logger.warning(
-                    "%s: skipping job %s with no log available",
-                    cname, component['job'].get('uuid'))
             else:
                 logger.info(
-                    "%s: logdata %s", cname, component['job']['log'])
+                    "%s: job %s", cname, component['job']['uuid'])
                 summarizer = JobSummarizer(component['job'], **kwargs)
-                summarizer.label = cname
+                summarizer.label = '{} {}'.format(
+                    cname, component['job']['uuid'])
                 self.summarizers[cname] = summarizer
         self.label = pipeline_instance_uuid
 
     def run(self):
+        threads = []
         for summarizer in self.summarizers.itervalues():
-            summarizer.run()
+            t = threading.Thread(target=summarizer.run)
+            t.daemon = True
+            t.start()
+            threads.append(t)
+        for t in threads:
+            t.join()
 
     def text_report(self):
         txt = ''
index 0ba0181c1215620c7f0a4334c2eb77dc3d64d7a6..f0a60957bba706b18b6d41288f5fc46ded754b1c 100644 (file)
@@ -1,16 +1,16 @@
 category       metric  task_max        task_max_rate   job_total
-blkio:0:0      read    0       0.00    0
-blkio:0:0      write   0       0.00    0
+blkio:0:0      read    0       0       0
+blkio:0:0      write   0       0       0
 cpu    cpus    8       -       -
 cpu    sys     1.92    0.04    1.92
 cpu    user    3.83    0.09    3.83
 cpu    user+sys        5.75    0.13    5.75
-fuseops        read    0       0.00    0
-fuseops        write   0       0.00    0
-keepcache      hit     0       0.00    0
-keepcache      miss    0       0.00    0
-keepcalls      get     0       0.00    0
-keepcalls      put     0       0.00    0
+fuseops        read    0       0       0
+fuseops        write   0       0       0
+keepcache      hit     0       0       0
+keepcache      miss    0       0       0
+keepcalls      get     0       0       0
+keepcalls      put     0       0       0
 mem    cache   1678139392      -       -
 mem    pgmajfault      0       -       0
 mem    rss     349814784       -       -
@@ -18,9 +18,9 @@ mem   swap    0       -       -
 net:eth0       rx      1754364530      41658344.87     1754364530
 net:eth0       tx      38837956        920817.97       38837956
 net:eth0       tx+rx   1793202486      42579162.83     1793202486
-net:keep0      rx      0       0.00    0
-net:keep0      tx      0       0.00    0
-net:keep0      tx+rx   0       0.00    0
+net:keep0      rx      0       0       0
+net:keep0      tx      0       0       0
+net:keep0      tx+rx   0       0       0
 time   elapsed 80      -       80
 # Number of tasks: 1
 # Max CPU time spent by a single task: 5.75s
@@ -29,5 +29,7 @@ time  elapsed 80      -       80
 # Max memory used by a single task: 0.35GB
 # Max network traffic in a single task: 1.79GB
 # Max network speed in a single interval: 42.58MB/s
+# Keep cache miss rate 0.00%
+# Keep cache utilization 0.00%
 #!! 4xphq-8i9sb-jq0ekny1xou3zoh max CPU usage was 13% -- try runtime_constraints "min_cores_per_node":1
 #!! 4xphq-8i9sb-jq0ekny1xou3zoh max RSS was 334 MiB -- try runtime_constraints "min_ram_mb_per_node":972
index 0641bbac9f6c4f88c008adb279440e88be4f628a..f9a34cfb98c7c6d42a633166b946b7c49db77cc1 100644 (file)
@@ -1,8 +1,8 @@
 category       metric  task_max        task_max_rate   job_total
 cpu    cpus    8       -       -
-cpu    sys     0.00    -       0.00
-cpu    user    0.00    -       0.00
-cpu    user+sys        0.00    -       0.00
+cpu    sys     0       -       0.00
+cpu    user    0       -       0.00
+cpu    user+sys        0       -       0.00
 mem    cache   12288   -       -
 mem    pgmajfault      0       -       0
 mem    rss     856064  -       -
@@ -12,8 +12,13 @@ net:eth0     tx      90      -       90
 net:eth0       tx+rx   180     -       180
 time   elapsed 2       -       4
 # Number of tasks: 2
-# Max CPU time spent by a single task: 0.00s
+# Max CPU time spent by a single task: 0s
+# Max CPU usage in a single interval: 0%
 # Overall CPU usage: 0.00%
 # Max memory used by a single task: 0.00GB
 # Max network traffic in a single task: 0.00GB
+# Max network speed in a single interval: 0.00MB/s
+# Keep cache miss rate 0.00%
+# Keep cache utilization 0.00%
+#!! 4xphq-8i9sb-zvb2ocfycpomrup max CPU usage was 0% -- try runtime_constraints "min_cores_per_node":1
 #!! 4xphq-8i9sb-zvb2ocfycpomrup max RSS was 1 MiB -- try runtime_constraints "min_ram_mb_per_node":972
index 19fe0ed764d4fc2c229c348161c0079d9d67b1dd..c54102d78a25d1158ba563a22de7279b3d39c2f2 100644 (file)
@@ -1,8 +1,8 @@
 category       metric  task_max        task_max_rate   job_total
 cpu    cpus    8       -       -
-cpu    sys     0.00    -       0.00
-cpu    user    0.00    -       0.00
-cpu    user+sys        0.00    -       0.00
+cpu    sys     0       -       0.00
+cpu    user    0       -       0.00
+cpu    user+sys        0       -       0.00
 mem    cache   8192    -       -
 mem    pgmajfault      0       -       0
 mem    rss     450560  -       -
@@ -12,8 +12,13 @@ net:eth0     tx      90      -       90
 net:eth0       tx+rx   180     -       180
 time   elapsed 2       -       3
 # Number of tasks: 2
-# Max CPU time spent by a single task: 0.00s
+# Max CPU time spent by a single task: 0s
+# Max CPU usage in a single interval: 0%
 # Overall CPU usage: 0.00%
 # Max memory used by a single task: 0.00GB
 # Max network traffic in a single task: 0.00GB
+# Max network speed in a single interval: 0.00MB/s
+# Keep cache miss rate 0.00%
+# Keep cache utilization 0.00%
+#!! 4xphq-8i9sb-v831jm2uq0g2g9x max CPU usage was 0% -- try runtime_constraints "min_cores_per_node":1
 #!! 4xphq-8i9sb-v831jm2uq0g2g9x max RSS was 1 MiB -- try runtime_constraints "min_ram_mb_per_node":972
index 6c1443733c35ec9ea76bd35e49fb3c69d6f83906..b1e5fed81d7b4481023de037d68656abe6fe4406 100644 (file)
@@ -144,6 +144,9 @@ class SummarizePipeline(ReportDiff):
             job_report + ['\n'] +
             ['### Summary for bar (zzzzz-8i9sb-000000000000001)\n'] +
             job_report + ['\n'] +
+            ['### Summary for unfinished-job (zzzzz-8i9sb-xxxxxxxxxxxxxxx)\n',
+             '(no report generated)\n',
+             '\n'] +
             ['### Summary for baz (zzzzz-8i9sb-000000000000002)\n'] +
             job_report)
         self.diff_report(cmd, expect)