Added scripts to analyze sanitizer poisoning

Scripts are meant to be used in conjunction with DexFileTrackingRegistrar.
After poisoning certain sections of a dex file, the logcat will contain
traces in addition to information of dex files. This is used in order to
condense the large amount of information that is dumped into the logcat.

Bug: 37754950
Test: art/tools/runtime_memusage/sanitizer_logcat_analysis.sh
[LOGCAT_FILE]

Change-Id: Ied28e09899ec097f09332784bf80481f9c7dcb3f
diff --git a/runtime/dex_file_tracking_registrar.cc b/runtime/dex_file_tracking_registrar.cc
index f41a50b..848e2f3 100644
--- a/runtime/dex_file_tracking_registrar.cc
+++ b/runtime/dex_file_tracking_registrar.cc
@@ -69,7 +69,10 @@
 
 // Intended for local changes only.
 void DexFileTrackingRegistrar::SetDexSections() {
-  if (kDexFileAccessTracking || dex_file_ != nullptr) {
+  if (kDexFileAccessTracking && dex_file_ != nullptr) {
+    // Logs the Dex File's location and starting address if tracking is enabled
+    LOG(ERROR) << "RegisterDexFile: " << dex_file_->GetLocation() + " @ " << std::hex
+               << reinterpret_cast<uintptr_t>(dex_file_->Begin());
     switch (kCurrentTrackingSystem) {
       case kWholeDexTracking:
         SetDexFileRegistration(true);
diff --git a/tools/add_package_property.sh b/tools/add_package_property.sh
old mode 100644
new mode 100755
diff --git a/tools/runtime_memusage/README b/tools/runtime_memusage/README
new file mode 100644
index 0000000..2543df1
--- /dev/null
+++ b/tools/runtime_memusage/README
@@ -0,0 +1,72 @@
+Dex File Poisoning Access
+=========================
+
+These set of executables are useful for condensing large amounts of memory reads
+of Dex Files into smaller, split pieces of information. Two kinds of information
+are provided:
+	1. Visualizing what part of a Dex File is being accessed at what time
+	as a graph
+	2. Ordering stack traces by most commonly occurring
+Both of these kinds of information can be split up further by providing category
+names as arguments. A trace is put into a category if the category name is a
+substring of the symbolized trace.
+
+How:
+======
+These set of tools  work in conjunction with the class
+DexFileTrackingRegistrar, which marks sections of Dex Files as poisoned. As Dex
+Files are marked for poisoning, their starting addresses are logged in logcat.
+In addition, when poisoned sections of memory are accesses, their stack trace is
+also outputted to logcat.
+
+sanitizer_logcat_analysis.sh is the main executable that will use the other two
+in order to give both types of information. The other two are used in some of
+the intermediary steps which are described in sanitizer_logcat_analysis.sh,
+though they can also be executed individually if provided the necessary input.
+
+Why:
+======
+
+The main reason for splitting the functionality across multiple files is because
+sanitizer_logcat_analysis.sh uses external executable development/scripts/stack.
+This is necessary  in order to get symbolized traces from the output given by
+Address Sanitizer.
+
+How to Use:
+
+sanitizer_logcat_analysis.sh at minimum requires all logcat output in the form
+of a file. Additional options specified below are useful for removing
+unnecessary trace information.
+
+===========================================================================
+Usage: sanitizer_logcat_analysis.sh [options] [LOGCAT_FILE] [CATEGORIES...]
+    -d  OUT_DIRECTORY
+        Puts all output in specified directory.
+        If not given, output will be put in a local
+        temp folder which will be deleted after
+        execution.
+
+    -e
+        All traces will have exactly the same number
+        of categories which is specified by either
+        the -m argument or by prune_sanitizer_output.py
+
+    -f
+        forces redo of all commands even if output
+        files exist.
+
+    -m  [MINIMUM_CALLS_PER_TRACE]
+        Filters out all traces that do not have
+        at least MINIMUM_CALLS_PER_TRACE lines.
+        default: specified by prune_sanitizer_output.py
+
+    CATEGORIES are words that are expected to show in
+       a large subset of symbolized traces. Splits
+       output based on each word.
+
+    LOGCAT_FILE is the piped output from adb logcat.
+===========================================================================
+
+
+
+
diff --git a/tools/runtime_memusage/prune_sanitizer_output.py b/tools/runtime_memusage/prune_sanitizer_output.py
new file mode 100755
index 0000000..222c3c7
--- /dev/null
+++ b/tools/runtime_memusage/prune_sanitizer_output.py
@@ -0,0 +1,160 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Cleans up overlapping portions of traces provided by logcat."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import argparse
+import os
+import sys
+
+STACK_DIVIDER = 65 * '='
+
+
+def has_min_lines(trace, stack_min_size):
+    """Checks if the trace has a minimum amount of levels in trace."""
+    # Line containing 'use-after-poison' contains address accessed, which is
+    # useful for extracting Dex File offsets
+    string_checks = ['use-after-poison', 'READ']
+    required_checks = string_checks + ['#%d ' % line_ctr
+                                       for line_ctr in
+                                       range(stack_min_size)
+                                       ]
+    try:
+        trace_indices = [trace.index(check) for check in required_checks]
+        return all(trace_indices[trace_ind] < trace_indices[trace_ind + 1]
+                   for trace_ind in range(len(trace_indices) - 1))
+    except ValueError:
+        return False
+    return True
+
+
+def prune_exact(trace, stack_min_size):
+    """Removes all of trace that comes after the (stack_min_size)th trace."""
+    string_checks = ['use-after-poison', 'READ']
+    required_checks = string_checks + ['#%d ' % line_ctr
+                                       for line_ctr in
+                                       range(stack_min_size)
+                                       ]
+    trace_indices = [trace.index(check) for check in required_checks]
+    new_line_index = trace.index("\n", trace_indices[-1])
+    return trace[:new_line_index + 1]
+
+
+def make_unique(trace):
+    """Removes overlapping line numbers and lines out of order."""
+    string_checks = ['use-after-poison', 'READ']
+    hard_checks = string_checks + ['#%d ' % line_ctr
+                                   for line_ctr in range(100)
+                                   ]
+    last_ind = -1
+    for str_check in hard_checks:
+        try:
+            location_ind = trace.index(str_check)
+            if last_ind > location_ind:
+                trace = trace[:trace[:location_ind].find("\n") + 1]
+            last_ind = location_ind
+            try:
+                next_location_ind = trace.index(str_check, location_ind + 1)
+                trace = trace[:next_location_ind]
+            except ValueError:
+                pass
+        except ValueError:
+            pass
+    return trace
+
+
+def parse_args(argv):
+    """Parses arguments passed in."""
+    parser = argparse.ArgumentParser()
+    parser.add_argument('-d', action='store',
+                        default="", dest="out_dir_name", type=is_directory,
+                        help='Output Directory')
+    parser.add_argument('-e', action='store_true',
+                        default=False, dest='check_exact',
+                        help='Forces each trace to be cut to have '
+                             'minimum number of lines')
+    parser.add_argument('-m', action='store',
+                        default=4, dest='stack_min_size', type=int,
+                        help='minimum number of lines a trace should have')
+    parser.add_argument('trace_file', action='store',
+                        type=argparse.FileType('r'),
+                        help='File only containing lines that are related to '
+                             'Sanitizer traces')
+    return parser.parse_args(argv)
+
+
+def is_directory(path_name):
+    """Checks if a path is an actual directory."""
+    if not os.path.isdir(path_name):
+        dir_error = "%s is not a directory" % (path_name)
+        raise argparse.ArgumentTypeError(dir_error)
+    return path_name
+
+
+def main(argv=None):
+    """Parses arguments and cleans up traces using other functions."""
+    stack_min_size = 4
+    check_exact = False
+
+    if argv is None:
+        argv = sys.argv
+
+    parsed_argv = parse_args(argv[1:])
+    stack_min_size = parsed_argv.stack_min_size
+    check_exact = parsed_argv.check_exact
+    out_dir_name = parsed_argv.out_dir_name
+    trace_file = parsed_argv.trace_file
+
+    trace_split = trace_file.read().split(STACK_DIVIDER)
+    trace_file.close()
+    # if flag -e is enabled
+    if check_exact:
+        trace_prune_split = [prune_exact(trace, stack_min_size)
+                             for trace in trace_split if
+                             has_min_lines(trace, stack_min_size)
+                             ]
+        trace_unique_split = [make_unique(trace)
+                              for trace in trace_prune_split
+                              ]
+    else:
+        trace_unique_split = [make_unique(trace)
+                              for trace in trace_split if
+                              has_min_lines(trace, stack_min_size)
+                              ]
+    # has_min_lines is called again because removing lines can prune too much
+    trace_clean_split = [trace for trace
+                         in trace_unique_split if
+                         has_min_lines(trace,
+                                       stack_min_size)
+                         ]
+
+    outfile = os.path.join(out_dir_name, trace_file.name + '_filtered')
+    with open(outfile, "w") as output_file:
+        output_file.write(STACK_DIVIDER.join(trace_clean_split))
+
+    filter_percent = 100.0 - (float(len(trace_clean_split)) /
+                              len(trace_split) * 100)
+    filter_amount = len(trace_split) - len(trace_clean_split)
+    print("Filtered out %d (%f%%) of %d."
+          % (filter_amount, filter_percent, len(trace_split)))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/runtime_memusage/sanitizer_logcat_analysis.sh b/tools/runtime_memusage/sanitizer_logcat_analysis.sh
new file mode 100755
index 0000000..75cb9a9
--- /dev/null
+++ b/tools/runtime_memusage/sanitizer_logcat_analysis.sh
@@ -0,0 +1,180 @@
+#!/bin/sh
+#
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Note: Requires $ANDROID_BUILD_TOP/build/envsetup.sh to have been run.
+#
+# This script takes in a logcat containing Sanitizer traces and outputs several
+# files, prints information regarding the traces, and plots information as well.
+USE_TEMP=true
+DO_REDO=false
+# EXACT_ARG and MIN_ARG are passed to prune_sanitizer_output.py
+EXACT_ARG=""
+MIN_ARG=""
+usage() {
+  echo "Usage: $0 [options] [LOGCAT_FILE] [CATEGORIES...]"
+  echo "    -d  OUT_DIRECTORY"
+  echo "        Puts all output in specified directory."
+  echo "        If not given, output will be put in a local"
+  echo "        temp folder which will be deleted after"
+  echo "        execution."
+  echo
+  echo "    -e"
+  echo "        All traces will have exactly the same number"
+  echo "        of categories which is specified by either"
+  echo "        the -m argument or by prune_sanitizer_output.py"
+  echo
+  echo "    -f"
+  echo "        forces redo of all commands even if output"
+  echo "        files exist. Steps are skipped if their output"
+  echo "        exist already and this is not enabled."
+  echo
+  echo "    -m  [MINIMUM_CALLS_PER_TRACE]"
+  echo "        Filters out all traces that do not have"
+  echo "        at least MINIMUM_CALLS_PER_TRACE lines."
+  echo "        default: specified by prune_sanitizer_output.py"
+  echo
+  echo "    CATEGORIES are words that are expected to show in"
+  echo "       a large subset of symbolized traces. Splits"
+  echo "       output based on each word."
+  echo
+  echo "    LOGCAT_FILE is the piped output from adb logcat."
+  echo
+}
+
+
+while [[ $# -gt 1 ]]; do
+case $1 in
+  -d)
+  shift
+  USE_TEMP=false
+  OUT_DIR=$1
+  shift
+  break
+  ;;
+  -e)
+  shift
+  EXACT_ARG='-e'
+  ;;
+  -f)
+  shift
+  DO_REDO=true
+  ;;
+  -m)
+  shift
+  MIN_ARG='-m '"$1"''
+  shift
+  ;;
+  *)
+  usage
+  exit
+esac
+done
+
+if [ $# -lt 1 ]; then
+  usage
+  exit
+fi
+
+LOGCAT_FILE=$1
+NUM_CAT=$(($# - 1))
+
+# Use a temp directory that will be deleted
+if [ $USE_TEMP = true ]; then
+  OUT_DIR=$(mktemp -d --tmpdir=$PWD)
+  DO_REDO=true
+fi
+
+if [ ! -d "$OUT_DIR" ]; then
+  mkdir $OUT_DIR
+  DO_REDO=true
+fi
+
+# Note: Steps are skipped if their output exists until -f flag is enabled
+# Step 1 - Only output lines related to Sanitizer
+# Folder that holds all file output
+echo "Output folder: $OUT_DIR"
+ASAN_OUT=$OUT_DIR/asan_output
+if [ ! -f $ASAN_OUT ] || [ $DO_REDO = true ]; then
+  DO_REDO=true
+  echo "Extracting ASAN output"
+  grep "app_process64" $LOGCAT_FILE > $ASAN_OUT
+else
+  echo "Skipped: Extracting ASAN output"
+fi
+
+# Step 2 - Only output lines containing Dex File Start Addresses
+DEX_START=$OUT_DIR/dex_start
+if [ ! -f $DEX_START ] || [ $DO_REDO = true ]; then
+  DO_REDO=true
+  echo "Extracting Start of Dex File(s)"
+  grep "RegisterDexFile" $LOGCAT_FILE > $DEX_START
+else
+  echo "Skipped: Extracting Start of Dex File(s)"
+fi
+
+# Step 3 - Clean Sanitizer output from Step 2 since logcat cannot
+# handle large amounts of output.
+ASAN_OUT_FILTERED=$OUT_DIR/asan_output_filtered
+if [ ! -f $ASAN_OUT_FILTERED ] || [ $DO_REDO = true ]; then
+  DO_REDO=true
+  echo "Filtering/Cleaning ASAN output"
+  python $ANDROID_BUILD_TOP/art/tools/runtime_memusage/prune_sanitizer_output.py \
+  $EXACT_ARG $MIN_ARG -d $OUT_DIR $ASAN_OUT
+else
+  echo "Skipped: Filtering/Cleaning ASAN output"
+fi
+
+# Step 4 - Retrieve symbolized stack traces from Step 3 output
+SYM_FILTERED=$OUT_DIR/sym_filtered
+if [ ! -f $SYM_FILTERED ] || [ $DO_REDO = true ]; then
+  DO_REDO=true
+  echo "Retrieving symbolized traces"
+  $ANDROID_BUILD_TOP/development/scripts/stack $ASAN_OUT_FILTERED > $SYM_FILTERED
+else
+  echo "Skipped: Retrieving symbolized traces"
+fi
+
+# Step 5 - Using Steps 2, 3, 4 outputs in order to output graph data
+# and trace data
+# Only the category names are needed for the commands giving final output
+shift
+TIME_OUTPUT=($OUT_DIR/time_output_*.dat)
+if [ ! -e ${TIME_OUTPUT[0]} ] || [ $DO_REDO = true ]; then
+  DO_REDO=true
+  echo "Creating Categorized Time Table"
+  python $ANDROID_BUILD_TOP/art/tools/runtime_memusage/symbol_trace_info.py \
+    -d $OUT_DIR $ASAN_OUT_FILTERED $SYM_FILTERED $DEX_START $@
+else
+  echo "Skipped: Creating Categorized Time Table"
+fi
+
+# Step 6 - Use graph data from Step 5 to plot graph
+# Contains the category names used for legend of gnuplot
+PLOT_CATS=`echo \"Uncategorized $@\"`
+echo "Plotting Categorized Time Table"
+# Plots the information from logcat
+gnuplot --persist -e \
+  'filename(n) = sprintf("'"$OUT_DIR"'/time_output_%d.dat", n);
+   catnames = '"$PLOT_CATS"';
+   set title "Dex File Offset vs. Time accessed since App Start";
+   set xlabel "Time (milliseconds)";
+   set ylabel "Dex File Offset (bytes)";
+   plot for [i=0:'"$NUM_CAT"'] filename(i) using 1:2 title word(catnames, i + 1);'
+
+if [ $USE_TEMP = true ]; then
+  echo "Removing temp directory and files"
+  rm -rf $OUT_DIR
+fi
diff --git a/tools/runtime_memusage/symbol_trace_info.py b/tools/runtime_memusage/symbol_trace_info.py
new file mode 100755
index 0000000..57ed6ce
--- /dev/null
+++ b/tools/runtime_memusage/symbol_trace_info.py
@@ -0,0 +1,214 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Outputs quantitative information about Address Sanitizer traces."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from collections import Counter
+from datetime import datetime
+import argparse
+import bisect
+import os
+import sys
+
+
+def find_match(list_substrings, big_string):
+    """Returns the category a trace belongs to by searching substrings."""
+    for ind, substr in enumerate(list_substrings):
+        if big_string.find(substr) != -1:
+            return ind
+    return list_substrings.index("Uncategorized")
+
+
+def absolute_to_relative(plot_list, dex_start_list, cat_list):
+    """Address changed to Dex File offset and shifting time to 0 min in ms."""
+    time_format_str = "%H:%M:%S.%f"
+    first_access_time = datetime.strptime(plot_list[0][0],
+                                          time_format_str)
+    for ind, elem in enumerate(plot_list):
+        elem_date_time = datetime.strptime(elem[0], time_format_str)
+        # Shift time values so that first access is at time 0 milliseconds
+        elem[0] = int((elem_date_time - first_access_time).total_seconds() *
+                      1000)
+        address_access = int(elem[1], 16)
+        # For each poisoned address, find highest Dex File starting address less
+        # than address_access
+        dex_file_start = dex_start_list[bisect.bisect(dex_start_list,
+                                                      address_access) - 1
+                                        ]
+        elem.insert(1, address_access - dex_file_start)
+        # Category that a data point belongs to
+        elem.insert(2, cat_list[ind])
+
+
+def print_category_info(cat_split, outname, out_dir_name, title):
+    """Prints information of category and puts related traces in a files."""
+    trace_counts_dict = Counter(cat_split)
+    trace_counts_list_ordered = trace_counts_dict.most_common()
+    print(53 * "-")
+    print(title)
+    print("\tNumber of distinct traces: " +
+          str(len(trace_counts_list_ordered)))
+    print("\tSum of trace counts: " +
+          str(sum([trace[1] for trace in trace_counts_list_ordered])))
+    print("\n\tCount: How many traces appeared with count\n\t")
+    print(Counter([trace[1] for trace in trace_counts_list_ordered]))
+    with open(os.path.join(out_dir_name, outname), "w") as output_file:
+        for trace in trace_counts_list_ordered:
+            output_file.write("\n\nNumber of times appeared: " +
+                              str(trace[1]) +
+                              "\n")
+            output_file.write(trace[0].strip())
+
+
+def print_categories(categories, symbol_file_split, out_dir_name):
+    """Prints details of all categories."""
+    # Info of traces containing a call to current category
+    for cat_num, cat_name in enumerate(categories[1:]):
+        print("\nCategory #%d" % (cat_num + 1))
+        cat_split = [trace for trace in symbol_file_split
+                     if cat_name in trace]
+        cat_file_name = cat_name.lower() + "cat_output"
+        print_category_info(cat_split, cat_file_name, out_dir_name,
+                            "Traces containing: " + cat_name)
+        noncat_split = [trace for trace in symbol_file_split
+                        if cat_name not in trace]
+        print_category_info(noncat_split, "non" + cat_file_name,
+                            out_dir_name,
+                            "Traces not containing: " +
+                            cat_name)
+
+    # All traces (including uncategorized) together
+    print_category_info(symbol_file_split, "allcat_output",
+                        out_dir_name,
+                        "All traces together:")
+    # Traces containing none of keywords
+    # Only used if categories are passed in
+    if len(categories) > 1:
+        noncat_split = [trace for trace in symbol_file_split if
+                        all(cat_name not in trace
+                            for cat_name in categories)]
+        print_category_info(noncat_split, "noncat_output",
+                            out_dir_name,
+                            "Uncategorized calls")
+
+
+def is_directory(path_name):
+    """Checks if a path is an actual directory."""
+    if not os.path.isdir(path_name):
+        dir_error = "%s is not a directory" % (path_name)
+        raise argparse.ArgumentTypeError(dir_error)
+    return path_name
+
+
+def parse_args(argv):
+    """Parses arguments passed in."""
+    parser = argparse.ArgumentParser()
+    parser.add_argument('-d', action='store',
+                        default="", dest="out_dir_name", type=is_directory,
+                        help='Output Directory')
+    parser.add_argument('sanitizer_trace', action='store',
+                        type=argparse.FileType('r'),
+                        help='File containing sanitizer traces filtered by '
+                             'prune_sanitizer_output.py')
+    parser.add_argument('symbol_trace', action='store',
+                        type=argparse.FileType('r'),
+                        help='File containing symbolized traces that match '
+                             'sanitizer_trace')
+    parser.add_argument('dex_starts', action='store',
+                        type=argparse.FileType('r'),
+                        help='File containing starting addresses of Dex Files')
+    parser.add_argument('categories', action='store', nargs='*',
+                        help='Keywords expected to show in large amounts of'
+                             ' symbolized traces')
+
+    return parser.parse_args(argv)
+
+
+def read_data(parsed_argv):
+    """Reads data from filepath arguments and parses them into lists."""
+    # Using a dictionary to establish relation between lists added
+    data_lists = {}
+    categories = parsed_argv.categories
+    # Makes sure each trace maps to some category
+    categories.insert(0, "Uncategorized")
+
+    logcat_file_data = parsed_argv.sanitizer_trace.readlines()
+    parsed_argv.sanitizer_trace.close()
+
+    symbol_file_split = parsed_argv.symbol_trace.read().split("Stack Trace")[
+        1:]
+    parsed_argv.symbol_trace.close()
+
+    dex_start_file_data = parsed_argv.dex_starts.readlines()
+    parsed_argv.dex_starts.close()
+
+    # Each element is a tuple of time and address accessed
+    data_lists["plot_list"] = [[elem[1] for elem in enumerate(line.split())
+                                if elem[0] in (1, 11)
+                                ]
+                               for line in logcat_file_data
+                               if "use-after-poison" in line
+                               ]
+    # Contains a mapping between traces and the category they belong to
+    # based on arguments
+    data_lists["cat_list"] = [categories[find_match(categories, trace)]
+                              for trace in symbol_file_split]
+
+    # Contains a list of starting address of all dex files to calculate dex
+    # offsets
+    data_lists["dex_start_list"] = [int(line.split("@")[1], 16)
+                                    for line in dex_start_file_data
+                                    if "RegisterDexFile" in line
+                                    ]
+    return data_lists, categories, symbol_file_split
+
+
+def main(argv=None):
+    """Takes in trace information and outputs details about them."""
+    if argv is None:
+        argv = sys.argv
+    parsed_argv = parse_args(argv[1:])
+
+    data_lists, categories, symbol_file_split = read_data(parsed_argv)
+    # Formats plot_list such that each element is a data point
+    absolute_to_relative(data_lists["plot_list"], data_lists["dex_start_list"],
+                         data_lists["cat_list"])
+    for file_ext, cat_name in enumerate(categories):
+        out_file_name = os.path.join(parsed_argv.out_dir_name, "time_output_" +
+                                     str(file_ext) +
+                                     ".dat")
+        with open(out_file_name, "w") as output_file:
+            output_file.write("# Category: " + cat_name + "\n")
+            output_file.write("# Time, Dex File Offset, Address \n")
+            for time, dex_offset, category, address in data_lists["plot_list"]:
+                if category == cat_name:
+                    output_file.write(
+                        str(time) +
+                        " " +
+                        str(dex_offset) +
+                        " #" +
+                        str(address) +
+                        "\n")
+
+    print_categories(categories, symbol_file_split, parsed_argv.out_dir_name)
+
+
+if __name__ == '__main__':
+    main()