[gjs] coverage: Serialize statistics to cache-path on cache misses



commit 84c8ededa093a2e0d93b16183f3f00cedb5ad13c
Author: Sam Spilsbury <smspillaz gmail com>
Date:   Sat Jun 13 10:40:46 2015 +0800

    coverage: Serialize statistics to cache-path on cache misses
    
    Running Reflect.parse can take a non-trivial amount of time to run
    and this can scale poorly when several files are passed to gjs
    as "coverage eligible." Autotools makes this problem worse, because
    each individual script containing tests invokes gjs again which
    means that potentially hundreds of files can be parsed per test.
    
    This problem makes tests with coverage mode enabled run very slowly.
    
    This change re-organizes the way that information is stored from
    Reflect so that we can easily serialize and de-serialize that
    information to a binary file between invocations.
    
    1) The process of fetching statistics out of coverage.js and into
       coverage.cpp was separated out from print_statistics_for_file
       into fetch_coverage_file_statistics_from_js
    2) All statistics are fetched for all coverage-eligible files,
       instead of just the actually-run files (although the actually
       run files are the only ones printed in the report).
    3) The importer is made available in the coverage compartment so
       that we can use JSUnit assertions with it.
    4) Function "keys" are immediately calculated whenever a function
       is detected in coverage.js, as opposed to being post-processed
       afterwards. This means that we can cache the keys themselves
       and restore them directly, as opposed to re-computing them.
    5) A "deactivate" method was added to CoverageStatistics, which
       disables the debugger completely in that compartment (allowing
       context re-use in tests).
    6) Finally, in coverage.js we determine the function key at the
       function detection site and tests were re-written to use
       function keys instead of function names. This will allow
       us to easily fetch from the cache object with the keys
       pre-filled instead of having to recompute them.
    
    The caching logic was also added. Some important features are:
    1) Regular files are stored with their modification time, and
       information obtained from individual files is not restored
       if the current modification time is greater than the stored
       one.
    2) GResource paths are stored along with a sha512sum of their
       contents. This is slighlty more expensive than using mtimes,
       but mtimes are not available for GResourceFile's.
    3) A method "staleCache" was added to CoverageStatistics which
       indicates whether information was read from Reflect. If so,
       coverage.cpp will write out a new cache.
    4) The cache file itself is stored in .internal-gjs-coverage-cache.
    5) The caching process happens in gjs_serialize_statistics, which
       calls the stringify method on CoverageStatistics in Coverage.
       This method converts the entire representation of the currently
       collected and relevant AST information into a JSON blob,
       which is then written out to disk.
    6) Restoration from the cache happens in
       gjs_deserialize_cache_to_object, which uses JSON.parse to
       deserialize the stored JSON blob back into an internal
       representation we can query straight away.
    7) Some helper functions like gjs_run_in_coverage_compartment
       and gjs_inject_value_into_coverage_compartment were added
       as "internal" functions for the coverage mode. They are
       essentially just helper functions to allow tests to inject
       objects and code into the coverage.js compartment.

 .gitignore                                         |    2 +
 Makefile-test.am                                   |   49 +-
 Makefile.am                                        |    3 +-
 configure.ac                                       |    2 +-
 gjs/console.cpp                                    |   11 +-
 gjs/coverage-internal.h                            |   20 +
 gjs/coverage.cpp                                   |  689 ++++++++++++++--
 gjs/coverage.h                                     |    4 +
 installed-tests/gjs-unit.cpp                       |    8 +-
 installed-tests/js/testCoverage.js                 |  354 ++++++--
 modules/coverage.js                                |  196 ++++-
 test/gjs-test-coverage.cpp                         |  904 +++++++++++++++++++-
 .../mock-js-resource-cache-after.gresource.xml     |    8 +
 .../cache_invalidation/after/resource.js           |    2 +
 .../mock-js-resource-cache-before.gresource.xml    |    8 +
 .../cache_invalidation/before/resource.js          |    2 +
 .../cache_notation/simple_branch.js                |    6 +
 .../cache_notation/simple_executable_lines.js      |    1 +
 .../cache_notation/simple_function.js              |    2 +
 test/mock-js-resources.gresource.xml               |    3 +
 20 files changed, 2067 insertions(+), 207 deletions(-)
---
diff --git a/.gitignore b/.gitignore
index 3401645..80850ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,6 +47,8 @@ jsunit-resources.[ch]
 testSystemExit.test
 modules-resources.[ch]
 mock-js-resources.[ch]
+mock-cache-invalidation-before.gresource
+mock-cache-invalidation-after.gresource
 *.gcda
 *.gcno
 lcov
diff --git a/Makefile-test.am b/Makefile-test.am
index 4f9d7d4..ac3efdb 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -41,8 +41,31 @@ mock-js-resources.h: $(srcdir)/test/mock-js-resources.gresource.xml $(modules_re
 mock-js-resources.c: $(srcdir)/test/mock-js-resources.gresource.xml $(modules_resource_files)
        $(AM_V_GEN) glib-compile-resources --target=$@ --sourcedir=$(srcdir) --sourcedir=$(builddir) 
--generate --c-name mock_js_resources $<
 
-EXTRA_DIST += $(mock_js_resources_files) $(srcdir)/test/mock-js-resources.gresource.xml \
-    $(srcdir)/test/gjs-test-coverage/loadedJSFromResource.js
+mock_js_invalidation_resources_dir = $(top_srcdir)/test/gjs-test-coverage/cache_invalidation
+mock_js_invalidation_before_resources_files =                                                          \
+       $(mock_js_invalidation_resources_dir)/before/resource.js                                        \
+       $(mock_js_invalidation_resources_dir)/before/mock-js-resource-cache-before.gresource.xml        \
+       $(NULL)
+mock_js_invalidation_after_resources_files =                                                           \
+       $(mock_js_invalidation_resources_dir)/after/resource.js                                         \
+       $(mock_js_invalidation_resources_dir)/after/mock-js-resource-cache-after.gresource.xml          \
+       $(NULL)
+mock_js_invalidation_resources_files =                 \
+       $(mock_js_invalidation_before_resources_files)  \
+       $(mock_js_invalidation_after_resources_files)   \
+       $(NULL)
+
+mock-cache-invalidation-before.gresource: $(mock_js_invalidation_before_resources_files)
+       $(AM_V_GEN) glib-compile-resources --target=$@ 
--sourcedir=$(mock_js_invalidation_resources_dir)/before 
$(mock_js_invalidation_resources_dir)/before/mock-js-resource-cache-before.gresource.xml
+mock-cache-invalidation-after.gresource: $(mock_js_invalidation_after_resources_files)
+       $(AM_V_GEN) glib-compile-resources --target=$@ 
--sourcedir=$(mock_js_invalidation_resources_dir)/after 
$(mock_js_invalidation_resources_dir)/after/mock-js-resource-cache-after.gresource.xml
+
+EXTRA_DIST +=                                                  \
+       $(mock_js_resources_files)                              \
+       $(mock_js_invalidation_resources_files)                 \
+       $(srcdir)/test/mock-js-resources.gresource.xml          \
+       $(srcdir)/test/gjs-test-coverage/loadedJSFromResource.js \
+       $(NULL)
 
 ## -rdynamic makes backtraces work
 gjs_tests_LDFLAGS = -rdynamic
@@ -50,11 +73,23 @@ gjs_tests_LDADD =           \
        libgjs.la               \
        $(GJSTESTS_LIBS)
 
-gjs_tests_SOURCES =            \
-       test/gjs-tests.cpp \
-       test/gjs-tests-add-funcs.h \
-       test/gjs-test-coverage.cpp \
-       mock-js-resources.c
+gjs_tests_SOURCES =                                    \
+       test/gjs-tests.cpp                              \
+       test/gjs-tests-add-funcs.h                      \
+       test/gjs-test-coverage.cpp                      \
+       mock-js-resources.c                             \
+       $(NULL)
+
+gjs_tests_DEPENDENCIES =                               \
+       mock-cache-invalidation-before.gresource        \
+       mock-cache-invalidation-after.gresource         \
+       $(NULL)
+
+CLEANFILES +=                                          \
+       mock-cache-invalidation-before.gresource        \
+       mock-cache-invalidation-after.gresource         \
+       mock-js-resources.c                             \
+       $(NULL)
 
 check-local: gjs-tests
        @test -z "${TEST_PROGS}" || ${GTESTER} --verbose ${TEST_PROGS} ${TEST_PROGS_OPTIONS}
diff --git a/Makefile.am b/Makefile.am
index ff2c008..9f8adbe 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -80,7 +80,8 @@ EXTRA_DIST +=                         \
 ########################################################################
 gjs_directory_defines =                                \
        -DGJS_TOP_SRCDIR=\"$(top_srcdir)\"              \
-       -DGJS_JS_DIR=\"$(gjsjsdir)\"                    \
+       -DGJS_TOP_BUILDDIR=\"$(top_builddir)\"          \
+       -DGJS_JS_DIR=\"$(gjsjsdir)\"                    \
        -DPKGLIBDIR=\"$(pkglibdir)\"
 
 ########################################################################
diff --git a/configure.ac b/configure.ac
index 3597371..9c49958 100644
--- a/configure.ac
+++ b/configure.ac
@@ -81,7 +81,7 @@ gjs_cairo_packages="cairo cairo-gobject $common_packages"
 gjs_gdbus_packages="gobject-2.0 >= glib_required_version gio-2.0"
 gjs_gtk_packages="gtk+-3.0"
 # gjs-tests links against everything
-gjstests_packages="$gjstests_packages $gjs_packages"
+gjstests_packages="gio-unix-2.0 $gjs_packages"
 
 PKG_CHECK_MODULES([GOBJECT], [gobject-2.0 >= glib_required_version])
 PKG_CHECK_MODULES([GJS], [$gjs_packages])
diff --git a/gjs/console.cpp b/gjs/console.cpp
index 50adca8..62cccbd 100644
--- a/gjs/console.cpp
+++ b/gjs/console.cpp
@@ -34,6 +34,8 @@ static char **coverage_prefixes = NULL;
 static char *coverage_output_path = NULL;
 static char *command = NULL;
 
+static const char *GJS_COVERAGE_CACHE_FILE_NAME = ".internal-gjs-coverage-cache";
+
 static GOptionEntry entries[] = {
     { "command", 'c', 0, G_OPTION_ARG_STRING, &command, "Program passed in as a string", "COMMAND" },
     { "coverage-prefix", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &coverage_prefixes, "Add the prefix PREFIX to 
the list of files to generate coverage info for", "PREFIX" },
@@ -121,8 +123,13 @@ main(int argc, char **argv)
         if (!coverage_output_path)
             g_error("--coverage-output is required when taking coverage statistics");
 
-        coverage = gjs_coverage_new((const gchar **) coverage_prefixes,
-                                    js_context);
+        char *path_to_cache_file = g_build_filename(coverage_output_path,
+                                                    GJS_COVERAGE_CACHE_FILE_NAME,
+                                                    NULL);
+        coverage = gjs_coverage_new_from_cache((const gchar **) coverage_prefixes,
+                                               js_context,
+                                               path_to_cache_file);
+        g_free(path_to_cache_file);
     }
 
     /* prepare command line arguments */
diff --git a/gjs/coverage-internal.h b/gjs/coverage-internal.h
index 681dc11..7d8629a 100644
--- a/gjs/coverage-internal.h
+++ b/gjs/coverage-internal.h
@@ -28,12 +28,32 @@
 #include "jsapi-util.h"
 #include "coverage.h"
 
+<<<<<<< HEAD
+=======
+GArray * gjs_fetch_statistics_from_js(GjsCoverage *coverage,
+                                      char        **covered_paths);
+GBytes * gjs_serialize_statistics(GjsCoverage *coverage);
+
+JSString * gjs_deserialize_cache_to_object(GjsCoverage *coverage,
+                                           GBytes      *cache_bytes);
+
+>>>>>>> be464c0... coverage: Serialize statistics to cache-path on cache misses
 gboolean gjs_run_script_in_coverage_compartment(GjsCoverage *coverage,
                                                 const char  *script);
 gboolean gjs_inject_value_into_coverage_compartment(GjsCoverage     *coverage,
                                                     JS::HandleValue value,
                                                     const char      *property);
 
+<<<<<<< HEAD
+=======
+gboolean gjs_get_path_mtime(const char *path,
+                            GTimeVal   *mtime);
+gchar * gjs_get_path_checksum(const char *path);
+
+gboolean gjs_write_cache_to_path(const char *path,
+                                 GBytes     *cache_bytes);
+
+>>>>>>> be464c0... coverage: Serialize statistics to cache-path on cache misses
 extern const char *GJS_COVERAGE_CACHE_FILE_NAME;
 
 #endif
diff --git a/gjs/coverage.cpp b/gjs/coverage.cpp
index 8d8b595..6891dba 100644
--- a/gjs/coverage.cpp
+++ b/gjs/coverage.cpp
@@ -23,6 +23,7 @@
 
 #include "gjs-module.h"
 #include "coverage.h"
+#include "coverage-internal.h"
 
 #include "util/error.h"
 
@@ -33,6 +34,8 @@ struct _GjsCoveragePrivate {
     gchar **prefixes;
     GjsContext *context;
     JSObject *coverage_statistics;
+
+    char *cache_path;
 };
 
 G_DEFINE_TYPE_WITH_PRIVATE(GjsCoverage,
@@ -43,6 +46,7 @@ enum {
     PROP_0,
     PROP_PREFIXES,
     PROP_CONTEXT,
+    PROP_CACHE,
     PROP_N
 };
 
@@ -275,6 +279,9 @@ static void
 copy_source_file_to_coverage_output(const char *source,
                                     const char *destination)
 {
+    /* Either source_file or destination_file could be a resource,
+     * so we must use g_file_new_for_commandline_arg to disambiguate
+     * between URI paths and filesystem paths. */
     GFile *source_file = g_file_new_for_commandline_arg(source);
     GFile *destination_file = g_file_new_for_commandline_arg(destination);
     GError *error = NULL;
@@ -479,15 +486,14 @@ convert_and_insert_unsigned_int(GArray    *array,
 }
 
 static GArray *
-get_executed_lines_for(GjsCoverage *coverage,
-                       jsval       *filename_value)
+get_executed_lines_for(JSContext        *context,
+                       JS::HandleObject  coverage_statistics,
+                       jsval            *filename_value)
 {
-    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
-    JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
     GArray *array = NULL;
     jsval rval;
 
-    if (!JS_CallFunctionName(context, priv->coverage_statistics, "getExecutedLinesFor", 1, filename_value, 
&rval)) {
+    if (!JS_CallFunctionName(context, coverage_statistics, "getExecutedLinesFor", 1, filename_value, &rval)) 
{
         gjs_log_exception(context);
         return NULL;
     }
@@ -582,15 +588,14 @@ convert_and_insert_function_decl(GArray    *array,
 }
 
 static GArray *
-get_functions_for(GjsCoverage *coverage,
-                  jsval       *filename_value)
+get_functions_for(JSContext        *context,
+                  JS::HandleObject  coverage_statistics,
+                  jsval            *filename_value)
 {
-    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
-    JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
     GArray *array = NULL;
     jsval rval;
 
-    if (!JS_CallFunctionName(context, priv->coverage_statistics, "getFunctionsFor", 1, filename_value, 
&rval)) {
+    if (!JS_CallFunctionName(context, coverage_statistics, "getFunctionsFor", 1, filename_value, &rval)) {
         gjs_log_exception(context);
         return NULL;
     }
@@ -743,15 +748,14 @@ convert_and_insert_branch_info(GArray    *array,
 }
 
 static GArray *
-get_branches_for(GjsCoverage *coverage,
-                 jsval       *filename_value)
+get_branches_for(JSContext        *context,
+                 JS::HandleObject  coverage_statistics,
+                 jsval            *filename_value)
 {
-    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
-    JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
     GArray *array = NULL;
     jsval rval;
 
-    if (!JS_CallFunctionName(context, priv->coverage_statistics, "getBranchesFor", 1, filename_value, 
&rval)) {
+    if (!JS_CallFunctionName(context, coverage_statistics, "getBranchesFor", 1, filename_value, &rval)) {
         gjs_log_exception(context);
         return NULL;
     }
@@ -764,44 +768,78 @@ get_branches_for(GjsCoverage *coverage,
     return array;
 }
 
+typedef struct _GjsCoverageFileStatistics {
+    char       *filename;
+    GArray     *lines;
+    GArray     *functions;
+    GArray     *branches;
+} GjsCoverageFileStatistics;
+
+static gboolean
+fetch_coverage_file_statistics_from_js(JSContext                 *context,
+                                       JS::HandleObject           coverage_statistics,
+                                       const char                *filename,
+                                       GjsCoverageFileStatistics *statistics)
+{
+    JSAutoCompartment compartment(context, coverage_statistics);
+    JSAutoRequest ar(context);
+
+    JSString *filename_jsstr = JS_NewStringCopyZ(context, filename);
+    jsval    filename_jsval = STRING_TO_JSVAL(filename_jsstr);
+
+    GArray *lines = get_executed_lines_for(context, coverage_statistics, &filename_jsval);
+    GArray *functions = get_functions_for(context, coverage_statistics, &filename_jsval);
+    GArray *branches = get_branches_for(context, coverage_statistics, &filename_jsval);
+
+    if (!lines || !functions || !branches)
+    {
+        g_clear_pointer(&lines, g_array_unref);
+        g_clear_pointer(&functions, g_array_unref);
+        g_clear_pointer(&branches, g_array_unref);
+        return FALSE;
+    }
+
+    statistics->filename = g_strdup(filename);
+    statistics->lines = lines;
+    statistics->functions = functions;
+    statistics->branches = branches;
+
+    return TRUE;
+}
+
 static void
-print_statistics_for_file(GjsCoverage   *coverage,
-                          char          *filename,
-                          const char    *output_directory,
-                          GOutputStream *ostream)
+gjs_coverage_statistics_file_statistics_clear(gpointer data)
 {
-    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    GjsCoverageFileStatistics *statistics = (GjsCoverageFileStatistics *) data;
+    g_free(statistics->filename);
+    g_array_unref(statistics->lines);
+    g_array_unref(statistics->functions);
+    g_array_unref(statistics->branches);
+}
 
+static void
+print_statistics_for_file(GjsCoverageFileStatistics *file_statistics,
+                          const char                *output_directory,
+                          GOutputStream             *ostream)
+{
     char *absolute_output_directory = get_absolute_path(output_directory);
     char *diverged_paths =
-        find_diverging_child_components(filename,
+        find_diverging_child_components(file_statistics->filename,
                                         absolute_output_directory);
     char *destination_filename = g_build_filename(absolute_output_directory,
                                                   diverged_paths,
                                                   NULL);
 
-    JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
-
-    JSString *filename_jsstr = JS_NewStringCopyZ(context, filename);
-    jsval    filename_jsval = STRING_TO_JSVAL(filename_jsstr);
-
-    GArray *lines = get_executed_lines_for(coverage, &filename_jsval);
-    GArray *functions = get_functions_for(coverage, &filename_jsval);
-    GArray *branches = get_branches_for(coverage, &filename_jsval);
-
-    if (!lines || !functions || !branches)
-        return;
-
-    copy_source_file_to_coverage_output(filename, destination_filename);
+    copy_source_file_to_coverage_output(file_statistics->filename, destination_filename);
 
     write_source_file_header(ostream, (const char *) destination_filename);
-    write_functions(ostream, functions);
+    write_functions(ostream, file_statistics->functions);
 
     unsigned int functions_hit_count = 0;
     unsigned int functions_found_count = 0;
 
     write_functions_hit_counts(ostream,
-                               functions,
+                               file_statistics->functions,
                                &functions_found_count,
                                &functions_hit_count);
     write_function_coverage(ostream,
@@ -812,7 +850,7 @@ print_statistics_for_file(GjsCoverage   *coverage,
     unsigned int branches_found_count = 0;
 
     write_branch_coverage(ostream,
-                          branches,
+                          file_statistics->branches,
                           &branches_found_count,
                           &branches_hit_count);
     write_branch_totals(ostream,
@@ -823,7 +861,7 @@ print_statistics_for_file(GjsCoverage   *coverage,
     unsigned int executable_lines_count = 0;
 
     write_line_coverage(ostream,
-                        lines,
+                        file_statistics->lines,
                         &lines_hit_count,
                         &executable_lines_count);
     write_line_totals(ostream,
@@ -831,10 +869,6 @@ print_statistics_for_file(GjsCoverage   *coverage,
                       executable_lines_count);
     write_end_of_record(ostream);
 
-    g_array_unref(lines);
-    g_array_unref(functions);
-    g_array_unref(branches);
-
     g_free(diverged_paths);
     g_free(destination_filename);
     g_free(absolute_output_directory);
@@ -886,6 +920,322 @@ get_covered_files(GjsCoverage *coverage)
     return NULL;
 }
 
+gboolean
+gjs_get_path_mtime(const char *path, GTimeVal *mtime)
+{
+    /* path could be a resource path, as the callers don't check
+     * if the path is a resource path, but rather if the mtime fetch
+     * operation succeeded. Use g_file_new_for_commandline_arg to handle
+     * that case. */
+    GError *error = NULL;
+    GFile *file = g_file_new_for_commandline_arg(path);
+    GFileInfo *info = g_file_query_info(file,
+                                        "time::modified,time::modified-usec",
+                                        G_FILE_QUERY_INFO_NONE,
+                                        NULL,
+                                        &error);
+
+    g_clear_object(&file);
+
+    if (!info) {
+        g_warning("Failed to get modification time of %s, "
+                  "falling back to checksum method for caching. Reason was: %s",
+                  path, error->message);
+        g_clear_object(&info);
+        return FALSE;
+    }
+
+    g_file_info_get_modification_time(info, mtime);
+    g_clear_object(&info);
+
+    /* For some URI types, eg, resources, the operation getting
+     * the mtime might succeed, but by default zero is returned.
+     *
+     * Check if that is the case for boht tv_sec and tv_usec and if
+     * so return FALSE. */
+    return !(mtime->tv_sec == 0 && mtime->tv_usec == 0);
+}
+
+static GBytes *
+read_all_bytes_from_path(const char *path)
+{
+    /* path could be a resource, so use g_file_new_for_commandline_arg. */
+    GFile *file = g_file_new_for_commandline_arg(path);
+
+    /* We have to use g_file_query_exists here since
+     * g_file_test(path, G_FILE_TEST_EXISTS) is implemented in terms
+     * of access(), which doesn't work with resource paths. */
+    if (!g_file_query_exists(file, NULL)) {
+        g_object_unref(file);
+        return NULL;
+    }
+
+    gsize len = 0;
+    gchar *data = NULL;
+
+    GError *error = NULL;
+
+    if (!g_file_load_contents(file,
+                              NULL,
+                              &data,
+                              &len,
+                              NULL,
+                              &error)) {
+        g_printerr("Unable to read bytes from: %s, reason was: %s\n",
+                   path, error->message);
+        g_clear_error(&error);
+        g_object_unref(file);
+        return NULL;
+    }
+
+    return g_bytes_new_take(data, len);
+}
+
+gchar *
+gjs_get_path_checksum(const char *path)
+{
+    GBytes *data = read_all_bytes_from_path(path);
+
+    if (!data)
+        return NULL;
+
+    gchar *checksum = g_compute_checksum_for_bytes(G_CHECKSUM_SHA512, data);
+
+    g_bytes_unref(data);
+    return checksum;
+}
+
+static unsigned int COVERAGE_STATISTICS_CACHE_MAGIC = 0xC0432463;
+
+/* The binary data for the cache has the following structure:
+ *
+ * {
+ *     array [ tuple {
+ *         string filename;
+ *         string? checksum;
+ *         tuple? {
+ *             mtime_sec;
+ *             mtime_usec;
+ *         }
+ *         array [
+ *             int line;
+ *         ] executable lines;
+ *         array [ tuple {
+ *             int branch_point;
+ *             array [
+ *                 int line;
+ *             ] exits;
+ *         } branch_info ] branches;
+ *         array [ tuple {
+ *             int line;
+ *             string key;
+ *         } function ] functions;
+ *     } file ] files;
+ */
+const char *COVERAGE_STATISTICS_CACHE_BINARY_DATA_TYPE = "a(sm(xx)msaia(iai)a(is))";
+
+GBytes *
+gjs_serialize_statistics(GjsCoverage *coverage)
+{
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context);
+    JSRuntime *js_runtime = JS_GetRuntime(js_context);
+
+    JSAutoRequest ar(js_context);
+    JSAutoCompartment ac(js_context,
+                         JS_GetGlobalForObject(js_context,
+                                               priv->coverage_statistics));
+
+    JS::RootedValue string_value_return(js_runtime);
+
+    if (!JS_CallFunctionName(js_context,
+                             priv->coverage_statistics,
+                             "stringify",
+                             0,
+                             NULL,
+                             &(string_value_return.get()))) {
+        gjs_log_exception(js_context);
+        return NULL;
+    }
+
+    if (!string_value_return.isString())
+        return NULL;
+
+    /* Free'd by g_bytes_new_take */
+    char *statistics_as_json_string = NULL;
+
+    if (!gjs_string_to_utf8(js_context,
+                            string_value_return.get(),
+                            &statistics_as_json_string)) {
+        gjs_log_exception(js_context);
+        return NULL;
+    }
+
+    return g_bytes_new_take((guint8 *) statistics_as_json_string,
+                            strlen(statistics_as_json_string));
+}
+
+static JSObject *
+gjs_get_generic_object_constructor(JSContext        *context,
+                                   JSRuntime        *runtime,
+                                   JS::HandleObject  global_object)
+{
+    JSAutoRequest ar(context);
+    JSAutoCompartment ac(context, global_object);
+
+    jsval object_constructor_value;
+    if (!JS_GetProperty(context, global_object, "Object", &object_constructor_value) ||
+        !JSVAL_IS_OBJECT(object_constructor_value))
+        g_assert_not_reached();
+
+    return JSVAL_TO_OBJECT(object_constructor_value);
+}
+
+static JSString *
+gjs_deserialize_cache_to_object_for_compartment(JSContext        *context,
+                                                JS::HandleObject global_object,
+                                                GBytes           *cache_data)
+{
+    JSAutoRequest ar(context);
+    JSAutoCompartment ac(context,
+                         JS_GetGlobalForObject(context,
+                                               global_object));
+
+    gsize len = 0;
+    gpointer string = g_bytes_unref_to_data(g_bytes_ref(cache_data),
+                                            &len);
+
+    return JS_NewStringCopyN(context, (const char *) string, len);
+}
+
+JSString *
+gjs_deserialize_cache_to_object(GjsCoverage *coverage,
+                                GBytes      *cache_data)
+{
+    /* Deserialize into an object with the following structure:
+     *
+     * object = {
+     *     'filename': {
+     *         contents: (file contents),
+     *         nLines: (number of lines in file),
+     *         lines: Number[nLines + 1],
+     *         branches: Array for n_branches of {
+     *             point: branch_point,
+     *             exits: Number[nLines + 1]
+     *         },
+     *         functions: Array for n_functions of {
+     *             key: function_name,r
+     *             line: line
+     *         }
+     * }
+     */
+
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
+    JSAutoRequest ar(context);
+    JSAutoCompartment ac(context, priv->coverage_statistics);
+    JS::RootedObject global_object(JS_GetRuntime(context),
+                                   JS_GetGlobalForObject(context, priv->coverage_statistics));
+    return gjs_deserialize_cache_to_object_for_compartment(context, global_object, cache_data);
+}
+
+GArray *
+gjs_fetch_statistics_from_js(GjsCoverage *coverage,
+                             gchar       **coverage_files)
+{
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    JSContext          *js_context = (JSContext *) gjs_context_get_native_context(priv->context);
+
+    GArray *file_statistics_array = g_array_new(FALSE,
+                                                FALSE,
+                                                sizeof(GjsCoverageFileStatistics));
+    g_array_set_clear_func(file_statistics_array,
+                           gjs_coverage_statistics_file_statistics_clear);
+
+    JS::RootedObject rooted_coverage_statistics(JS_GetRuntime(js_context),
+                                                priv->coverage_statistics);
+
+    char                      **file_iter = coverage_files;
+    GjsCoverageFileStatistics *statistics_iter = (GjsCoverageFileStatistics *) file_statistics_array->data;
+    while (*file_iter) {
+        GjsCoverageFileStatistics statistics;
+        if (fetch_coverage_file_statistics_from_js(js_context,
+                                                   rooted_coverage_statistics,
+                                                   *file_iter,
+                                                   &statistics))
+            g_array_append_val(file_statistics_array, statistics);
+        else
+            g_warning("Couldn't fetch statistics for %s", *file_iter);
+
+        ++file_iter;
+    }
+
+    return file_statistics_array;
+}
+
+gboolean
+gjs_write_cache_to_path(const char *path,
+                        GBytes     *cache)
+{
+    GFile *file = g_file_new_for_commandline_arg(path);
+    gsize cache_len = 0;
+    char *cache_data = (char *) g_bytes_get_data(cache, &cache_len);
+    GError *error = NULL;
+
+    if (!g_file_replace_contents(file,
+                                 cache_data,
+                                 cache_len,
+                                 NULL,
+                                 FALSE,
+                                 G_FILE_CREATE_NONE,
+                                 NULL,
+                                 NULL,
+                                 &error)) {
+        g_object_unref(file);
+        g_warning("Failed to write all bytes to %s, reason was: %s\n",
+                  path, error->message);
+        g_warning("Will remove this file to prevent inconsistent cache "
+                  "reads next time.");
+        g_clear_error(&error);
+        if (!g_file_delete(file, NULL, &error)) {
+            g_assert(error != NULL);
+            g_critical("Deleting %s failed because %s! You will need to "
+                       "delete it manually before running the coverage "
+                       "mode again.");
+            g_clear_error(&error);
+        }
+
+        return FALSE;
+    }
+
+    g_object_unref(file);
+
+    return TRUE;
+}
+
+static JSBool
+coverage_statistics_has_stale_cache(GjsCoverage *coverage)
+{
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    JSContext          *js_context = (JSContext *) gjs_context_get_native_context(priv->context);
+
+    JSAutoRequest ar(js_context);
+    JSAutoCompartment ac(js_context, priv->coverage_statistics);
+
+    jsval stale_cache_value;
+    if (!JS_CallFunctionName(js_context,
+                             priv->coverage_statistics,
+                             "staleCache",
+                             0,
+                             NULL,
+                             &stale_cache_value)) {
+        gjs_log_exception(js_context);
+        g_error("Failed to call into javascript to get stale cache value. This is a bug");
+    }
+
+    return JSVAL_TO_BOOLEAN(stale_cache_value);
+}
+
 void
 gjs_coverage_write_statistics(GjsCoverage *coverage,
                               const char  *output_directory)
@@ -912,13 +1262,37 @@ gjs_coverage_write_statistics(GjsCoverage *coverage,
                                          NULL,
                                          &error));
 
-    char **files = get_covered_files(coverage);
-    if (files) {
-        for (char **file_iter = files; *file_iter; file_iter++)
-            print_statistics_for_file(coverage, *file_iter, output_directory, ostream);
-        g_strfreev(files);
+    char **executed_coverage_files = get_covered_files(coverage);
+    GArray *file_statistics_array = gjs_fetch_statistics_from_js(coverage,
+                                                                 executed_coverage_files);
+
+    for (size_t i = 0; i < file_statistics_array->len; ++i)
+    {
+        GjsCoverageFileStatistics *statistics = &(g_array_index(file_statistics_array, 
GjsCoverageFileStatistics, i));
+
+        /* Only print statistics if the file was actually executed */
+        for (char **iter = executed_coverage_files; *iter; ++iter) {
+            if (g_strcmp0(*iter, statistics->filename) == 0) {
+                print_statistics_for_file(statistics, output_directory, ostream);
+
+                /* Inner loop */
+                break;
+            }
+        }
+    }
+
+    g_strfreev(executed_coverage_files);
+
+    const gboolean has_cache_path = priv->cache_path != NULL;
+    const gboolean cache_is_stale = coverage_statistics_has_stale_cache(coverage);
+
+    if (has_cache_path && cache_is_stale) {
+        GBytes *cache_data = gjs_serialize_statistics(coverage);
+        gjs_write_cache_to_path(priv->cache_path, cache_data);
+        g_bytes_unref(cache_data);
     }
 
+    g_array_unref(file_statistics_array);
     g_object_unref(ostream);
     g_object_unref(output_file);
 }
@@ -969,6 +1343,7 @@ gjs_context_eval_file_in_compartment(GjsContext *context,
                              compartment_object,
                              script, script_len, filename,
                              &return_value)) {
+        g_free(script);
         gjs_log_exception(js_context);
         g_free(script);
         g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED, "Failed to evaluate %s", filename);
@@ -1024,6 +1399,97 @@ coverage_warning(JSContext *context,
     return JS_TRUE;
 }
 
+static char *
+get_filename_from_filename_as_js_string(JSContext    *context,
+                                        JS::CallArgs &args) {
+    char *filename = NULL;
+
+    if (!gjs_parse_call_args(context, "getFileContents", "s", args,
+                             "filename", &filename))
+        return NULL;
+
+    return filename;
+}
+
+static GFile *
+get_file_from_filename_as_js_string(JSContext    *context,
+                                    JS::CallArgs &args) {
+    char *filename = get_filename_from_filename_as_js_string(context, args);
+
+    if (!filename) {
+        gjs_throw(context, "Failed to parse arguments for filename");
+        return NULL;
+    }
+
+    GFile *file = g_file_new_for_commandline_arg(filename);
+
+    g_free(filename);
+    return file;
+}
+
+static JSBool
+coverage_get_file_modification_time(JSContext *context,
+                                    unsigned  argc,
+                                    jsval     *vp)
+{
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    JSRuntime    *runtime = JS_GetRuntime(context);
+    GTimeVal mtime;
+    JSBool ret = JS_FALSE;
+    char *filename = get_filename_from_filename_as_js_string(context, args);
+
+    if (!filename)
+        goto out;
+
+    if (gjs_get_path_mtime(filename, &mtime)) {
+        JS::RootedObject mtime_values_array(runtime,
+                                            JS_NewArrayObject(context, 0, NULL));
+        if (!JS_DefineElement(context, mtime_values_array, 0, JS::Int32Value(mtime.tv_sec), NULL, NULL, 0))
+            goto out;
+        if (!JS_DefineElement(context, mtime_values_array, 1, JS::Int32Value(mtime.tv_usec), NULL, NULL, 0))
+            goto out;
+        args.rval().setObject(*(mtime_values_array.get()));
+    } else {
+        args.rval().setNull();
+    }
+
+    ret = JS_TRUE;
+
+out:
+    g_free(filename);
+    return ret;
+}
+
+static JSBool
+coverage_get_file_checksum(JSContext *context,
+                           unsigned  argc,
+                           jsval     *vp)
+{
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    JSRuntime    *runtime = JS_GetRuntime(context);
+    GTimeVal mtime;
+    JSBool ret = JS_FALSE;
+    char *filename = get_filename_from_filename_as_js_string(context, args);
+
+    if (!filename)
+        return JS_FALSE;
+
+    char *checksum = gjs_get_path_checksum(filename);
+
+    if (!checksum) {
+        gjs_throw(context, "Failed to read %s and get its checksum", filename);
+        return JS_FALSE;
+    }
+
+    JS::RootedString rooted_checksum(runtime, JS_NewStringCopyZ(context,
+                                                                checksum));
+    args.rval().setString(rooted_checksum);
+
+    g_free(filename);
+    g_free(checksum);
+    return JS_TRUE;
+}
+
 static JSBool
 coverage_get_file_contents(JSContext *context,
                            unsigned   argc,
@@ -1067,9 +1533,11 @@ coverage_get_file_contents(JSContext *context,
 }
 
 static JSFunctionSpec coverage_funcs[] = {
-    { "warning", JSOP_WRAPPER (coverage_warning), 1, GJS_MODULE_PROP_FLAGS },
-    { "getFileContents", JSOP_WRAPPER (coverage_get_file_contents), 1, GJS_MODULE_PROP_FLAGS },
-    { NULL },
+    { "warning", JSOP_WRAPPER(coverage_warning), 1, GJS_MODULE_PROP_FLAGS },
+    { "getFileContents", JSOP_WRAPPER(coverage_get_file_contents), 1, GJS_MODULE_PROP_FLAGS },
+    { "getFileModificationTime", JSOP_WRAPPER(coverage_get_file_modification_time), 1, GJS_MODULE_PROP_FLAGS 
},
+    { "getFileChecksum", JSOP_WRAPPER(coverage_get_file_checksum), 1, GJS_MODULE_PROP_FLAGS },
+    { NULL }
 };
 
 static void
@@ -1157,6 +1625,7 @@ bootstrap_coverage(GjsCoverage *coverage)
 {
     static const char  *coverage_script = "resource:///org/gnome/gjs/modules/coverage.js";
     GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    GBytes             *cache_bytes = NULL;
     GError             *error = NULL;
 
     JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
@@ -1165,7 +1634,8 @@ bootstrap_coverage(GjsCoverage *coverage)
     JSObject *debuggee = JS_GetGlobalObject(context);
     JS::CompartmentOptions options;
     options.setVersion(JSVERSION_LATEST);
-    JSObject *debugger_compartment = JS_NewGlobalObject(context, &coverage_global_class, NULL, options);
+    JS::RootedObject debugger_compartment(JS_GetRuntime(context),
+                                          JS_NewGlobalObject(context, &coverage_global_class, NULL, 
options));
 
     {
         JSAutoCompartment compartment(context, debugger_compartment);
@@ -1196,6 +1666,23 @@ bootstrap_coverage(GjsCoverage *coverage)
             return FALSE;
         }
 
+        JS::RootedObject wrapped_importer(JS_GetRuntime(context),
+                                          gjs_wrap_root_importer_in_compartment(context,
+                                                                                debugger_compartment));;
+
+        if (!wrapped_importer) {
+            gjs_throw(context, "Failed to wrap root importer in debugger compartment");
+            return FALSE;
+        }
+
+        /* Now copy the global root importer (which we just created,
+         * if it didn't exist) to our global object
+         */
+        if (!gjs_define_root_importer_object(context, debugger_compartment, wrapped_importer)) {
+            gjs_throw(context, "Failed to set 'imports' on debugger compartment");
+            return FALSE;
+        }
+
         if (!JS_DefineFunctions(context, debugger_compartment, &coverage_funcs[0]))
             g_error("Failed to init coverage");
 
@@ -1212,18 +1699,37 @@ bootstrap_coverage(GjsCoverage *coverage)
             return FALSE;
         }
 
+        /* Create value for holding the cache. This will be undefined if
+         * the cache does not exist, otherwise it will be an object set
+         * to the value of the cache */
+        JS::RootedValue cache_value(JS_GetRuntime(context));
+
+        if (priv->cache_path)
+            cache_bytes = read_all_bytes_from_path(priv->cache_path);
+
+        if (cache_bytes) {
+            JSString *cache_object = gjs_deserialize_cache_to_object_for_compartment(context,
+                                                                                     debugger_compartment,
+                                                                                     cache_bytes);
+            cache_value.set(JS::StringValue(cache_object));
+            g_bytes_unref(cache_bytes);
+        } else {
+            cache_value.set(JS::UndefinedValue());
+        }
+
         JSObject *coverage_statistics_constructor = JSVAL_TO_OBJECT(coverage_statistics_prototype_value);
 
         /* Now create the array to pass the desired prefixes over */
         JSObject *prefixes = gjs_build_string_array(context, -1, priv->prefixes);
 
         jsval coverage_statistics_constructor_arguments[] = {
-            OBJECT_TO_JSVAL(prefixes)
+            OBJECT_TO_JSVAL(prefixes),
+            cache_value.get()
         };
 
         JSObject *coverage_statistics = JS_New(context,
                                                coverage_statistics_constructor,
-                                               1,
+                                               2,
                                                coverage_statistics_constructor_arguments);
 
         if (!coverage_statistics) {
@@ -1279,6 +1785,9 @@ gjs_coverage_set_property(GObject      *object,
     case PROP_CONTEXT:
         priv->context = GJS_CONTEXT(g_value_dup_object(value));
         break;
+    case PROP_CACHE:
+        priv->cache_path = g_value_dup_string(value);
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -1286,18 +1795,45 @@ gjs_coverage_set_property(GObject      *object,
 }
 
 static void
-gjs_coverage_dispose(GObject *object)
+gjs_clear_js_side_statistics_from_coverage_object(GjsCoverage *coverage)
 {
-    GjsCoverage *coverage = GJS_DEBUG_COVERAGE (object);
     GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
 
-    /* Remove tracer before disposing the context */
-    JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context);
-    JS_RemoveExtraGCRootsTracer(JS_GetRuntime(js_context),
-                                coverage_statistics_tracer,
-                                coverage);
+    if (priv->coverage_statistics) {
+        /* Remove tracer before disposing the context */
+        JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context);
+        JSAutoRequest ar(js_context);
+        JSAutoCompartment ac(js_context, priv->coverage_statistics);
+        JS::RootedValue rval(JS_GetRuntime(js_context));
+        if (!JS_CallFunctionName(js_context,
+                                 priv->coverage_statistics,
+                                 "deactivate",
+                                 0,
+                                 NULL,
+                                 rval.address())) {
+            gjs_log_exception(js_context);
+            g_error("Failed to deactivate debugger - this is a fatal error");
+        }
 
+        /* Remove GC roots trace after we've decomissioned the object
+         * and no longer need it to be traced here. */
+        JS_RemoveExtraGCRootsTracer(JS_GetRuntime(js_context),
+                                    coverage_statistics_tracer,
+                                    coverage);
+
+        priv->coverage_statistics = NULL;
+    }
+}
+
+static void
+gjs_coverage_dispose(GObject *object)
+{
+    GjsCoverage *coverage = GJS_DEBUG_COVERAGE(object);
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
 
+    /* Decomission objects inside of the JSContext before
+     * disposing of the context */
+    gjs_clear_js_side_statistics_from_coverage_object(coverage);
     g_clear_object(&priv->context);
 
     G_OBJECT_CLASS(gjs_coverage_parent_class)->dispose(object);
@@ -1310,6 +1846,7 @@ gjs_coverage_finalize (GObject *object)
     GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
 
     g_strfreev(priv->prefixes);
+    g_clear_pointer(&priv->cache_path, (GDestroyNotify) g_free);
 
     G_OBJECT_CLASS(gjs_coverage_parent_class)->finalize(object);
 }
@@ -1334,6 +1871,11 @@ gjs_coverage_class_init (GjsCoverageClass *klass)
                                                    "A context to gather coverage stats for",
                                                    GJS_TYPE_CONTEXT,
                                                    (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_WRITABLE));
+    properties[PROP_CACHE] = g_param_spec_string("cache",
+                                                 "Cache",
+                                                 "Path to a file containing a cache to preload ASTs from",
+                                                 NULL,
+                                                 (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE));
 
     g_object_class_install_properties(object_class,
                                       PROP_N,
@@ -1359,3 +1901,30 @@ gjs_coverage_new (const char **prefixes,
 
     return coverage;
 }
+
+/**
+ * gjs_coverage_new_from_cache:
+ * Creates a new GjsCoverage object, but uses @cache_path to pre-fill the AST information for
+ * the specified scripts in coverage_paths, so long as the data in the cache has the same
+ * mtime as those scripts.
+ *
+ * @coverage_prefixes: (transfer none): A null-terminated strv of prefixes of files to perform coverage on
+ * @context: (transfer full): A #GjsContext object.
+ * @cache_path: A path to a file containing a serialized cache.
+ *
+ * Returns: A #GjsCoverage object
+ */
+GjsCoverage *
+gjs_coverage_new_from_cache(const char **coverage_prefixes,
+                            GjsContext *context,
+                            const char *cache_path)
+{
+    GjsCoverage *coverage =
+        GJS_DEBUG_COVERAGE(g_object_new(GJS_TYPE_DEBUG_COVERAGE,
+                                        "prefixes", coverage_prefixes,
+                                        "context", context,
+                                        "cache", cache_path,
+                                        NULL));
+
+    return coverage;
+}
diff --git a/gjs/coverage.h b/gjs/coverage.h
index c013e4f..ed08eb4 100644
--- a/gjs/coverage.h
+++ b/gjs/coverage.h
@@ -81,6 +81,10 @@ void gjs_coverage_write_statistics(GjsCoverage *coverage,
 GjsCoverage * gjs_coverage_new(const char   **coverage_prefixes,
                                GjsContext    *coverage_context);
 
+GjsCoverage * gjs_coverage_new_from_cache(const char **coverage_prefixes,
+                                          GjsContext *context,
+                                          const char *cache_path);
+
 G_END_DECLS
 
 #endif
diff --git a/installed-tests/gjs-unit.cpp b/installed-tests/gjs-unit.cpp
index 512aa55..3f93364 100644
--- a/installed-tests/gjs-unit.cpp
+++ b/installed-tests/gjs-unit.cpp
@@ -77,7 +77,13 @@ setup(GjsTestJSFixture *fix,
             g_error("GJS_UNIT_COVERAGE_OUTPUT is required when using GJS_UNIT_COVERAGE_PREFIX");
         }
 
-        fix->coverage = gjs_coverage_new(coverage_prefixes, fix->context);
+        char *path_to_cache_file = g_build_filename(data->coverage_output_path,
+                                                    ".internal-coverage-cache",
+                                                    NULL);
+        fix->coverage = gjs_coverage_new_from_cache((const char **) coverage_prefixes,
+                                                    fix->context,
+                                                    path_to_cache_file);
+        g_free(path_to_cache_file);
     }
 }
 
diff --git a/installed-tests/js/testCoverage.js b/installed-tests/js/testCoverage.js
index 26a8931..c81ec48 100644
--- a/installed-tests/js/testCoverage.js
+++ b/installed-tests/js/testCoverage.js
@@ -275,8 +275,8 @@ function testFunctionsFoundNoTrailingNewline() {
                                                  "function f2() {}\n");
     assertArrayEquals(foundFuncs,
                       [
-                          { name: "f1", line: 1, n_params: 0 },
-                          { name: "f2", line: 2, n_params: 0 }
+                          { key: "f1:1:0", line: 1, n_params: 0 },
+                          { key: "f2:2:0", line: 2, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -288,9 +288,9 @@ function testFunctionsFoundForDeclarations() {
                                     "function f3() {}\n");
     assertArrayEquals(foundFunctionDeclarations,
                       [
-                          { name: "f1", line: 1, n_params: 0 },
-                          { name: "f2", line: 2, n_params: 0 },
-                          { name: "f3", line: 3, n_params: 0 }
+                          { key: "f1:1:0", line: 1, n_params: 0 },
+                          { key: "f2:2:0", line: 2, n_params: 0 },
+                          { key: "f3:3:0", line: 3, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -305,9 +305,9 @@ function testFunctionsFoundForNestedFunctions() {
                                     "}\n");
     assertArrayEquals(foundFunctions,
                       [
-                          { name: "f1", line: 1, n_params: 0 },
-                          { name: null, line: 2, n_params: 0 },
-                          { name: null, line: 3, n_params: 0 }
+                          { key: "f1:1:0", line: 1, n_params: 0 },
+                          { key: "(anonymous):2:0", line: 2, n_params: 0 },
+                          { key: "(anonymous):3:0", line: 3, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -323,9 +323,9 @@ function testFunctionsFoundOnSameLineButDifferentiatedOnArgs() {
                                     "}");
     assertArrayEquals(foundFunctionsOnSameLine,
                       [
-                          { name: "f1", line: 1, n_params: 0 },
-                          { name: null, line: 1, n_params: 1 },
-                          { name: null, line: 1, n_params: 2 }
+                          { key: "f1:1:0", line: 1, n_params: 0 },
+                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                          { key: "(anonymous):1:2", line: 1, n_params: 2 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -336,7 +336,7 @@ function testFunctionsInsideArrayExpression() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 },
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 },
                       ],
                       functionDeclarationsEqual);
 }
@@ -347,7 +347,7 @@ function testFunctionsInsideArrowExpression() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -359,8 +359,8 @@ function testFunctionsInsideSequence() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 1 },
-                          { name: null, line: 1, n_params: 2 },
+                          { key: "(anonymous):1:0", line: 1, n_params: 1 },
+                          { key: "(anonymous):1:2", line: 1, n_params: 2 },
                       ],
                       functionDeclarationsEqual);
 }
@@ -371,7 +371,7 @@ function testFunctionsInsideUnaryExpression() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 },
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 },
                       ],
                       functionDeclarationsEqual);
 }
@@ -383,8 +383,8 @@ function testFunctionsInsideBinaryExpression() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 1 },
-                          { name: null, line: 1, n_params: 2 }
+                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                          { key: "(anonymous):1:2", line: 1, n_params: 2 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -395,7 +395,7 @@ function testFunctionsInsideAssignmentExpression() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -407,7 +407,7 @@ function testFunctionsInsideUpdateExpression() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 2, n_params: 0 }
+                          { key: "(anonymous):2:0", line: 2, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -419,8 +419,8 @@ function testFunctionsInsideIfConditions() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 1 },
-                          { name: null, line: 1, n_params: 2 }
+                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                          { key: "(anonymous):1:2", line: 1, n_params: 2 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -432,8 +432,8 @@ function testFunctionsInsideWhileConditions() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 1 },
-                          { name: null, line: 1, n_params: 2 }
+                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                          { key: "(anonymous):1:2", line: 1, n_params: 2 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -444,7 +444,7 @@ function testFunctionsInsideForInitializer() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -458,7 +458,7 @@ function testFunctionsInsideForLetInitializer() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -469,7 +469,7 @@ function testFunctionsInsideForVarInitializer() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -480,7 +480,7 @@ function testFunctionsInsideForCondition() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -491,7 +491,7 @@ function testFunctionsInsideForIncrement() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -502,7 +502,7 @@ function testFunctionsInsideForInObject() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -513,7 +513,7 @@ function testFunctionsInsideForEachInObject() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -524,7 +524,7 @@ function testFunctionsInsideForOfObject() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -535,7 +535,7 @@ function testFunctionsUsedAsObjectFound() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -546,7 +546,7 @@ function testFunctionsUsedAsObjectDynamicProp() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -558,8 +558,8 @@ function testFunctionsOnEitherSideOfLogicalExpression() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 1, n_params: 1 },
-                          { name: null, line: 1, n_params: 2 }
+                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                          { key: "(anonymous):1:2", line: 1, n_params: 2 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -572,8 +572,8 @@ function testFunctionsOnEitherSideOfConditionalExpression() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 2, n_params: 1 },
-                          { name: null, line: 2, n_params: 2 }
+                          { key: "(anonymous):2:1", line: 2, n_params: 1 },
+                          { key: "(anonymous):2:1", line: 2, n_params: 2 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -584,8 +584,8 @@ function testFunctionsYielded() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: "a", line: 1, n_params: 0 },
-                          { name: null, line: 1, n_params: 0 }
+                          { key: "a:1:0", line: 1, n_params: 0 },
+                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -597,7 +597,7 @@ function testFunctionsInArrayComprehensionBody() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 2, n_params: 0 }
+                          { key: "(anonymous):2:0", line: 2, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -609,7 +609,7 @@ function testFunctionsInArrayComprehensionBlock() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 2, n_params: 0 }
+                          { key: "(anonymous):2:0", line: 2, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -622,7 +622,7 @@ function testFunctionsInArrayComprehensionFilter() {
 
     assertArrayEquals(foundFunctions,
                       [
-                          { name: null, line: 2, n_params: 0 }
+                          { key: "(anonymous):2:0", line: 2, n_params: 0 }
                       ],
                       functionDeclarationsEqual);
 }
@@ -908,9 +908,17 @@ function testHitIsAlwaysInitiallyFalse() {
 function testFunctionForKeyFromFunctionWithNameMatchesSchema() {
     let expectedFunctionKey = 'f:1:2';
     let functionKeyForFunctionName =
-        Coverage._getFunctionKeyFromReflectedFunction({ name: 'f',
-                                                        line: 1,
-                                                        n_params: 2 });
+        Coverage._getFunctionKeyFromReflectedFunction({
+            id: {
+                name: 'f'
+            },
+            loc: {
+              start: {
+                  line: 1
+              }
+            },
+            params: ['a', 'b']
+        });
 
     JSUnit.assertEquals(expectedFunctionKey, functionKeyForFunctionName);
 }
@@ -918,9 +926,15 @@ function testFunctionForKeyFromFunctionWithNameMatchesSchema() {
 function testFunctionKeyFromFunctionWithoutNameIsAnonymous() {
     let expectedFunctionKey = '(anonymous):2:3';
     let functionKeyForAnonymousFunction =
-        Coverage._getFunctionKeyFromReflectedFunction({ name: null,
-                                                        line: 2,
-                                                        n_params: 3 });
+        Coverage._getFunctionKeyFromReflectedFunction({
+            id: null,
+            loc: {
+              start: {
+                  line: 2
+              }
+            },
+            params: ['a', 'b', 'c']
+        });
 
     JSUnit.assertEquals(expectedFunctionKey, functionKeyForAnonymousFunction);
 }
@@ -928,14 +942,28 @@ function testFunctionKeyFromFunctionWithoutNameIsAnonymous() {
 
 
 function testFunctionCounterMapReturnedForFunctionKeys() {
-    let func = {
-        name: 'name',
-        line: 1,
-        n_params: 0
+    let ast = {
+        body: [{
+            type: 'FunctionDeclaration',
+            id: {
+                name: 'name'
+            },
+            loc: {
+              start: {
+                  line: 1
+              }
+            },
+            params: [],
+            body: {
+                type: 'BlockStatement',
+                body: []
+            }
+        }]
     };
 
-    let functionKey = Coverage._getFunctionKeyFromReflectedFunction(func);
-    let functionCounters = Coverage._functionsToFunctionCounters([func]);
+    let detectedFunctions = Coverage.functionsForAST(ast);
+    let functionKey = Coverage._getFunctionKeyFromReflectedFunction(ast.body[0]);
+    let functionCounters = Coverage._functionsToFunctionCounters(detectedFunctions);
 
     JSUnit.assertEquals(0, functionCounters[functionKey].hitCount);
 }
@@ -965,16 +993,29 @@ function testIncrementFunctionCountersForFunctionOnSameExecutionStartLine() {
 }
 
 function testIncrementFunctionCountersForFunctionOnEarlierStartLine() {
-    let functions = [
-        {
-            name: 'name',
-            line: 1,
-            n_params: 0
-        }
-    ];
-    let functionKey = Coverage._getFunctionKeyFromReflectedFunction(functions[0]);
-    let knownFunctionsArray = Coverage._populateKnownFunctions(functions, 3);
-    let functionCounters = Coverage._functionsToFunctionCounters(functions);
+    let ast = {
+        body: [{
+            type: 'FunctionDeclaration',
+            id: {
+                name: 'name'
+            },
+            loc: {
+              start: {
+                  line: 1
+              }
+            },
+            params: [],
+            body: {
+                type: 'BlockStatement',
+                body: []
+            }
+        }]
+    };
+
+    let detectedFunctions = Coverage.functionsForAST(ast);
+    let functionKey = Coverage._getFunctionKeyFromReflectedFunction(ast.body[0]);
+    let knownFunctionsArray = Coverage._populateKnownFunctions(detectedFunctions, 3);
+    let functionCounters = Coverage._functionsToFunctionCounters(detectedFunctions);
 
     /* We're entering at line two, but the function definition was actually
      * at line one */
@@ -984,16 +1025,28 @@ function testIncrementFunctionCountersForFunctionOnEarlierStartLine() {
 }
 
 function testIncrementFunctionCountersThrowsErrorOnUnexpectedFunction() {
-    let functions = [
-        {
-            name: 'name',
-            line: 1,
-            n_params: 0
-        }
-    ];
-    let functionKey = Coverage._getFunctionKeyFromReflectedFunction(functions[0]);
-    let knownFunctionsArray = Coverage._populateKnownFunctions(functions, 3);
-    let functionCounters = Coverage._functionsToFunctionCounters(functions);
+    let ast = {
+        body: [{
+            type: 'FunctionDeclaration',
+            id: {
+                name: 'name'
+            },
+            loc: {
+              start: {
+                  line: 1
+              }
+            },
+            params: [],
+            body: {
+                type: 'BlockStatement',
+                body: []
+            }
+        }]
+    };
+    let detectedFunctions = Coverage.functionsForAST(ast);
+    let functionKey = Coverage._getFunctionKeyFromReflectedFunction(ast.body[0]);
+    let knownFunctionsArray = Coverage._populateKnownFunctions(detectedFunctions, 3);
+    let functionCounters = Coverage._functionsToFunctionCounters(detectedFunctions);
 
     /* We're entering at line two, but the function definition was actually
      * at line one */
@@ -1120,17 +1173,37 @@ function testConvertFunctionCountersToArrayIsSorted() {
 }
 
 const MockFiles = {
-    'filename': "let f = function() { return 1; };"
+    'filename': "function f() {\n" +
+                "    return 1;\n" +
+                "}\n" +
+                "if (f())\n" +
+                "    f = 0;\n" +
+                "\n",
+    'uncached': "function f() {\n" +
+                "    return 1;\n" +
+                "}\n"
 };
 
-const MockFilenames = Object.keys(MockFiles);
+const MockFilenames = (function() {
+    let keys = Object.keys(MockFiles);
+    keys.push('nonexistent');
+    return keys;
+})();
 
 Coverage.getFileContents = function(filename) {
     if (MockFiles[filename])
         return MockFiles[filename];
-    throw new Error("Non existent");
+    return undefined;
 };
 
+Coverage.getFileChecksum = function(filename) {
+    return "abcd";
+}
+
+Coverage.getFileModificationTime = function(filename) {
+    return [1, 2];
+}
+
 function testCoverageStatisticsContainerFetchesValidStatisticsForFile() {
     let container = new Coverage.CoverageStatisticsContainer(MockFilenames);
 
@@ -1149,4 +1222,129 @@ function testCoverageStatisticsContainerThrowsForNonExistingFile() {
     });
 }
 
+const MockCache = '{ \
+    "filename": { \
+        "mtime": [1, 2], \
+        "checksum": null, \
+        "lines": [2, 4, 5], \
+        "branches": [ \
+            { \
+                "point": 4, \
+                "exits": [5] \
+            } \
+        ], \
+        "functions": [ \
+            { \
+                "key": "f:1:0", \
+                "line": 1 \
+            } \
+        ] \
+    } \
+}';
+
+/* A simple wrapper to monkey-patch object[functionProperty] with
+ * a wrapper that checks to see if it was called. Returns true
+ * if the function was called at all */
+function _checkIfCalledWhilst(object, functionProperty, clientCode) {
+    let original = object[functionProperty];
+    let called = false;
+
+    object[functionProperty] = function() {
+        called = true;
+        return original.apply(this, arguments);
+    };
+
+    clientCode();
+
+    object[functionProperty] = original;
+    return called;
+}
+
+function testCoverageCountersFetchedFromCache() {
+    let called = _checkIfCalledWhilst(Coverage,
+                                      '_fetchCountersFromReflection',
+                                      function() {
+                                          let container = new 
Coverage.CoverageStatisticsContainer(MockFilenames,
+                                                                                                   
MockCache);
+                                          let statistics = container.fetchStatistics('filename');
+                                      });
+    JSUnit.assertFalse(called);
+}
+
+function testCoverageCountersFetchedFromReflectionIfMissed() {
+    let called = _checkIfCalledWhilst(Coverage,
+                                      '_fetchCountersFromReflection',
+                                      function() {
+                                          let container = new 
Coverage.CoverageStatisticsContainer(MockFilenames,
+                                                                                                   
MockCache);
+                                          let statistics = container.fetchStatistics('uncached');
+                                      });
+    JSUnit.assertTrue(called);
+}
+
+function testCoverageContainerCacheNotStaleIfAllHit() {
+    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
+                                                             MockCache);
+    let statistics = container.fetchStatistics('filename');
+    JSUnit.assertFalse(container.staleCache());
+}
+
+function testCoverageContainerCacheStaleIfMiss() {
+    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
+                                                             MockCache);
+    let statistics = container.fetchStatistics('uncached');
+    JSUnit.assertTrue(container.staleCache());
+}
+
+function testCoverageCountersFromCacheHaveSameExecutableLinesAsReflection() {
+    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
+                                                             MockCache);
+    let statistics = container.fetchStatistics('filename');
+
+    let containerWithNoCaching = new Coverage.CoverageStatisticsContainer(MockFilenames);
+    let statisticsWithNoCaching = containerWithNoCaching.fetchStatistics('filename');
+
+    assertArrayEquals(statisticsWithNoCaching.expressionCounters,
+                      statistics.expressionCounters,
+                      JSUnit.assertEquals);
+}
+
+function testCoverageCountersFromCacheHaveSameBranchExitsAsReflection() {
+    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
+                                                             MockCache);
+    let statistics = container.fetchStatistics('filename');
+
+    let containerWithNoCaching = new Coverage.CoverageStatisticsContainer(MockFilenames);
+    let statisticsWithNoCaching = containerWithNoCaching.fetchStatistics('filename');
+
+    /* Branch starts on line 4 */
+    JSUnit.assertEquals(statisticsWithNoCaching.branchCounters[4].exits[0].line,
+                        statistics.branchCounters[4].exits[0].line);
+}
+
+function testCoverageCountersFromCacheHaveSameBranchPointsAsReflection() {
+    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
+                                                             MockCache);
+    let statistics = container.fetchStatistics('filename');
+
+    let containerWithNoCaching = new Coverage.CoverageStatisticsContainer(MockFilenames);
+    let statisticsWithNoCaching = containerWithNoCaching.fetchStatistics('filename');
+    JSUnit.assertEquals(statisticsWithNoCaching.branchCounters[4].point,
+                        statistics.branchCounters[4].point);
+}
+
+function testCoverageCountersFromCacheHaveSameFunctionKeysAsReflection() {
+    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
+                                                             MockCache);
+    let statistics = container.fetchStatistics('filename');
+
+    let containerWithNoCaching = new Coverage.CoverageStatisticsContainer(MockFilenames);
+    let statisticsWithNoCaching = containerWithNoCaching.fetchStatistics('filename');
+
+    /* Functions start on line 1 */
+    assertArrayEquals(Object.keys(statisticsWithNoCaching.functionCounters),
+                      Object.keys(statistics.functionCounters),
+                      JSUnit.assertEquals);
+}
+
 JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
diff --git a/modules/coverage.js b/modules/coverage.js
index 84b2ba1..ec94adb 100644
--- a/modules/coverage.js
+++ b/modules/coverage.js
@@ -180,6 +180,14 @@ function collectForSubNodes(subNodes, collector) {
     return result;
 }
 
+function _getFunctionKeyFromReflectedFunction(node) {
+    let name = node.id !== null ? node.id.name : '(anonymous)';
+    let line = node.loc.start.line;
+    let n_params = node.params.length;
+
+    return [name, line, n_params].join(':');
+}
+
 /* Unfortunately, the Reflect API doesn't give us enough information to
  * uniquely identify a function. A function might be anonymous, in which
  * case the JS engine uses some heurisitics to get a unique string identifier
@@ -211,22 +219,9 @@ function functionsForNode(node) {
     switch (node.type) {
     case 'FunctionDeclaration':
     case 'FunctionExpression':
-        if (node.id !== null) {
-            functionNames.push({ name: node.id.name,
-                                 line: node.loc.start.line,
-                                 n_params: node.params.length });
-        }
-        /* If the function wasn't found, we just push a name
-         * that looks like 'function:lineno' to signify that
-         * this was an anonymous function. If the coverage tool
-         * enters a function with no name (but a line number)
-         * then it can probably use this information to
-         * figure out which function it was */
-        else {
-            functionNames.push({ name: null,
-                                 line: node.loc.start.line,
-                                 n_params: node.params.length });
-        }
+        functionNames.push({ key: _getFunctionKeyFromReflectedFunction(node),
+                             line: node.loc.start.line,
+                             n_params: node.params.length });
     }
 
     return functionNames;
@@ -481,20 +476,11 @@ function _branchesToBranchCounters(branches, nLines) {
     return counters;
 }
 
-function _getFunctionKeyFromReflectedFunction(func) {
-    let name = func.name !== null ? func.name : '(anonymous)';
-    let line = func.line;
-    let n_params = func.n_params;
-
-    return name + ':' + line + ':' + n_params;
-}
-
 function _functionsToFunctionCounters(functions) {
     let functionCounters = {};
 
     functions.forEach(function(func) {
-        let functionKey = _getFunctionKeyFromReflectedFunction(func);
-        functionCounters[functionKey] = {
+        functionCounters[func.key] = {
             hitCount: 0
         };
     });
@@ -653,8 +639,58 @@ function _convertFunctionCountersToArray(functionCounters) {
     return arrayReturn;
 }
 
-function CoverageStatisticsContainer(prefixes) {
-    let coveredFiles = {};
+/* Looks up filename in cache and fetches statistics
+ * directly from the cache */
+function _fetchCountersFromCache(filename, cache, nLines) {
+    if (!cache)
+        return null;
+
+    if (Object.keys(cache).indexOf(filename) !== -1) {
+        let cache_for_file = cache[filename];
+
+        if (cache_for_file.mtime) {
+             let mtime = getFileModificationTime(filename);
+             if (mtime[0] != cache[filename].mtime[0] ||
+                 mtime[1] != cache[filename].mtime[1])
+                 return null;
+        } else {
+            let checksum = getFileChecksum(filename);
+            if (checksum != cache[filename].checksum)
+                return null;
+        }
+
+        let functions = cache_for_file.functions;
+
+        return {
+            expressionCounters: _expressionLinesToCounters(cache_for_file.lines, nLines),
+            branchCounters: _branchesToBranchCounters(cache_for_file.branches, nLines),
+            functionCounters: _functionsToFunctionCounters(functions),
+            linesWithKnownFunctions: _populateKnownFunctions(functions, nLines),
+            nLines: nLines
+        };
+    }
+
+    return null;
+}
+
+function _fetchCountersFromReflection(contents, nLines) {
+    let reflection = Reflect.parse(contents);
+    let functions = functionsForAST(reflection);
+
+    return {
+        expressionCounters: _expressionLinesToCounters(expressionLinesForAST(reflection), nLines),
+        branchCounters: _branchesToBranchCounters(branchesForAST(reflection), nLines),
+        functionCounters: _functionsToFunctionCounters(functions),
+        linesWithKnownFunctions: _populateKnownFunctions(functions, nLines),
+        nLines: nLines
+    };
+}
+
+function CoverageStatisticsContainer(prefixes, cache) {
+    /* Copy the files array, so that it can be re-used in the tests */
+    let cachedASTs = cache !== undefined ? JSON.parse(cache) : null;
+    let coveredFiles = {}
+    let cacheMisses = 0;
 
     function wantsStatisticsFor(filename) {
         return prefixes.some(function(prefix) {
@@ -664,38 +700,96 @@ function CoverageStatisticsContainer(prefixes) {
 
     function createStatisticsFor(filename) {
         let contents = getFileContents(filename);
-        let reflection = Reflect.parse(contents);
         let nLines = _getNumberOfLinesForScript(contents);
 
-        let functions = functionsForAST(reflection);
+        let counters = _fetchCountersFromCache(filename, cachedASTs, nLines);
+        if (counters === null) {
+            cacheMisses++;
+            counters = _fetchCountersFromReflection(contents, nLines);
+        }
+
+        if (counters === null)
+            throw new Error('Failed to parse and reflect file ' + filename);
 
-        return {
-            contents: contents,
-            nLines: nLines,
-            expressionCounters: _expressionLinesToCounters(expressionLinesForAST(reflection), nLines),
-            branchCounters: _branchesToBranchCounters(branchesForAST(reflection), nLines),
-            functionCounters: _functionsToFunctionCounters(functions),
-            linesWithKnownFunctions: _populateKnownFunctions(functions, nLines)
-        };
+        /* Set contents here as we don't pass it to _fetchCountersFromCache. */
+        counters.contents = contents;
+
+        return counters;
     }
 
     function ensureStatisticsFor(filename) {
-        if (!coveredFiles[filename] && wantsStatisticsFor(filename))
+        let wantStatistics = wantsStatisticsFor(filename);
+        let haveStatistics = !!coveredFiles[filename];
+
+        if (wantStatistics && !haveStatistics)
             coveredFiles[filename] = createStatisticsFor(filename);
         return coveredFiles[filename];
     }
 
+    this.stringify = function() {
+        let cache_data = {}
+        Object.keys(coveredFiles).forEach(function(filename) {
+            let statisticsForFilename = coveredFiles[filename];
+            let mtime = getFileModificationTime(filename);
+            let cacheDataForFilename = {
+                mtime: mtime,
+                checksum: mtime === null ? getFileChecksum(filename) : null,
+                lines: [],
+                branches: [],
+                functions: Object.keys(statisticsForFilename.functionCounters).map(function(key) {
+                    return {
+                        key: key,
+                        line: Number(key.split(':')[1])
+                    };
+                })
+            };
+
+            /* We're using a index based loop here since we need access to the
+             * index, since it actually represents the current line number
+             * on the file (see _expressionLinesToCounters). */
+            for (let line_index = 0;
+                 line_index < statisticsForFilename.expressionCounters.length;
+                 ++line_index) {
+                 if (statisticsForFilename.expressionCounters[line_index] !== undefined)
+                     cacheDataForFilename.lines.push(line_index);
+
+                 if (statisticsForFilename.branchCounters[line_index] !== undefined) {
+                     let branchCounters = statisticsForFilename.branchCounters[line_index]
+                     cacheDataForFilename.branches.push({
+                         point: statisticsForFilename.branchCounters[line_index].point,
+                         exits: statisticsForFilename.branchCounters[line_index].exits.map(function(exit) {
+                             return exit.line;
+                         })
+                     });
+                 }
+            }
+            cacheDataForFilename.functions = 
Object.keys(statisticsForFilename.functionCounters).map(function(key) {
+                return {
+                    key: key,
+                    line: Number(key.split(':')[1])
+                };
+            });
+            cache_data[filename] = cacheDataForFilename;
+        });
+        return JSON.stringify(cache_data);
+    }
+
     this.getCoveredFiles = function() {
         return Object.keys(coveredFiles);
     };
 
     this.fetchStatistics = function(filename) {
         let statistics = ensureStatisticsFor(filename);
+
         if (statistics === undefined)
             throw new Error('Not tracking statistics for ' + filename);
         return statistics;
     };
 
+    this.staleCache = function() {
+        return cacheMisses > 0;
+    };
+
     this.deleteStatistics = function(filename) {
         coveredFiles[filename] = undefined;
     };
@@ -706,8 +800,8 @@ function CoverageStatisticsContainer(prefixes) {
  *
  * It isn't poissible to unit test this class because it depends on running
  * Debugger which in turn depends on objects injected in from another compartment */
-function CoverageStatistics(prefixes) {
-    this.container = new CoverageStatisticsContainer(prefixes);
+function CoverageStatistics(prefixes, cache) {
+    this.container = new CoverageStatisticsContainer(prefixes, cache);
     let fetchStatistics = this.container.fetchStatistics.bind(this.container);
     let deleteStatistics = this.container.deleteStatistics.bind(this.container);
 
@@ -746,10 +840,10 @@ function CoverageStatistics(prefixes) {
             return undefined;
         }
 
-        function _logExceptionAndReset(e, callee, line) {
-            warning(e.fileName + ":" + e.lineNumber + " (processing " +
+        function _logExceptionAndReset(exception, callee, line) {
+            warning(exception.fileName + ":" + exception.lineNumber + " (processing " +
                     frame.script.url + ":" + callee + ":" + line + ") - " +
-                    e.message);
+                    exception.message);
             warning("Will not log statistics for this file");
             frame.onStep = undefined;
             frame._branchTracker = undefined;
@@ -800,12 +894,20 @@ function CoverageStatistics(prefixes) {
             } catch (e) {
                 /* Something bad happened. Log the exception and delete
                  * statistics for this file */
-                _logExceptionAndReset(e, name, offsetLine);
+                _logExceptionAndReset(e, frame.callee, offsetLine);
             }
         };
 
-        /* Explicitly return here to satisfy strict mode */
         return undefined;
     };
-}
 
+    this.deactivate = function() {
+        /* This property is designed to be a one-stop-shop to
+         * disable the debugger for this debugee, without having
+         * to traverse all its scripts or frames */
+        this.dbg.enabled = false;
+    };
+
+    this.staleCache = this.container.staleCache.bind(this.container);
+    this.stringify = this.container.stringify.bind(this.container);
+}
diff --git a/test/gjs-test-coverage.cpp b/test/gjs-test-coverage.cpp
index de4a4e8..a56128d 100644
--- a/test/gjs-test-coverage.cpp
+++ b/test/gjs-test-coverage.cpp
@@ -30,8 +30,11 @@
 
 #include <glib.h>
 #include <gio/gio.h>
+#include <gio/gunixoutputstream.h>
 #include <gjs/gjs.h>
 #include <gjs/coverage.h>
+#include <gjs/coverage-internal.h>
+#include <gjs/gjs-module.h>
 
 typedef struct _GjsCoverageFixture {
     GjsContext    *context;
@@ -334,6 +337,36 @@ coverage_data_matches_values_for_key(const char            *data,
     return FALSE;
 }
 
+/* A simple wrapper around gjs_coverage_new */
+GjsCoverage *
+create_coverage_for_script(GjsContext *context,
+                           const char *script)
+{
+    const char *coverage_scripts[] = {
+        script,
+        NULL
+    };
+
+    return gjs_coverage_new(coverage_scripts,
+                            context);
+}
+
+GjsCoverage *
+create_coverage_for_script_and_cache(GjsContext *context,
+                                     const char *cache,
+                                     const char *script)
+{
+    const char *coverage_scripts[] = {
+        script,
+        NULL
+    };
+
+
+    return gjs_coverage_new_from_cache(coverage_scripts,
+                                       context,
+                                       cache);
+}
+
 static void
 test_covered_file_is_duplicated_into_output_if_resource(gpointer      fixture_data,
                                                         gconstpointer user_data)
@@ -371,11 +404,7 @@ test_covered_file_is_duplicated_into_output_if_resource(gpointer      fixture_da
                          "org/gnome/gjs/mock/test/gjs-test-coverage/loadedJSFromResource.js",
                          NULL);
 
-    GFile *file_for_expected_path = g_file_new_for_path(expected_temporary_js_script_file_path);
-
-    g_assert(g_file_query_exists(file_for_expected_path, NULL) == TRUE);
-
-    g_object_unref(file_for_expected_path);
+    g_assert_true(g_file_test(expected_temporary_js_script_file_path, G_FILE_TEST_EXISTS));
     g_free(expected_temporary_js_script_file_path);
 }
 
@@ -401,11 +430,8 @@ test_covered_file_is_duplicated_into_output_if_path(gpointer      fixture_data,
                          temporary_js_script_basename,
                          NULL);
 
-    GFile *file_for_expected_path = g_file_new_for_path(expected_temporary_js_script_file_path);
-
-    g_assert(g_file_query_exists(file_for_expected_path, NULL) == TRUE);
+    g_assert_true(g_file_test(expected_temporary_js_script_file_path, G_FILE_TEST_EXISTS));
 
-    g_object_unref(file_for_expected_path);
     g_free(expected_temporary_js_script_file_path);
     g_free(temporary_js_script_basename);
 }
@@ -523,6 +549,7 @@ test_expected_entry_not_written_for_nonexistent_file(gpointer      fixture_data,
                                                     "SF:",
                                                     temporary_js_script_basename)));
 
+    g_free(coverage_data_contents);
     g_free(temporary_js_script_basename);
 }
 
@@ -1231,7 +1258,6 @@ test_no_hits_to_coverage_data_for_unexecuted(gpointer      fixture_data,
 
     /* No files were executed, so the coverage data is empty. */
     g_assert_cmpstr(coverage_data_contents, ==, "");
-
     g_free(coverage_data_contents);
 }
 
@@ -1379,6 +1405,18 @@ check_coverage_data_for_source_file(ExpectedSourceFileCoverageData *expected,
     return FALSE;
 }
 
+static char *
+get_output_path_for_script_on_disk(const char *path_to_script,
+                                   const char *path_to_output_dir)
+
+{
+    char *base = g_filename_display_basename(path_to_script);
+    char *output_path = g_build_filename(path_to_output_dir, base, NULL);
+
+    g_free(base);
+    return output_path;
+}
+
 static void
 test_correct_line_coverage_data_written_for_both_source_file_sectons(gpointer      fixture_data,
                                                                      gconstpointer user_data)
@@ -1454,6 +1492,678 @@ test_correct_line_coverage_data_written_for_both_source_file_sectons(gpointer
     g_free(coverage_data_contents);
 }
 
+typedef GjsCoverageToSingleOutputFileFixture GjsCoverageCacheFixture;
+
+static void
+gjs_coverage_cache_fixture_set_up(gpointer      fixture_data,
+                                  gconstpointer user_data)
+{
+    gjs_coverage_to_single_output_file_fixture_set_up(fixture_data, user_data);
+}
+
+static void
+gjs_coverage_cache_fixture_tear_down(gpointer      fixture_data,
+                                     gconstpointer user_data)
+{
+    gjs_coverage_to_single_output_file_fixture_tear_down(fixture_data, user_data);
+}
+
+static GString *
+append_tuples_to_array_in_object_notation(GString    *string,
+                                          const char  *tuple_contents_strv)
+{
+    char *original_ptr = (char *) tuple_contents_strv;
+    char *expected_tuple_contents = NULL;
+    while ((expected_tuple_contents = strsep((char **) &tuple_contents_strv, ";")) != NULL) {
+       if (!strlen(expected_tuple_contents))
+           continue;
+
+       if (expected_tuple_contents != original_ptr)
+           g_string_append_printf(string, ",");
+        g_string_append_printf(string, "{%s}", expected_tuple_contents);
+    }
+
+    return string;
+}
+
+static GString *
+format_expected_cache_object_notation(const char *mtimes,
+                                      const char *hash,
+                                      const char *script_name,
+                                      const char *expected_executable_lines_array,
+                                      const char *expected_branches,
+                                      const char *expected_functions)
+{
+    GString *string = g_string_new("");
+    g_string_append_printf(string,
+                           "{\"%s\":{\"mtime\":%s,\"checksum\":%s,\"lines\":[%s],\"branches\":[",
+                           script_name,
+                           mtimes,
+                           hash,
+                           expected_executable_lines_array);
+    append_tuples_to_array_in_object_notation(string, expected_branches);
+    g_string_append_printf(string, "],\"functions\":[");
+    append_tuples_to_array_in_object_notation(string, expected_functions);
+    g_string_append_printf(string, "]}}");
+    return string;
+}
+
+typedef struct _GjsCoverageCacheObjectNotationTestTableData {
+    const char *test_name;
+    const char *script;
+    const char *resource_path;
+    const char *expected_executable_lines;
+    const char *expected_branches;
+    const char *expected_functions;
+} GjsCoverageCacheObjectNotationTableTestData;
+
+static GBytes *
+serialize_ast_to_bytes(GjsCoverage *coverage,
+                       const char **coverage_paths)
+{
+    return gjs_serialize_statistics(coverage);
+}
+
+static char *
+serialize_ast_to_object_notation(GjsCoverage *coverage,
+                                 const char **coverage_paths)
+{
+    /* Unfortunately, we need to pass in this paramater here since
+     * the len parameter is not allow-none.
+     *
+     * The caller doesn't need to know about the length of the
+     * data since it is only used for strcmp and the data is
+     * NUL-terminated anyway. */
+    gsize len = 0;
+    return (char *)g_bytes_unref_to_data(serialize_ast_to_bytes(coverage, coverage_paths),
+                                         &len);
+}
+
+static char *
+eval_file_for_ast_in_object_notation(GjsContext  *context,
+                                     GjsCoverage *coverage,
+                                     const char  *filename)
+{
+    gboolean success = gjs_context_eval_file(context,
+                                             filename,
+                                             NULL,
+                                             NULL);
+    g_assert_true(success);
+    
+    const gchar *coverage_paths[] = {
+        filename,
+        NULL
+    };
+
+    return serialize_ast_to_object_notation(coverage, coverage_paths);
+}
+
+static void
+test_coverage_cache_data_in_expected_format(gpointer      fixture_data,
+                                            gconstpointer user_data)
+{
+    GjsCoverageCacheFixture                     *fixture = (GjsCoverageCacheFixture *) fixture_data;
+    GjsCoverageCacheObjectNotationTableTestData *table_data = (GjsCoverageCacheObjectNotationTableTestData 
*) user_data;
+
+    GTimeVal mtime;
+    gboolean successfully_got_mtime = gjs_get_path_mtime(fixture->base_fixture.temporary_js_script_filename,
+                                                         &mtime);
+    g_assert_true(successfully_got_mtime);
+
+    char    *mtime_string = g_strdup_printf("[%lli,%lli]", (gint64) mtime.tv_sec, (gint64) mtime.tv_usec);
+    GString *expected_cache_object_notation = format_expected_cache_object_notation(mtime_string,
+                                                                                    "null",
+                                                                                    
fixture->base_fixture.temporary_js_script_filename,
+                                                                                    
table_data->expected_executable_lines,
+                                                                                    
table_data->expected_branches,
+                                                                                    
table_data->expected_functions);
+
+    write_to_file_at_beginning(fixture->base_fixture.temporary_js_script_open_handle, table_data->script);
+    char *cache_in_object_notation = eval_file_for_ast_in_object_notation(fixture->base_fixture.context,
+                                                                          fixture->base_fixture.coverage,
+                                                                          
fixture->base_fixture.temporary_js_script_filename);
+    g_assert(cache_in_object_notation != NULL);
+
+    g_assert_cmpstr(cache_in_object_notation, ==, expected_cache_object_notation->str);
+
+    g_string_free(expected_cache_object_notation, TRUE);
+    g_free(cache_in_object_notation);
+    g_free(mtime_string);
+}
+
+static void
+test_coverage_cache_data_in_expected_format_resource(gpointer      fixture_data,
+                                                     gconstpointer user_data)
+{
+    GjsCoverageCacheFixture                     *fixture = (GjsCoverageCacheFixture *) fixture_data;
+    GjsCoverageCacheObjectNotationTableTestData *table_data = (GjsCoverageCacheObjectNotationTableTestData 
*) user_data;
+
+    char *hash_string_no_quotes = gjs_get_path_checksum(table_data->resource_path);
+    char *hash_string = g_strdup_printf("\"%s\"", hash_string_no_quotes);
+    g_free(hash_string_no_quotes);
+
+    GString *expected_cache_object_notation = format_expected_cache_object_notation("null",
+                                                                                    hash_string,
+                                                                                    
table_data->resource_path,
+                                                                                    
table_data->expected_executable_lines,
+                                                                                    
table_data->expected_branches,
+                                                                                    
table_data->expected_functions);
+
+    g_clear_object(&fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = create_coverage_for_script(fixture->base_fixture.context,
+                                                                             table_data->resource_path);
+    char *cache_in_object_notation = eval_file_for_ast_in_object_notation(fixture->base_fixture.context,
+                                                                          fixture->base_fixture.coverage,
+                                                                          table_data->resource_path);
+
+    g_assert_cmpstr(cache_in_object_notation, ==, expected_cache_object_notation->str);
+
+    g_string_free(expected_cache_object_notation, TRUE);
+    g_free(cache_in_object_notation);
+    g_free(hash_string);
+}
+
+static char *
+generate_coverage_compartment_verify_script(const char *coverage_script_filename,
+                                            const char *user_script)
+{
+    return g_strdup_printf("const JSUnit = imports.jsUnit;\n"
+                           "const covered_script_filename = '%s';\n"
+                           "function assertArrayEquals(lhs, rhs) {\n"
+                           "    JSUnit.assertEquals(lhs.length, rhs.length);\n"
+                           "    for (let i = 0; i < lhs.length; i++)\n"
+                           "        JSUnit.assertEquals(lhs[i], rhs[i]);\n"
+                           "}\n"
+                           "\n"
+                           "%s", coverage_script_filename, user_script);
+}
+
+typedef struct _GjsCoverageCacheJSObjectTableTestData {
+    const char *test_name;
+    const char *script;
+    const char *verify_js_script;
+} GjsCoverageCacheJSObjectTableTestData;
+
+static void
+test_coverage_cache_as_js_object_has_expected_properties(gpointer      fixture_data,
+                                                         gconstpointer user_data)
+{
+    GjsCoverageCacheFixture               *fixture = (GjsCoverageCacheFixture *) fixture_data;
+    GjsCoverageCacheJSObjectTableTestData *table_data = (GjsCoverageCacheJSObjectTableTestData *) user_data;
+
+    write_to_file_at_beginning(fixture->base_fixture.temporary_js_script_open_handle, table_data->script);
+    gjs_context_eval_file(fixture->base_fixture.context,
+                          fixture->base_fixture.temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    const gchar *coverage_paths[] = {
+        fixture->base_fixture.temporary_js_script_filename,
+        NULL
+    };
+
+    GBytes *cache = serialize_ast_to_bytes(fixture->base_fixture.coverage,
+                                           coverage_paths);
+    JS::RootedString cache_results(JS_GetRuntime((JSContext *) 
gjs_context_get_native_context(fixture->base_fixture.context)),
+                                   gjs_deserialize_cache_to_object(fixture->base_fixture.coverage, cache));
+    JS::RootedValue cache_result_value(JS_GetRuntime((JSContext *) 
gjs_context_get_native_context(fixture->base_fixture.context)),
+                                       STRING_TO_JSVAL(cache_results));
+    gjs_inject_value_into_coverage_compartment(fixture->base_fixture.coverage,
+                                               cache_result_value,
+                                               "coverage_cache");
+
+    gchar *verify_script_complete = 
generate_coverage_compartment_verify_script(fixture->base_fixture.temporary_js_script_filename,
+                                                                                
table_data->verify_js_script);
+    gjs_run_script_in_coverage_compartment(fixture->base_fixture.coverage,
+                                           verify_script_complete);
+    g_free(verify_script_complete);
+
+    g_bytes_unref(cache);
+}
+
+typedef struct _GjsCoverageCacheEqualResultsTableTestData {
+    const char *test_name;
+    const char *script;
+} GjsCoverageCacheEqualResultsTableTestData;
+
+static char *
+write_cache_to_temporary_file(const char *temp_dir,
+                              GBytes     *cache)
+{
+    /* Just need a temporary file, don't care about its fd */
+    char *temporary_file = g_build_filename(temp_dir, "gjs_coverage_cache_XXXXXX", NULL);
+    close(mkstemps(temporary_file, 0));
+
+    if (!gjs_write_cache_to_path(temporary_file, cache)) {
+        g_free(temporary_file);
+        return NULL;
+    }
+
+    return temporary_file;
+}
+
+static char *
+serialize_ast_to_cache_in_temporary_file(GjsCoverage *coverage,
+                                         const char  *output_directory,
+                                         const char  **coverage_paths)
+{
+    GBytes   *cache = serialize_ast_to_bytes(coverage, coverage_paths);
+    char   *cache_path = write_cache_to_temporary_file(output_directory, cache);
+
+    g_bytes_unref(cache);
+
+    return cache_path;
+}
+
+static void
+test_coverage_cache_equal_results_to_reflect_parse(gpointer      fixture_data,
+                                                   gconstpointer user_data)
+{
+    GjsCoverageCacheFixture                   *fixture = (GjsCoverageCacheFixture *) fixture_data;
+    GjsCoverageCacheEqualResultsTableTestData *equal_results_data = 
(GjsCoverageCacheEqualResultsTableTestData *) user_data;
+
+    write_to_file_at_beginning(fixture->base_fixture.temporary_js_script_open_handle,
+                               equal_results_data->script);
+
+    const gchar *coverage_paths[] = {
+        fixture->base_fixture.temporary_js_script_filename,
+        NULL
+    };
+
+    char *coverage_data_contents_no_cache =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+    char *cache_path = serialize_ast_to_cache_in_temporary_file(fixture->base_fixture.coverage,
+                                                                fixture->output_file_directory,
+                                                                coverage_paths);
+    g_assert(cache_path != NULL);
+
+    g_clear_object(&fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = create_coverage_for_script_and_cache(fixture->base_fixture.context,
+                                                                          cache_path,
+                                                                          
fixture->base_fixture.temporary_js_script_filename);
+    g_free(cache_path);
+
+    /* Overwrite tracefile with nothing and start over */
+    write_to_file_at_beginning(fixture->output_file_handle, "");
+
+    char *coverage_data_contents_cached =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    g_assert_cmpstr(coverage_data_contents_cached, ==, coverage_data_contents_no_cache);
+
+    g_free(coverage_data_contents_cached);
+    g_free(coverage_data_contents_no_cache);
+}
+
+static char *
+eval_file_for_ast_cache_path(GjsContext  *context,
+                             GjsCoverage *coverage,
+                             const char  *filename,
+                             const char  *output_directory)
+{
+    gboolean success = gjs_context_eval_file(context,
+                                             filename,
+                                             NULL,
+                                             NULL);
+    g_assert_true(success);
+
+    const gchar *coverage_paths[] = {
+        filename,
+        NULL
+    };
+
+    return serialize_ast_to_cache_in_temporary_file(coverage,
+                                                    output_directory,
+                                                    coverage_paths);
+}
+
+/* Effectively, the results should be what we expect even though
+ * we overwrote the original script after getting coverage and
+ * fetching the cache */
+static void
+test_coverage_cache_invalidation(gpointer      fixture_data,
+                                 gconstpointer user_data)
+{
+    GjsCoverageCacheFixture *fixture = (GjsCoverageCacheFixture *) fixture_data;
+
+    char *cache_path = eval_file_for_ast_cache_path(fixture->base_fixture.context,
+                                                    fixture->base_fixture.coverage,
+                                                    fixture->base_fixture.temporary_js_script_filename,
+                                                    fixture->output_file_directory);
+
+    /* Sleep for a little while to make sure that the new file has a
+     * different mtime */
+    sleep(1);
+
+    /* Overwrite tracefile with nothing */
+    write_to_file_at_beginning(fixture->output_file_handle, "");
+
+    /* Write a new script into the temporary js file, which will be
+     * completely different to the original script that was there */
+    write_to_file_at_beginning(fixture->base_fixture.temporary_js_script_open_handle,
+                               "let i = 0;\n"
+                               "let j = 0;\n");
+
+    g_clear_object(&fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = create_coverage_for_script_and_cache(fixture->base_fixture.context,
+                                                                          cache_path,
+                                                                          
fixture->base_fixture.temporary_js_script_filename);
+    g_free(cache_path);
+
+    gsize coverage_data_len = 0;
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          &coverage_data_len);
+
+    LineCountIsMoreThanData matchers[] =
+    {
+        {
+            1,
+            0
+        },
+        {
+            2,
+            0
+        }
+    };
+
+    char *script_output_path = 
get_output_path_for_script_on_disk(fixture->base_fixture.temporary_js_script_filename,
+                                                                  fixture->output_file_directory);
+
+    ExpectedSourceFileCoverageData expected[] = {
+        {
+            script_output_path,
+            matchers,
+            2,
+            '2',
+            '2'
+        }
+    };
+
+    const gsize expected_len = G_N_ELEMENTS(expected);
+    const char *record = line_starting_with(coverage_data_contents, "SF:");
+    g_assert(check_coverage_data_for_source_file(expected, expected_len, record));
+
+    g_free(script_output_path);
+    g_free(coverage_data_contents);
+}
+
+static void
+unload_resource(GResource *resource)
+{
+    g_resources_unregister(resource);
+    g_resource_unref(resource);
+}
+
+static GResource *
+load_resource_from_builddir(const char *name)
+{
+    char *resource_path = g_build_filename(GJS_TOP_BUILDDIR,
+                                           name,
+                                           NULL);
+
+    GError    *error = NULL;
+    GResource *resource = g_resource_load(resource_path,
+                                          &error);
+
+    g_assert_no_error(error);
+    g_resources_register(resource);
+
+    g_free(resource_path);
+
+    return resource;
+}
+
+/* Load first resource, then unload and load second resource. Both have
+ * the same path, but different contents */
+static void
+test_coverage_cache_invalidation_resource(gpointer      fixture_data,
+                                          gconstpointer user_data)
+{
+    GjsCoverageCacheFixture *fixture = (GjsCoverageCacheFixture *) fixture_data;
+
+    const char *mock_resource_filename = "resource:///org/gnome/gjs/mock/cache/resource.js";
+
+    /* Load the resource archive and register it */
+    GResource *first_resource = load_resource_from_builddir("mock-cache-invalidation-before.gresource");
+
+    g_clear_object(&fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = create_coverage_for_script(fixture->base_fixture.context,
+                                                                mock_resource_filename);
+
+    char *cache_path = eval_file_for_ast_cache_path(fixture->base_fixture.context,
+                                                    fixture->base_fixture.coverage,
+                                                    mock_resource_filename,
+                                                    fixture->output_file_directory);
+
+    /* Load the "after" resource, but have the exact same coverage paths */
+    unload_resource(first_resource);
+    GResource *second_resource = load_resource_from_builddir("mock-cache-invalidation-after.gresource");
+
+    /* Overwrite tracefile with nothing */
+    write_to_file_at_beginning(fixture->output_file_handle, "");
+
+    g_clear_object(&fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = create_coverage_for_script_and_cache(fixture->base_fixture.context,
+                                                                          cache_path,
+                                                                          mock_resource_filename);
+    g_free(cache_path);
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          mock_resource_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    /* Don't need this anymore */
+    unload_resource(second_resource);
+
+    /* Now assert that the coverage file has executable lines in
+     * the places that we expect them to be */
+    LineCountIsMoreThanData matchers[] = {
+        {
+            1,
+            0
+        },
+        {
+            2,
+            0
+        }
+    };
+
+    char *script_output_path =
+        g_build_filename(fixture->output_file_directory,
+                         "org/gnome/gjs/mock/cache/resource.js",
+                         NULL);
+
+    ExpectedSourceFileCoverageData expected[] = {
+        {
+            script_output_path,
+            matchers,
+            2,
+            '2',
+            '2'
+        }
+    };
+
+    const gsize expected_len = G_N_ELEMENTS(expected);
+    const char *record = line_starting_with(coverage_data_contents, "SF:");
+    g_assert(check_coverage_data_for_source_file(expected, expected_len, record));
+
+    g_free(script_output_path);
+    g_free(coverage_data_contents);
+}
+
+static char *
+get_coverage_cache_path(const char *output_directory)
+{
+    char *cache_path = g_build_filename(output_directory,
+                                        "coverage-cache-XXXXXX",
+                                        NULL);
+    close(mkstemp(cache_path));
+    unlink(cache_path);
+
+    return cache_path;
+}
+
+static void
+test_coverage_cache_file_written_when_no_cache_exists(gpointer      fixture_data,
+                                                      gconstpointer user_data)
+{
+    GjsCoverageCacheFixture *fixture = (GjsCoverageCacheFixture *) fixture_data;
+    char *cache_path = get_coverage_cache_path(fixture->output_file_directory);
+
+    g_clear_object(&fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = create_coverage_for_script_and_cache(fixture->base_fixture.context,
+                                                                          cache_path,
+                                                                          
fixture->base_fixture.temporary_js_script_filename);
+
+    /* We need to execute the script now in order for a cache entry
+     * to be created, since unexecuted scripts are not counted as
+     * part of the coverage report. */
+    gboolean success = gjs_context_eval_file(fixture->base_fixture.context,
+                                             fixture->base_fixture.temporary_js_script_filename,
+                                             NULL,
+                                             NULL);
+    g_assert_true(success);
+
+    gjs_coverage_write_statistics(fixture->base_fixture.coverage,
+                                  fixture->output_file_directory);
+
+    g_assert_true(g_file_test(cache_path, G_FILE_TEST_EXISTS));
+    g_free(cache_path);
+}
+
+static GTimeVal
+eval_script_for_cache_mtime(GjsContext  *context,
+                            GjsCoverage *coverage,
+                            const char  *cache_path,
+                            const char  *script,
+                            const char  *output_directory)
+{
+    gboolean success = gjs_context_eval_file(context,
+                                             script,
+                                             NULL,
+                                             NULL);
+    g_assert_true(success);
+
+    gjs_coverage_write_statistics(coverage,
+                                  output_directory);
+
+    GTimeVal mtime;
+    gboolean successfully_got_mtime = gjs_get_path_mtime(cache_path, &mtime);
+    g_assert_true(successfully_got_mtime);
+
+    return mtime;
+}
+
+static void
+test_coverage_cache_updated_when_cache_stale(gpointer      fixture_data,
+                                             gconstpointer user_data)
+{
+    GjsCoverageCacheFixture *fixture = (GjsCoverageCacheFixture *) fixture_data;
+
+    char *cache_path = get_coverage_cache_path(fixture->output_file_directory);
+    g_clear_object(&fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = create_coverage_for_script_and_cache(fixture->base_fixture.context,
+                                                                          cache_path,
+                                                                          
fixture->base_fixture.temporary_js_script_filename);
+
+    GTimeVal first_cache_mtime = eval_script_for_cache_mtime(fixture->base_fixture.context,
+                                                             fixture->base_fixture.coverage,
+                                                             cache_path,
+                                                             
fixture->base_fixture.temporary_js_script_filename,
+                                                             fixture->output_file_directory);
+
+    /* Sleep for a little while to make sure that the new file has a
+     * different mtime */
+    sleep(1);
+
+    /* Write a new script into the temporary js file, which will be
+     * completely different to the original script that was there */
+    write_to_file_at_beginning(fixture->base_fixture.temporary_js_script_open_handle,
+                               "let i = 0;\n"
+                               "let j = 0;\n");
+
+    /* Re-create coverage object, covering new script */
+    g_clear_object(&fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = create_coverage_for_script_and_cache(fixture->base_fixture.context,
+                                                                          cache_path,
+                                                                          
fixture->base_fixture.temporary_js_script_filename);
+
+
+    /* Run the script again, which will cause an attempt
+     * to look up the AST data. Upon writing the statistics
+     * again, the cache should have been missed some of the time
+     * so the second mtime will be greater than the first */
+    GTimeVal second_cache_mtime = eval_script_for_cache_mtime(fixture->base_fixture.context,
+                                                              fixture->base_fixture.coverage,
+                                                              cache_path,
+                                                              
fixture->base_fixture.temporary_js_script_filename,
+                                                              fixture->output_file_directory);
+
+
+    const gboolean seconds_different = (first_cache_mtime.tv_sec != second_cache_mtime.tv_sec);
+    const gboolean microseconds_different (first_cache_mtime.tv_usec != second_cache_mtime.tv_usec);
+
+    g_assert_true(seconds_different || microseconds_different);
+
+    g_free(cache_path);
+}
+
+static void
+test_coverage_cache_not_updated_on_full_hits(gpointer      fixture_data,
+                                             gconstpointer user_data)
+{
+    GjsCoverageCacheFixture *fixture = (GjsCoverageCacheFixture *) fixture_data;
+
+    char *cache_path = get_coverage_cache_path(fixture->output_file_directory);
+    g_clear_object(&fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = create_coverage_for_script_and_cache(fixture->base_fixture.context,
+                                                                          cache_path,
+                                                                          
fixture->base_fixture.temporary_js_script_filename);
+
+    GTimeVal first_cache_mtime = eval_script_for_cache_mtime(fixture->base_fixture.context,
+                                                             fixture->base_fixture.coverage,
+                                                             cache_path,
+                                                             
fixture->base_fixture.temporary_js_script_filename,
+                                                             fixture->output_file_directory);
+
+    /* Re-create coverage object, covering same script */
+    g_clear_object(&fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = create_coverage_for_script_and_cache(fixture->base_fixture.context,
+                                                                          cache_path,
+                                                                          
fixture->base_fixture.temporary_js_script_filename);
+
+
+    /* Run the script again, which will cause an attempt
+     * to look up the AST data. Upon writing the statistics
+     * again, the cache should have been hit of the time
+     * so the second mtime will be the same as the first */
+    GTimeVal second_cache_mtime = eval_script_for_cache_mtime(fixture->base_fixture.context,
+                                                              fixture->base_fixture.coverage,
+                                                              cache_path,
+                                                              
fixture->base_fixture.temporary_js_script_filename,
+                                                              fixture->output_file_directory);
+
+    g_assert_cmpint(first_cache_mtime.tv_sec, ==, second_cache_mtime.tv_sec);
+    g_assert_cmpint(first_cache_mtime.tv_usec, ==, second_cache_mtime.tv_usec);
+
+    g_free(cache_path);
+}
+
 typedef struct _FixturedTest {
     gsize            fixture_size;
     GTestFixtureFunc set_up;
@@ -1474,6 +2184,40 @@ add_test_for_fixture(const char      *name,
                       fixture->tear_down);
 }
 
+/* All table driven tests must be binary compatible with at
+ * least this header */
+typedef struct _TestTableDataHeader {
+    const char *test_name;
+} TestTableDataHeader;
+
+static void
+add_table_driven_test_for_fixture(const char                *name,
+                                  FixturedTest              *fixture,
+                                  GTestFixtureFunc          test_func,
+                                  gsize                     table_entry_size,
+                                  gsize                     n_table_entries,
+                                  const TestTableDataHeader *test_table)
+{
+    const char  *test_table_ptr = (const char *)test_table;
+    gsize test_table_index;
+
+    for (test_table_index = 0;
+         test_table_index < n_table_entries;
+         ++test_table_index, test_table_ptr += table_entry_size) {
+        TestTableDataHeader *header = (TestTableDataHeader *) test_table_ptr;
+        gchar *test_name_for_table_index = g_strdup_printf("%s/%s",
+                                                           name,
+                                                           header->test_name);
+        g_test_add_vtable(test_name_for_table_index,
+                          fixture->fixture_size,
+                          test_table_ptr,
+                          fixture->set_up,
+                          test_func,
+                          fixture->tear_down);
+        g_free(test_name_for_table_index);
+    }
+}
+
 void gjs_test_add_tests_for_coverage()
 {
     FixturedTest coverage_to_single_output_fixture = {
@@ -1581,4 +2325,144 @@ void gjs_test_add_tests_for_coverage()
                          &coverage_for_multiple_files_to_single_output_fixture,
                          test_correct_line_coverage_data_written_for_both_source_file_sectons,
                          NULL);
+
+    FixturedTest coverage_cache_fixture = {
+        sizeof(GjsCoverageCacheFixture),
+        gjs_coverage_cache_fixture_set_up,
+        gjs_coverage_cache_fixture_tear_down
+    };
+
+    /* This must be static, because g_test_add_vtable does not copy it */
+    static GjsCoverageCacheObjectNotationTableTestData data_in_expected_format_table[] = {
+        {
+            "simple_executable_lines",
+            "let i = 0;\n",
+            "resource://org/gnome/gjs/mock/test/gjs-test-coverage/cache_notation/simple_executable_lines.js",
+            "1",
+            "",
+            ""
+        },
+        {
+            "simple_branch",
+            "let i = 0;\n"
+            "if (i) {\n"
+            "    i = 1;\n"
+            "} else {\n"
+            "    i = 2;\n"
+            "}\n",
+            "resource://org/gnome/gjs/mock/test/gjs-test-coverage/cache_notation/simple_branch.js",
+            "1,2,3,5",
+            "\"point\":2,\"exits\":[3,5]",
+            ""
+        },
+        {
+            "simple_function",
+            "function f() {\n"
+            "}\n",
+            "resource://org/gnome/gjs/mock/test/gjs-test-coverage/cache_notation/simple_function.js",
+            "1,2",
+            "",
+            "\"key\":\"f:1:0\",\"line\":1"
+        }
+    };
+
+    add_table_driven_test_for_fixture("/gjs/coverage/cache/data_format",
+                                      &coverage_cache_fixture,
+                                      test_coverage_cache_data_in_expected_format,
+                                      sizeof(GjsCoverageCacheObjectNotationTableTestData),
+                                      G_N_ELEMENTS(data_in_expected_format_table),
+                                      (const TestTableDataHeader *) data_in_expected_format_table);
+
+    add_table_driven_test_for_fixture("/gjs/coverage/cache/data_format_resource",
+                                      &coverage_cache_fixture,
+                                      test_coverage_cache_data_in_expected_format_resource,
+                                      sizeof(GjsCoverageCacheObjectNotationTableTestData),
+                                      G_N_ELEMENTS(data_in_expected_format_table),
+                                      (const TestTableDataHeader *) data_in_expected_format_table);
+
+    static GjsCoverageCacheJSObjectTableTestData object_has_expected_properties_table[] = {
+        {
+            "simple_executable_lines",
+            "let i = 0;\n",
+            "assertArrayEquals(JSON.parse(coverage_cache)[covered_script_filename].lines, [1]);\n"
+        },
+        {
+            "simple_branch",
+            "let i = 0;\n"
+            "if (i) {\n"
+            "    i = 1;\n"
+            "} else {\n"
+            "    i = 2;\n"
+            "}\n",
+            "JSUnit.assertEquals(2, 
JSON.parse(coverage_cache)[covered_script_filename].branches[0].point);\n"
+            "assertArrayEquals([3, 5], 
JSON.parse(coverage_cache)[covered_script_filename].branches[0].exits);\n"
+        },
+        {
+            "simple_function",
+            "function f() {\n"
+            "}\n",
+            "JSUnit.assertEquals('f:1:0', 
JSON.parse(coverage_cache)[covered_script_filename].functions[0].key);\n"
+        }
+    };
+
+    add_table_driven_test_for_fixture("/gjs/coverage/cache/object_props",
+                                      &coverage_cache_fixture,
+                                      test_coverage_cache_as_js_object_has_expected_properties,
+                                      sizeof(GjsCoverageCacheJSObjectTableTestData),
+                                      G_N_ELEMENTS(object_has_expected_properties_table),
+                                      (const TestTableDataHeader *) object_has_expected_properties_table);
+
+    static GjsCoverageCacheEqualResultsTableTestData equal_results_table[] = {
+        {
+            "simple_executable_lines",
+            "let i = 0;\n"
+            "let j = 1;\n"
+        },
+        {
+            "simple_branch",
+            "let i = 0;\n"
+            "if (i) {\n"
+            "    i = 1;\n"
+            "} else {\n"
+            "    i = 2;\n"
+            "}\n"
+        },
+        {
+            "simple_function",
+            "function f() {\n"
+            "}\n"
+        }
+    };
+
+    add_table_driven_test_for_fixture("/gjs/coverage/cache/equal/executable_lines",
+                                      &coverage_cache_fixture,
+                                      test_coverage_cache_equal_results_to_reflect_parse,
+                                      sizeof(GjsCoverageCacheEqualResultsTableTestData),
+                                      G_N_ELEMENTS(equal_results_table),
+                                      (const TestTableDataHeader *) equal_results_table);
+
+    add_test_for_fixture("/gjs/coverage/cache/invalidation",
+                         &coverage_cache_fixture,
+                         test_coverage_cache_invalidation,
+                         NULL);
+
+    add_test_for_fixture("/gjs/coverage/cache/invalidation_resource",
+                         &coverage_cache_fixture,
+                         test_coverage_cache_invalidation_resource,
+                         NULL);
+
+    add_test_for_fixture("/gjs/coverage/cache/file_written",
+                         &coverage_cache_fixture,
+                         test_coverage_cache_file_written_when_no_cache_exists,
+                         NULL);
+
+    add_test_for_fixture("/gjs/coverage/cache/no_update_on_full_hits",
+                         &coverage_cache_fixture,
+                         test_coverage_cache_not_updated_on_full_hits,
+                         NULL);
+
+    add_test_for_fixture("/gjs/coverage/cache/update_on_misses",
+                         &coverage_cache_fixture,
+                         test_coverage_cache_updated_when_cache_stale,
+                         NULL);
 }
diff --git a/test/gjs-test-coverage/cache_invalidation/after/mock-js-resource-cache-after.gresource.xml 
b/test/gjs-test-coverage/cache_invalidation/after/mock-js-resource-cache-after.gresource.xml
new file mode 100644
index 0000000..fd81ad8
--- /dev/null
+++ b/test/gjs-test-coverage/cache_invalidation/after/mock-js-resource-cache-after.gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/gjs/mock/cache">
+    <!-- Note: This file is the same as the 'before' file, but
+         are run with separate working directories-->
+    <file>resource.js</file>
+  </gresource>
+</gresources>
diff --git a/test/gjs-test-coverage/cache_invalidation/after/resource.js 
b/test/gjs-test-coverage/cache_invalidation/after/resource.js
new file mode 100644
index 0000000..0d31e2a
--- /dev/null
+++ b/test/gjs-test-coverage/cache_invalidation/after/resource.js
@@ -0,0 +1,2 @@
+let i = 0;
+let j = 1;
diff --git a/test/gjs-test-coverage/cache_invalidation/before/mock-js-resource-cache-before.gresource.xml 
b/test/gjs-test-coverage/cache_invalidation/before/mock-js-resource-cache-before.gresource.xml
new file mode 100644
index 0000000..6122dd2
--- /dev/null
+++ b/test/gjs-test-coverage/cache_invalidation/before/mock-js-resource-cache-before.gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/gjs/mock/cache">
+    <!-- Note: This file is the same as the 'before' file, but             
+         are run with separate working directories-->
+    <file>resource.js</file>
+  </gresource>
+</gresources>
diff --git a/test/gjs-test-coverage/cache_invalidation/before/resource.js 
b/test/gjs-test-coverage/cache_invalidation/before/resource.js
new file mode 100644
index 0000000..23a0439
--- /dev/null
+++ b/test/gjs-test-coverage/cache_invalidation/before/resource.js
@@ -0,0 +1,2 @@
+function f() {
+}
diff --git a/test/gjs-test-coverage/cache_notation/simple_branch.js 
b/test/gjs-test-coverage/cache_notation/simple_branch.js
new file mode 100644
index 0000000..916e34e
--- /dev/null
+++ b/test/gjs-test-coverage/cache_notation/simple_branch.js
@@ -0,0 +1,6 @@
+let i = 0;
+if (i) {
+    i = 1;
+} else {
+    i = 2;
+}
diff --git a/test/gjs-test-coverage/cache_notation/simple_executable_lines.js 
b/test/gjs-test-coverage/cache_notation/simple_executable_lines.js
new file mode 100644
index 0000000..2a183bf
--- /dev/null
+++ b/test/gjs-test-coverage/cache_notation/simple_executable_lines.js
@@ -0,0 +1 @@
+let i = 0;
diff --git a/test/gjs-test-coverage/cache_notation/simple_function.js 
b/test/gjs-test-coverage/cache_notation/simple_function.js
new file mode 100644
index 0000000..23a0439
--- /dev/null
+++ b/test/gjs-test-coverage/cache_notation/simple_function.js
@@ -0,0 +1,2 @@
+function f() {
+}
diff --git a/test/mock-js-resources.gresource.xml b/test/mock-js-resources.gresource.xml
index 196f639..5585f91 100644
--- a/test/mock-js-resources.gresource.xml
+++ b/test/mock-js-resources.gresource.xml
@@ -2,5 +2,8 @@
 <gresources>
   <gresource prefix="/org/gnome/gjs/mock">
     <file>test/gjs-test-coverage/loadedJSFromResource.js</file>
+    <file>test/gjs-test-coverage/cache_notation/simple_executable_lines.js</file>
+    <file>test/gjs-test-coverage/cache_notation/simple_branch.js</file>
+    <file>test/gjs-test-coverage/cache_notation/simple_function.js</file>
   </gresource>
 </gresources>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]