[autotest] Report test result sizes to metrics

There are two reporting source:
1. Test jobs report result size information in tko/parse.
2. Special tasks report result size information at the end of autoserv process.

BUG=chromium:716218
TEST=unittest, local run test

Change-Id: I2410f3a3a5a16c1673446633e15c038a5e4ef81f
Reviewed-on: https://chromium-review.googlesource.com/571859
Commit-Ready: Dan Shi <dshi@google.com>
Tested-by: Dan Shi <dshi@google.com>
Reviewed-by: Dan Shi <dshi@google.com>
diff --git a/client/bin/result_tools/utils_lib.py b/client/bin/result_tools/utils_lib.py
index 4322e79..f9b33a7 100644
--- a/client/bin/result_tools/utils_lib.py
+++ b/client/bin/result_tools/utils_lib.py
@@ -4,6 +4,9 @@
 
 """Shared constants and methods for result utilities."""
 
+import collections
+
+
 # Following are key names for directory summaries. The keys are started with /
 # so it can be differentiated with a valid file name. The short keys are
 # designed for smaller file size of the directory summary.
@@ -18,4 +21,61 @@
 DIRS = '/D'
 # Default root directory name. To allow summaries to be merged effectively, all
 # summaries are collected with root directory of ''
-ROOT_DIR = ''
\ No newline at end of file
+ROOT_DIR = ''
+
+# Information of test result sizes to be stored in tko_job_keyvals.
+# The total size (in kB) of test results that generated during the test,
+# including:
+#  * server side test logs and result files.
+#  * client side test logs, sysinfo, system logs and crash dumps.
+# Note that a test can collect the same test result files from DUT multiple
+# times during the test, before and after each iteration/test. So the value of
+# client_result_collected_KB could be larger than the value of
+# result_uploaded_KB, which is the size of result directory on the server side,
+# even if the test result throttling is not applied.
+#
+# Attributes of the named tuple includes:
+# client_result_collected_KB: The total size (in KB) of test results collected
+#         from test device.
+# original_result_total_KB: The original size (in KB) of test results before
+#         being trimmed.
+# result_uploaded_KB: The total size (in KB) of test results to be uploaded by
+#         gs_offloader.
+# result_throttled: Flag to indicate if test results collection is throttled.
+ResultSizeInfo = collections.namedtuple(
+        'ResultSizeInfo',
+        ['client_result_collected_KB',
+         'original_result_total_KB',
+         'result_uploaded_KB',
+         'result_throttled'])
+
+
+def get_result_size_info(client_collected_bytes, summary):
+    """Get the result size information.
+
+    @param client_collected_bytes: Size in bytes of results collected from the
+            test device.
+    @param summary: A dictionary of directory summary.
+    @return: A namedtuple of result size informations, including:
+            client_result_collected_KB: The total size (in KB) of test results
+                    collected from test device.
+            original_result_total_KB: The original size (in KB) of test results
+                    before being trimmed.
+            result_uploaded_KB: The total size (in KB) of test results to be
+                    uploaded.
+            result_throttled: True if test results collection is throttled.
+    """
+    root_entry = summary[ROOT_DIR]
+    client_result_collected_KB= client_collected_bytes / 1024
+    original_result_total_KB = root_entry[ORIGINAL_SIZE_BYTES] / 1024
+    result_uploaded_KB = root_entry[TRIMMED_SIZE_BYTES] / 1024
+    # Test results are considered to be throttled if the total size of
+    # results collected is different from the total size of trimmed results
+    # from the client side.
+    result_throttled = (
+            root_entry[ORIGINAL_SIZE_BYTES] != root_entry[TRIMMED_SIZE_BYTES])
+
+    return ResultSizeInfo(client_result_collected_KB=client_result_collected_KB,
+                          original_result_total_KB=original_result_total_KB,
+                          result_uploaded_KB=result_uploaded_KB,
+                          result_throttled=result_throttled)
\ No newline at end of file
diff --git a/server/autoserv b/server/autoserv
index 90b899c..a6c4422 100755
--- a/server/autoserv
+++ b/server/autoserv
@@ -26,6 +26,7 @@
 from autotest_lib.client.common_lib import global_config
 from autotest_lib.client.common_lib import utils
 from autotest_lib.client.common_lib.cros.graphite import autotest_es
+from autotest_lib.server import site_utils
 
 try:
     from chromite.lib import metrics
@@ -558,6 +559,10 @@
 
         finally:
             job.close()
+            # Special task doesn't run parse, so result summary needs to be
+            # built here.
+            if results and (repair or verify or reset or cleanup or provision):
+                site_utils.collect_result_sizes(results)
     except:
         exit_code = 1
         traceback.print_exc()
diff --git a/server/site_utils.py b/server/site_utils.py
index 4e8626f..d57a690 100644
--- a/server/site_utils.py
+++ b/server/site_utils.py
@@ -12,11 +12,16 @@
 import random
 import re
 import time
+import traceback
 import urllib2
 
 import common
+from autotest_lib.client.bin.result_tools import utils as result_utils
+from autotest_lib.client.bin.result_tools import utils_lib as result_utils_lib
+from autotest_lib.client.bin.result_tools import view as result_view
 from autotest_lib.client.common_lib import utils
 from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib import file_utils
 from autotest_lib.client.common_lib import global_config
 from autotest_lib.client.common_lib import host_queue_entry_states
 from autotest_lib.client.common_lib import host_states
@@ -24,6 +29,11 @@
 from autotest_lib.server.cros.dynamic_suite import constants
 from autotest_lib.server.cros.dynamic_suite import job_status
 
+try:
+    from chromite.lib import metrics
+except ImportError:
+    metrics = utils.metrics_mock
+
 
 CONFIG = global_config.global_config
 
@@ -53,6 +63,8 @@
         'gm4g_sprout': 'seed_l8150',
         'bat': 'bat_land'
         }
+# Prefix for the metrics name for result size information.
+RESULT_METRICS_PREFIX = 'chromeos/autotest/result_collection/'
 
 class TestLabException(Exception):
     """Exception raised when the Test Lab blocks a test or suite."""
@@ -807,7 +819,7 @@
 
     @param duts: List of duts to check for idle state.
     @param afe: afe instance.
-    @param max_wait: Max wait time in seconds.
+    @param max_wait: Max wait time in seconds to wait for duts to be idle.
 
     @returns Boolean True if all hosts are idle or False if any hosts did not
             go idle within max_wait.
@@ -847,7 +859,7 @@
     @param duts: List of duts to lock.
     @param afe: afe instance.
     @param lock_msg: message for afe on locking this host.
-    @param max_wait: Max wait time in seconds.
+    @param max_wait: Max wait time in seconds to wait for duts to be idle.
 
     @returns Boolean lock_success where True if all duts locked successfully or
              False if we timed out waiting too long for hosts to go idle.
@@ -884,3 +896,91 @@
         if not re.match('board:[^-]+-\d+', board):
             return False
     return True
+
+
+def _get_default_size_info(path):
+    """Get the default result size information.
+
+    In case directory summary is failed to build, assume the test result is not
+    throttled and all result sizes are the size of existing test results.
+
+    @return: A namedtuple of result size informations, including:
+            client_result_collected_KB: The total size (in KB) of test results
+                    collected from test device. Set to be the total size of the
+                    given path.
+            original_result_total_KB: The original size (in KB) of test results
+                    before being trimmed. Set to be the total size of the given
+                    path.
+            result_uploaded_KB: The total size (in KB) of test results to be
+                    uploaded. Set to be the total size of the given path.
+            result_throttled: True if test results collection is throttled.
+                    It's set to False in this default behavior.
+    """
+    total_size = file_utils.get_directory_size_kibibytes(path);
+    return result_utils_lib.ResultSizeInfo(
+            client_result_collected_KB=total_size,
+            original_result_total_KB=total_size,
+            result_uploaded_KB=total_size,
+            result_throttled=False)
+
+
+def _report_result_size_metrics(result_size_info):
+    """Report result sizes information to metrics.
+
+    @param result_size_info: A ResultSizeInfo namedtuple containing information
+            of test result sizes.
+    """
+    fields = {'result_throttled' : result_size_info.result_throttled}
+    metrics.Counter(RESULT_METRICS_PREFIX + 'client_result_collected_KB',
+                    description='The total size (in KB) of test results '
+                    'collected from test device. Set to be the total size of '
+                    'the given path.'
+                    ).increment_by(result_size_info.client_result_collected_KB,
+                                   fields=fields)
+    metrics.Counter(RESULT_METRICS_PREFIX + 'original_result_total_KB',
+                    description='The original size (in KB) of test results '
+                    'before being trimmed.'
+                    ).increment_by(result_size_info.original_result_total_KB,
+                                   fields=fields)
+    metrics.Counter(RESULT_METRICS_PREFIX + 'result_uploaded_KB',
+                    description='The total size (in KB) of test results to be '
+                    'uploaded.'
+                    ).increment_by(result_size_info.result_uploaded_KB,
+                                   fields=fields)
+
+
+def collect_result_sizes(path, log=logging.debug):
+    """Collect the result sizes information and build result summary.
+
+    It first tries to merge directory summaries and calculate the result sizes
+    including:
+    client_result_collected_KB: The volume in KB that's transfered from the test
+            device.
+    original_result_total_KB: The volume in KB that's the original size of the
+            result files before being trimmed.
+    result_uploaded_KB: The volume in KB that will be uploaded.
+    result_throttled: Indicating if the result files were throttled.
+
+    If directory summary merging failed for any reason, fall back to use the
+    total size of the given result directory.
+
+    @param path: Path of the result directory to get size information.
+    @param log: The logging method, default to logging.debug
+    @return: A ResultSizeInfo namedtuple containing information of test result
+             sizes.
+    """
+    try:
+        client_collected_bytes, summary = result_utils.merge_summaries(path)
+        result_size_info = result_utils_lib.get_result_size_info(
+                client_collected_bytes, summary)
+        html_file = os.path.join(path, result_view.DEFAULT_RESULT_SUMMARY_NAME)
+        result_view.build(client_collected_bytes, summary, html_file)
+    except:
+        log('Failed to calculate result sizes based on directory summaries for '
+            'directory %s. Fall back to record the total size.\nException: %s' %
+            (path, traceback.format_exc()))
+        result_size_info = _get_default_size_info(path)
+
+    _report_result_size_metrics(result_size_info)
+
+    return result_size_info
\ No newline at end of file
diff --git a/tko/parse.py b/tko/parse.py
index 4a80aa3..4d8d4f1 100755
--- a/tko/parse.py
+++ b/tko/parse.py
@@ -13,10 +13,6 @@
 import traceback
 
 import common
-from autotest_lib.client.bin.result_tools import utils as result_utils
-from autotest_lib.client.bin.result_tools import utils_lib as result_utils_lib
-from autotest_lib.client.bin.result_tools import view as result_view
-from autotest_lib.client.common_lib import file_utils
 from autotest_lib.client.common_lib import global_config
 from autotest_lib.client.common_lib import mail, pidfile
 from autotest_lib.client.common_lib import utils
@@ -35,25 +31,6 @@
     'ParseOptions', ['reparse', 'mail_on_failure', 'dry_run', 'suite_report',
                      'datastore_creds', 'export_to_gcloud_path'])
 
-# Key names related to test result sizes to be stored in tko_job_keyvals.
-# The total size (in kB) of test results that generated during the test,
-# including:
-#  * server side test logs and result files.
-#  * client side test logs, sysinfo, system logs and crash dumps.
-# Note that a test can collect the same test result files from DUT multiple
-# times during the test, before and after each iteration/test. So the value of
-# CLIENT_RESULT_COLLECTED_KB could be larger than the value of
-# RESULT_UPLOADED_KB, which is the size of result directory on the server side,
-# even if the test result throttling is not applied.
-# The total size (in KB) of test results collected from test device.
-CLIENT_RESULT_COLLECTED_KB = 'client_result_collected_KB'
-# The original size (in KB) of test results before being trimmed.
-ORIGINAL_RESULT_TOTAL_KB = 'original_result_total_KB'
-# The total size (in KB) of test results to be uploaded by gs_offloader.
-RESULT_UPLOADED_KB = 'result_uploaded_KB'
-# Flag to indicate if test results collection is throttled.
-RESULT_THROTTLED = 'result_throttled'
-
 def parse_args():
     """Parse args."""
     # build up our options parser and parse sys.argv
@@ -255,53 +232,6 @@
     tko_utils.dprint('DEBUG: Invalidated tests associated to job: ' + msg)
 
 
-def _get_result_sizes(path):
-    """Get the result sizes information.
-
-    It first tries to merge directory summaries and calculate the result sizes
-    including:
-    CLIENT_RESULT_COLLECTED_KB: The volume in KB that's transfered from the test
-            device.
-    ORIGINAL_RESULT_TOTAL_KB: The volume in KB that's the original size of the
-            result files before being trimmed.
-    RESULT_UPLOADED_KB: The volume in KB that will be uploaded.
-    RESULT_THROTTLED: Indicating if the result files were throttled.
-
-    If directory summary merging failed for any reason, fall back to use the
-    total size of the given result directory.
-
-    @param path: Path of the result directory to get size information.
-    @return: A dictionary of result sizes information.
-    """
-    sizes = {}
-    try:
-        client_collected_bytes, summary = result_utils.merge_summaries(path)
-        root_entry = summary[result_utils_lib.ROOT_DIR]
-        sizes[CLIENT_RESULT_COLLECTED_KB] = client_collected_bytes / 1024
-        sizes[ORIGINAL_RESULT_TOTAL_KB] = (
-                root_entry[result_utils_lib.ORIGINAL_SIZE_BYTES]) / 1024
-        sizes[RESULT_UPLOADED_KB] = (
-                root_entry[result_utils_lib.TRIMMED_SIZE_BYTES])/ 1024
-        # Test results are considered to be throttled if the total size of
-        # results collected is different from the total size of trimmed results
-        # from the client side.
-        sizes[RESULT_THROTTLED] = (
-                root_entry[result_utils_lib.ORIGINAL_SIZE_BYTES] !=
-                root_entry[result_utils_lib.TRIMMED_SIZE_BYTES])
-        html_file = os.path.join(path, result_view.DEFAULT_RESULT_SUMMARY_NAME)
-        result_view.build(client_collected_bytes, summary, html_file)
-    except:
-        tko_utils.dprint('Failed to calculate result sizes based on directory '
-                         'summaries. Fall back to record the total size.\n'
-                         'Exception: %s' % traceback.format_exc())
-        total_size = file_utils.get_directory_size_kibibytes(path);
-        sizes[CLIENT_RESULT_COLLECTED_KB] = total_size
-        sizes[ORIGINAL_RESULT_TOTAL_KB] = total_size
-        sizes[RESULT_UPLOADED_KB] = total_size
-        sizes[RESULT_THROTTLED] = 0
-    return sizes
-
-
 def parse_one(db, jobname, path, parse_options):
     """Parse a single job. Optionally send email on failure.
 
@@ -395,15 +325,18 @@
             job.board = label_info.get('board', None)
             job.suite = label_info.get('suite', None)
 
+    # Record test result size to job_keyvals
+    result_size_info = site_utils.collect_result_sizes(
+            path, log=tko_utils.dprint)
+    job.keyval_dict.update(result_size_info.__dict__)
+
     # Upload job details to Sponge.
     if not dry_run:
         sponge_url = sponge_utils.upload_results(job, log=tko_utils.dprint)
         if sponge_url:
             job.keyval_dict['sponge_url'] = sponge_url
 
-    # Record test result size to job_keyvals
-    sizes = _get_result_sizes(path)
-    job.keyval_dict.update(sizes)
+    # TODO(dshi): Update sizes with sponge_invocation.xml and throttle it.
 
     # check for failures
     message_lines = [""]