Bug 1302663 - Part 1 - Add script to generate headers with event data from Events.yaml. r=dexter
☠☠ backed out by 557a7e72a150 ☠ ☠
authorGeorg Fritzsche <georg.fritzsche@googlemail.com>
Sun, 13 Nov 2016 01:52:28 +0700
changeset 349036 ac12dbbe7b632995c7b7f46e0394979de8d28e72
parent 349035 884861c655c718bec160c5b2778220c14fa3906a
child 349037 ab64c55508aabf1af5063cda3322c526c67422ad
push id10298
push userraliiev@mozilla.com
push dateMon, 14 Nov 2016 12:33:03 +0000
treeherdermozilla-aurora@7e29173b1641 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdexter
bugs1302663
milestone52.0a1
Bug 1302663 - Part 1 - Add script to generate headers with event data from Events.yaml. r=dexter
toolkit/components/telemetry/EventInfo.h
toolkit/components/telemetry/Events.yaml
toolkit/components/telemetry/gen-event-data.py
toolkit/components/telemetry/gen-event-enum.py
toolkit/components/telemetry/moz.build
toolkit/components/telemetry/parse_events.py
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/EventInfo.h
@@ -0,0 +1,52 @@
+/* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryEventInfo_h__
+#define TelemetryEventInfo_h__
+
+// This module is internal to Telemetry. The structures here hold data that
+// describe events.
+// It should only be used by TelemetryEventData.h and TelemetryEvent.cpp.
+//
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace {
+
+struct CommonEventInfo {
+  // Indices for the category and expiration strings.
+  uint32_t category_offset;
+  uint32_t expiration_version_offset;
+
+  // The index and count for the extra key offsets in the extra table.
+  uint32_t extra_index;
+  uint32_t extra_count;
+
+  // The day since UNIX epoch that this probe expires on.
+  uint32_t expiration_day;
+
+  // The dataset this event is recorded in.
+  uint32_t dataset;
+
+  // Convenience functions for accessing event strings.
+  const char* expiration_version() const;
+  const char* category() const;
+  const char* extra_key(uint32_t index) const;
+};
+
+struct EventInfo {
+  // The corresponding CommonEventInfo.
+  const CommonEventInfo& common_info;
+
+  // Indices for the method & object strings.
+  uint32_t method_offset;
+  uint32_t object_offset;
+
+  const char* method() const;
+  const char* object() const;
+};
+
+} // namespace
+
+#endif // TelemetryEventInfo_h__
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/Events.yaml
@@ -0,0 +1,53 @@
+# This category contains event entries used for Telemetry tests.
+# They will not be sent out with any pings.
+telemetry.test:
+- methods: ["test1", "test2"]
+  objects: ["object1", "object2"]
+  bug_numbers: [1286606]
+  notification_emails: ["telemetry-client-dev@mozilla.com"]
+  description: This is a test entry for Telemetry.
+  expiry_date: never
+  extra_keys:
+    key1: This is just a test description.
+    key2: This is another test description.
+- methods: ["test_optout"]
+  objects: ["object1", "object2"]
+  bug_numbers: [1286606]
+  notification_emails: ["telemetry-client-dev@mozilla.com"]
+  description: This is an opt-out test entry.
+  expiry_date: never
+  release_channel_collection: opt-out
+  extra_keys:
+    key1: This is just a test description.
+- methods: ["test_expired_version"]
+  objects: ["object1", "object2"]
+  bug_numbers: [1286606]
+  notification_emails: ["telemetry-client-dev@mozilla.com"]
+  description: This is a test entry with an expired version.
+  expiry_version: "3.6"
+- methods: ["test_expired_date"]
+  objects: ["object1", "object2"]
+  bug_numbers: [1286606]
+  notification_emails: ["telemetry-client-dev@mozilla.com"]
+  description: This is a test entry with an expired date.
+  expiry_date: 2014-01-28
+- methods: ["test_not_expired_optout"]
+  objects: ["object1"]
+  bug_numbers: [1286606]
+  notification_emails: ["telemetry-client-dev@mozilla.com"]
+  description: This is an opt-out test entry with unexpired date and version.
+  release_channel_collection: opt-out
+  expiry_date: 2099-01-01
+  expiry_version: "999.0"
+
+# This is a secondary category used for Telemetry tests.
+# The events here will not be sent out with any pings.
+telemetry.test.second:
+- methods: ["test"]
+  objects: ["object1", "object2", "object3"]
+  bug_numbers: [1286606]
+  notification_emails: ["telemetry-client-dev@mozilla.com"]
+  description: This is a test entry for Telemetry.
+  expiry_date: never
+  extra_keys:
+    key1: This is just a test description.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/gen-event-data.py
@@ -0,0 +1,142 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out event information for C++. The events are defined
+# in a file provided as a command-line argument.
+
+from __future__ import print_function
+from shared_telemetry_utils import StringTable, static_assert
+
+import parse_events
+import sys
+import itertools
+
+# The banner/text at the top of the generated file.
+banner = """/* This file is auto-generated, only for internal use in TelemetryEvent.h,
+   see gen-event-data.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryEventData_h
+#define mozilla_TelemetryEventData_h
+#include "EventInfo.h"
+namespace {
+"""
+
+file_footer = """\
+} // namespace
+#endif // mozilla_TelemetryEventData_h
+"""
+
+def write_extra_table(events, output, string_table):
+    table_name = "gExtraKeysTable"
+    extra_table = []
+    extra_count = 0
+
+    print("const uint32_t %s[] = {" % table_name, file=output)
+
+    for e in events:
+        extra_index = 0
+        extra_keys = e.extra_keys
+        if len(extra_keys) > 0:
+            extra_index = extra_count
+            extra_count += len(extra_keys)
+            indexes = string_table.stringIndexes(extra_keys)
+
+            print("  // %s, [%s], [%s]" % (
+                    e.category,
+                    ", ".join(e.methods),
+                    ", ".join(e.objects)),
+                  file=output)
+            print("  // extra_keys: %s" % ", ".join(extra_keys), file=output)
+            print("  %s," % ", ".join(map(str, indexes)),
+                  file=output)
+
+        extra_table.append((extra_index, len(extra_keys)))
+
+    print("};", file=output)
+    static_assert(output, "sizeof(%s) <= UINT32_MAX" % table_name,
+                  "index overflow")
+
+    return extra_table
+
+def write_common_event_table(events, output, string_table, extra_table):
+    table_name = "gCommonEventInfo"
+    extra_count = 0
+
+    print("const CommonEventInfo %s[] = {" % table_name, file=output)
+    for e,extras in zip(events, extra_table):
+        # Write a comment to make the file human-readable.
+        print("  // category: %s" % e.category, file=output)
+        print("  // methods: [%s]" % ", ".join(e.methods), file=output)
+        print("  // objects: [%s]" % ", ".join(e.objects), file=output)
+
+        # Write the common info structure
+        print("  {%d, %d, %d, %d, %d, %s}," %
+                (string_table.stringIndex(e.category),
+                 string_table.stringIndex(e.expiry_version),
+                 extras[0], # extra keys index
+                 extras[1], # extra keys count
+                 e.expiry_day,
+                 e.dataset),
+              file=output)
+
+    print("};", file=output)
+    static_assert(output, "sizeof(%s) <= UINT32_MAX" % table_name,
+                  "index overflow")
+
+def write_event_table(events, output, string_table):
+    table_name = "gEventInfo"
+    print("const EventInfo %s[] = {" % table_name, file=output)
+
+    for common_info_index,e in enumerate(events):
+        for method_name, object_name in itertools.product(e.methods, e.objects):
+            print("  // category: %s, method: %s, object: %s" %
+                    (e.category, method_name, object_name),
+                  file=output)
+
+            print("  {gCommonEventInfo[%d], %d, %d}," %
+                    (common_info_index,
+                     string_table.stringIndex(method_name),
+                     string_table.stringIndex(object_name)),
+                  file=output)
+
+    print("};", file=output)
+    static_assert(output, "sizeof(%s) <= UINT32_MAX" % table_name,
+                  "index overflow")
+
+def main(output, *filenames):
+    # Load the event data.
+    if len(filenames) > 1:
+        raise Exception('We don\'t support loading from more than one file.')
+    events = parse_events.load_events(filenames[0])
+
+    # Write the scalar data file.
+    print(banner, file=output)
+    print(file_header, file=output)
+
+    # Write the extra keys table.
+    string_table = StringTable()
+    extra_table = write_extra_table(events, output, string_table)
+    print("", file=output)
+
+    # Write a table with the common event data.
+    write_common_event_table(events, output, string_table, extra_table)
+    print("", file=output)
+
+    # Write the data for individual events.
+    write_event_table(events, output, string_table)
+    print("", file=output)
+
+    # Write the string table.
+    string_table_name = "gEventsStringTable"
+    string_table.writeDefinition(output, string_table_name)
+    static_assert(output, "sizeof(%s) <= UINT32_MAX" % string_table_name,
+                  "index overflow")
+    print("", file=output)
+
+    print(file_footer, file=output)
+
+if __name__ == '__main__':
+    main(sys.stdout, *sys.argv[1:])
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/gen-event-enum.py
@@ -0,0 +1,73 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out C++ enum definitions that represent the different event types.
+#
+# The events are defined in files provided as command-line arguments.
+
+from __future__ import print_function
+
+import sys
+import parse_events
+
+banner = """/* This file is auto-generated, see gen-event-enum.py.  */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryEventEnums_h
+#define mozilla_TelemetryEventEnums_h
+namespace mozilla {
+namespace Telemetry {
+namespace EventID {
+"""
+
+file_footer = """\
+} // namespace EventID
+} // namespace mozilla
+} // namespace Telemetry
+#endif // mozilla_TelemetryEventEnums_h
+"""
+
+def main(output, *filenames):
+    # Load the events first.
+    if len(filenames) > 1:
+        raise Exception('We don\'t support loading from more than one file.')
+    events = parse_events.load_events(filenames[0])
+
+    grouped = dict()
+    index = 0
+    for e in events:
+        category = e.category
+        if not category in grouped:
+            grouped[category] = []
+        grouped[category].append((index, e))
+        index += len(e.enum_labels)
+
+    # Write the enum file.
+    print(banner, file=output)
+    print(file_header, file=output);
+
+    for category,indexed in grouped.iteritems():
+        category_cpp = indexed[0][1].category_cpp
+
+        print("// category: %s" % category, file=output)
+        print("enum class %s : uint32_t {" % category_cpp, file=output)
+
+        for event_index,e in indexed:
+            cpp_guard = e.cpp_guard
+            if cpp_guard:
+                print("#if defined(%s)" % cpp_guard, file=output)
+            for offset,label in enumerate(e.enum_labels):
+                print("  %s = %d," % (label, event_index + offset), file=output)
+            if cpp_guard:
+                print("#endif", file=output)
+
+        print("};\n", file=output)
+
+    print("const uint32_t EventCount = %d;\n" % index, file=output)
+
+    print(file_footer, file=output)
+
+if __name__ == '__main__':
+    main(sys.stdout, *sys.argv[1:])
--- a/toolkit/components/telemetry/moz.build
+++ b/toolkit/components/telemetry/moz.build
@@ -1,16 +1,32 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 HAS_MISC_RULE = True
 
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
+
+DEFINES['MOZ_APP_VERSION'] = '"%s"' % CONFIG['MOZ_APP_VERSION']
+
+LOCAL_INCLUDES += [
+    '/xpcom/build',
+    '/xpcom/threads',
+]
+
+SPHINX_TREES['telemetry'] = 'docs'
+
+if CONFIG['GNU_CXX']:
+    CXXFLAGS += ['-Wno-error=shadow']
+
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 
 XPIDL_SOURCES += [
     'nsITelemetry.idl',
 ]
 
 XPIDL_MODULE = 'telemetry'
@@ -53,60 +69,60 @@ EXTRA_JS_MODULES += [
     'ThirdPartyCookieProbe.jsm',
     'UITelemetry.jsm',
 ]
 
 TESTING_JS_MODULES += [
   'tests/unit/TelemetryArchiveTesting.jsm',
 ]
 
-include('/ipc/chromium/chromium-config.mozbuild')
-
-FINAL_LIBRARY = 'xul'
-
 GENERATED_FILES = [
+    'TelemetryEventData.h',
+    'TelemetryEventEnums.h',
     'TelemetryHistogramData.inc',
     'TelemetryHistogramEnums.h',
     'TelemetryScalarData.h',
     'TelemetryScalarEnums.h',
 ]
 
+# Generate histogram files.
 histogram_files = [
     'Histograms.json',
     '/dom/base/UseCounters.conf',
     '/dom/base/nsDeprecatedOperationList.h',
 ]
 
 data = GENERATED_FILES['TelemetryHistogramData.inc']
 data.script = 'gen-histogram-data.py'
 data.inputs = histogram_files
 
 enums = GENERATED_FILES['TelemetryHistogramEnums.h']
 enums.script = 'gen-histogram-enum.py'
 enums.inputs = histogram_files
 
-# Generate Scalars
+# Generate scalar files.
 scalar_files = [
     'Scalars.yaml',
 ]
 
 scalar_data = GENERATED_FILES['TelemetryScalarData.h']
 scalar_data.script = 'gen-scalar-data.py'
 scalar_data.inputs = scalar_files
 
 scalar_enums = GENERATED_FILES['TelemetryScalarEnums.h']
 scalar_enums.script = 'gen-scalar-enum.py'
 scalar_enums.inputs = scalar_files
 
-DEFINES['MOZ_APP_VERSION'] = '"%s"' % CONFIG['MOZ_APP_VERSION']
-
-LOCAL_INCLUDES += [
-    '/xpcom/build',
-    '/xpcom/threads',
+# Generate event files.
+event_files = [
+    'Events.yaml',
 ]
 
-SPHINX_TREES['telemetry'] = 'docs'
+event_data = GENERATED_FILES['TelemetryEventData.h']
+event_data.script = 'gen-event-data.py'
+event_data.inputs = event_files
 
-if CONFIG['GNU_CXX']:
-    CXXFLAGS += ['-Wno-error=shadow']
+event_enums = GENERATED_FILES['TelemetryEventEnums.h']
+event_enums.script = 'gen-event-enum.py'
+event_enums.inputs = event_files
 
 with Files('**'):
     BUG_COMPONENT = ('Toolkit', 'Telemetry')
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/parse_events.py
@@ -0,0 +1,271 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import re
+import yaml
+import itertools
+import datetime
+import string
+from shared_telemetry_utils import add_expiration_postfix
+
+MAX_CATEGORY_NAME_LENGTH = 100
+MAX_METHOD_NAME_LENGTH = 40
+MAX_OBJECT_NAME_LENGTH = 40
+MAX_EXTRA_KEYS_COUNT = 20
+MAX_EXTRA_KEY_NAME_LENGTH = 20
+
+IDENTIFIER_PATTERN = r'^[a-zA-Z][a-zA-Z0-9_.]+[a-zA-Z0-9]$'
+DATE_PATTERN = r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
+
+def nice_type_name(t):
+    if isinstance(t, basestring):
+        return "string"
+    return t.__name__
+
+def convert_to_cpp_identifier(s, sep):
+    return string.capwords(s, sep).replace(sep, "")
+
+class OneOf:
+    """This is a placeholder type for the TypeChecker below.
+    It signals that the checked value should match one of the following arguments
+    passed to the TypeChecker constructor.
+    """
+    pass
+
+class TypeChecker:
+    """This implements a convenience type TypeChecker to make the validation code more readable."""
+    def __init__(self, kind, *args):
+        """This takes 1-3 arguments, specifying the value type to check for.
+        It supports:
+        - atomic values, e.g.: TypeChecker(int)
+        - list values, e.g.: TypeChecker(list, basestring)
+        - dict values, e.g.: TypeChecker(dict, basestring, int)
+        - atomic values that can have different types, e.g.: TypeChecker(OneOf, int, date)"""
+        self._kind = kind
+        self._args = args
+
+    def check(self, key, value):
+        # Check fields that can be one of two different types.
+        if self._kind is OneOf:
+            if not isinstance(value, self._args[0]) and not isinstance(value, self._args[1]):
+                raise ValueError, "failed type check for %s - expected %s or %s, got %s" %\
+                                  (key,
+                                   nice_type_name(self._args[0]),
+                                   nice_type_name(self._args[1]),
+                                   nice_type_name(type(value)))
+            return
+
+        # Check basic type of value.
+        if not isinstance(value, self._kind):
+            raise ValueError, "failed type check for %s - expected %s, got %s" %\
+                              (key,
+                               nice_type_name(self._kind),
+                               nice_type_name(type(value)))
+
+        # Check types of values in lists.
+        if self._kind is list:
+            if len(value) < 1:
+                raise ValueError, "failed check for %s - list should not be empty" % key
+            for x in value:
+                if not isinstance(x, self._args[0]):
+                    raise ValueError, "failed type check for %s - expected list value type %s, got %s" %\
+                                      (key,
+                                       nice_type_name(self._args[0]),
+                                       nice_type_name(type(x)))
+        # Check types of keys and values in dictionaries.
+        elif self._kind is dict:
+            if len(value.keys()) < 1:
+                    raise ValueError, "failed check for %s - dict should not be empty" % key
+            for x in value.iterkeys():
+                if not isinstance(x, self._args[0]):
+                    raise ValueError, "failed dict type check for %s - expected key type %s, got %s" %\
+                                      (key,
+                                       nice_type_name(self._args[0]),
+                                       nice_type_name(type(x)))
+            for k,v in value.iteritems():
+                if not isinstance(x, self._args[1]):
+                    raise ValueError, "failed dict type check for %s - expected value type %s for key %s, got %s" %\
+                                      (key,
+                                       nice_type_name(self._args[1]),
+                                       k,
+                                       nice_type_name(type(x)))
+
+def type_check_event_fields(category, definition):
+    """Perform a type/schema check on the event definition."""
+    REQUIRED_FIELDS = {
+        'methods': TypeChecker(list, basestring),
+        'objects': TypeChecker(list, basestring),
+        'bug_numbers': TypeChecker(list, int),
+        'notification_emails': TypeChecker(list, basestring),
+        'description': TypeChecker(basestring),
+    }
+    OPTIONAL_FIELDS = {
+        'release_channel_collection': TypeChecker(basestring),
+        'expiry_date': TypeChecker(OneOf, basestring, datetime.date),
+        'expiry_version': TypeChecker(basestring),
+        'extra_keys': TypeChecker(dict, basestring, basestring),
+    }
+    ALL_FIELDS = REQUIRED_FIELDS.copy()
+    ALL_FIELDS.update(OPTIONAL_FIELDS)
+
+    # Check that all the required fields are available.
+    missing_fields = [f for f in REQUIRED_FIELDS.keys() if f not in definition]
+    if len(missing_fields) > 0:
+        raise KeyError(category + ' - missing required fields: ' + ', '.join(missing_fields))
+
+    # Is there any unknown field?
+    unknown_fields = [f for f in definition.keys() if f not in ALL_FIELDS]
+    if len(unknown_fields) > 0:
+        raise KeyError(category + ' - unknown fields: ' + ', '.join(unknown_fields))
+
+    # Type-check fields.
+    for k,v in definition.iteritems():
+        ALL_FIELDS[k].check(k, v)
+
+def string_check(category, field_name, value, min_length, max_length, regex=None):
+    # Length check.
+    if len(value) > max_length:
+        raise ValueError("Value for %s in %s exceeds maximum length of %d" %\
+                         (field_name, category, max_length))
+    # Regex check.
+    if regex and not re.match(regex, value):
+        raise ValueError, 'String value for %s in %s is not matching pattern "%s": %s' % \
+                          (field_name, category, regex, value)
+
+class EventData:
+    """A class representing one event."""
+
+    def __init__(self, category, definition):
+        type_check_event_fields(category, definition)
+
+        string_check(category, 'methods', definition.get('methods')[0], 1, MAX_METHOD_NAME_LENGTH, regex=IDENTIFIER_PATTERN)
+        string_check(category, 'objects', definition.get('objects')[0], 1, MAX_OBJECT_NAME_LENGTH, regex=IDENTIFIER_PATTERN)
+
+        # Check release_channel_collection
+        rcc_key = 'release_channel_collection'
+        rcc = definition.get(rcc_key, 'opt-in')
+        allowed_rcc = ["opt-in", "opt-out"]
+        if not rcc in allowed_rcc:
+            raise ValueError, "Value for %s in %s should be one of: %s" %\
+                              (rcc_key, category, ", ".join(allowed_rcc))
+
+        # Check extra_keys.
+        extra_keys = definition.get('extra_keys', {})
+        if len(extra_keys.keys()) > MAX_EXTRA_KEYS_COUNT:
+            raise ValueError, "Number of extra_keys in %s exceeds limit %d" %\
+                              (category, MAX_EXTRA_KEYS_COUNT)
+        for key in extra_keys.iterkeys():
+            string_check(category, 'extra_keys', key, 1, MAX_EXTRA_KEY_NAME_LENGTH, regex=IDENTIFIER_PATTERN)
+
+        # Check expiry.
+        if not 'expiry_version' in definition and not 'expiry_date' in definition:
+            raise KeyError, "Event in %s is missing an expiration - either expiry_version or expiry_date is required" %\
+                            (category)
+        expiry_date = definition.get('expiry_date')
+        if expiry_date and isinstance(expiry_date, basestring) and expiry_date != 'never':
+            if not re.match(DATE_PATTERN, expiry_date):
+                raise ValueError, "Event in %s has invalid expiry_date, it should be either 'never' or match this format: %s" %\
+                                  (category, DATE_PATTERN)
+            # Parse into date.
+            definition['expiry_date'] = datetime.datetime.strptime(expiry_date, '%Y-%m-%d')
+
+        # Finish setup.
+        self._category = category
+        self._definition = definition
+        definition['expiry_version'] = add_expiration_postfix(definition.get('expiry_version', 'never'))
+
+    @property
+    def category(self):
+        return self._category
+
+    @property
+    def category_cpp(self):
+        # Transform e.g. category.example into CategoryExample.
+        return convert_to_cpp_identifier(self._category, ".")
+
+    @property
+    def methods(self):
+        return self._definition.get('methods')
+
+    @property
+    def objects(self):
+        return self._definition.get('objects')
+
+    @property
+    def expiry_version(self):
+        return self._definition.get('expiry_version')
+
+    @property
+    def expiry_day(self):
+        date = self._definition.get('expiry_date')
+        if not date:
+            return 0
+        if isinstance(date, basestring) and date == 'never':
+            return 0
+
+        # Convert date to days since UNIX epoch.
+        epoch = datetime.date(1970, 1, 1)
+        days = (date - epoch).total_seconds() / (24 * 60 * 60)
+        return round(days)
+
+    @property
+    def cpp_guard(self):
+        return self._definition.get('cpp_guard')
+
+    @property
+    def enum_labels(self):
+        def enum(method_name, object_name):
+            m = convert_to_cpp_identifier(method_name, "_")
+            o = convert_to_cpp_identifier(object_name, "_")
+            return m + '_' + o
+        combinations = itertools.product(self.methods, self.objects)
+        return [enum(t[0], t[1]) for t in combinations]
+
+    @property
+    def dataset(self):
+        """Get the nsITelemetry constant equivalent for release_channel_collection.
+        """
+        rcc = self._definition.get('release_channel_collection', 'opt-in')
+        if rcc == 'opt-out':
+            return 'nsITelemetry::DATASET_RELEASE_CHANNEL_OPTOUT'
+        else:
+            return 'nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN'
+
+    @property
+    def extra_keys(self):
+        return self._definition.get('extra_keys', {}).keys()
+
+def load_events(filename):
+    """Parses a YAML file containing the event definitions.
+
+    :param filename: the YAML file containing the event definitions.
+    :raises Exception: if the event file cannot be opened or parsed.
+    """
+
+    # Parse the event definitions from the YAML file.
+    events = None
+    try:
+        with open(filename, 'r') as f:
+            events = yaml.safe_load(f)
+    except IOError, e:
+        raise Exception('Error opening ' + filename + ': ' + e.message)
+    except ValueError, e:
+        raise Exception('Error parsing events in ' + filename + ': ' + e.message)
+
+    event_list = []
+
+    # Events are defined in a fixed two-level hierarchy within the definition file.
+    # The first level contains the category (group name), while the second level contains the
+    # event definitions (e.g. "category.name: [<event definition>, ...], ...").
+    for category_name,category in events.iteritems():
+        string_check('', 'category', category_name, 1, MAX_CATEGORY_NAME_LENGTH, regex=IDENTIFIER_PATTERN)
+
+        # Make sure that the category has at least one entry in it.
+        if not category or len(category) == 0:
+            raise ValueError(category_name + ' must contain at least one entry')
+
+        for entry in category:
+            event_list.append(EventData(category_name, entry))
+
+    return event_list