pw_chrono: Add timestamp analyzer

Added timestamp analyzer change snapshot to support printing
timestamp.

Change-Id: I540c845718fb37e9ecb048cbcc70d3d114d57329
Requires: pigweed-internal:32003
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/95841
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Reviewed-by: Armando Montanez <amontanez@google.com>
Pigweed-Auto-Submit: Tina Mashhour <tmashhour@google.com>
diff --git a/pw_chrono/BUILD.bazel b/pw_chrono/BUILD.bazel
index f8a41f4..1952c57 100644
--- a/pw_chrono/BUILD.bazel
+++ b/pw_chrono/BUILD.bazel
@@ -19,6 +19,7 @@
     "pw_cc_test",
 )
 load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -102,6 +103,13 @@
     srcs = [
         "chrono.proto",
     ],
+    import_prefix = "pw_chrono_protos",
+    strip_import_prefix = "//pw_chrono",
+)
+
+py_proto_library(
+    name = "chrono_proto_pb2",
+    srcs = ["chrono.proto"],
 )
 
 pw_proto_library(
diff --git a/pw_chrono/BUILD.gn b/pw_chrono/BUILD.gn
index 6bfcd15..8c8004f 100644
--- a/pw_chrono/BUILD.gn
+++ b/pw_chrono/BUILD.gn
@@ -103,7 +103,7 @@
 
 pw_proto_library("protos") {
   sources = [ "chrono.proto" ]
-  prefix = "pw_chrono"
+  prefix = "pw_chrono_protos"
 }
 
 pw_doc_group("docs") {
diff --git a/pw_chrono/CMakeLists.txt b/pw_chrono/CMakeLists.txt
index b98c812..4f16a4e 100644
--- a/pw_chrono/CMakeLists.txt
+++ b/pw_chrono/CMakeLists.txt
@@ -60,7 +60,7 @@
   SOURCES
     chrono.proto
   PREFIX
-    pw_chrono
+    pw_chrono_protos
 )
 
 # TODO(ewout): Renable this once we've resolved the backend variable definition
diff --git a/pw_chrono/docs.rst b/pw_chrono/docs.rst
index 468b922..dea99da 100644
--- a/pw_chrono/docs.rst
+++ b/pw_chrono/docs.rst
@@ -527,6 +527,8 @@
 in device snapshots. Simplified capture utilies and host-side tooling to
 interpret this data are not yet provided by ``pw_chrono``.
 
+There is tooling that take these proto and make them more human readable.
+
 ---------------
 Software Timers
 ---------------
diff --git a/pw_chrono/py/BUILD.bazel b/pw_chrono/py/BUILD.bazel
new file mode 100644
index 0000000..4242501
--- /dev/null
+++ b/pw_chrono/py/BUILD.bazel
@@ -0,0 +1,37 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+    name = "pw_chrono",
+    srcs = [
+        "pw_chrono/__init__.py",
+        "pw_chrono/timestamp_analyzer.py",
+    ],
+    deps = [
+        "//pw_chrono:chrono_proto_pb2",
+    ],
+)
+
+py_test(
+    name = "timestamp_analyzer_test",
+    srcs = [
+        "timestamp_analyzer_test.py",
+    ],
+    deps = [
+        ":pw_chrono",
+        "//pw_chrono:chrono_proto_pb2",
+    ],
+)
diff --git a/pw_chrono/py/BUILD.gn b/pw_chrono/py/BUILD.gn
new file mode 100644
index 0000000..dbdb304
--- /dev/null
+++ b/pw_chrono/py/BUILD.gn
@@ -0,0 +1,35 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+import("$dir_pw_docgen/docs.gni")
+
+pw_python_package("py") {
+  generate_setup = {
+    metadata = {
+      name = "pw_chrono"
+      version = "0.0.1"
+    }
+  }
+
+  sources = [
+    "pw_chrono/__init__.py",
+    "pw_chrono/timestamp_analyzer.py",
+  ]
+  tests = [ "timestamp_analyzer_test.py" ]
+  python_deps = [ "..:protos.python" ]
+  pylintrc = "$dir_pigweed/.pylintrc"
+}
diff --git a/pw_chrono/py/pw_chrono/__init__.py b/pw_chrono/py/pw_chrono/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_chrono/py/pw_chrono/__init__.py
diff --git a/pw_chrono/py/pw_chrono/py.typed b/pw_chrono/py/pw_chrono/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_chrono/py/pw_chrono/py.typed
diff --git a/pw_chrono/py/pw_chrono/timestamp_analyzer.py b/pw_chrono/py/pw_chrono/timestamp_analyzer.py
new file mode 100644
index 0000000..d9d934b
--- /dev/null
+++ b/pw_chrono/py/pw_chrono/timestamp_analyzer.py
@@ -0,0 +1,64 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Library to analyze timestamp."""
+
+from typing import List
+import datetime
+from pw_chrono_protos import chrono_pb2
+
+_UTC_EPOCH = datetime.datetime(1970, 1, 1, 00, 00, 00)
+
+_UNKNOWN = chrono_pb2.EpochType.Enum.UNKNOWN
+_TIME_SINCE_BOOT = chrono_pb2.EpochType.Enum.TIME_SINCE_BOOT
+_UTC_WALL_CLOCK = chrono_pb2.EpochType.Enum.UTC_WALL_CLOCK
+
+
+def process_snapshot(serialized_snapshot: bytes):
+    captured_timestamps = chrono_pb2.SnapshotTimestamps()
+    captured_timestamps.ParseFromString(serialized_snapshot)
+    return timestamp_output(captured_timestamps)
+
+
+def timestamp_output(timestamps: chrono_pb2.SnapshotTimestamps):
+    output: List[str] = []
+    if not timestamps.timestamps:
+        return ''
+
+    plural = '' if len(timestamps.timestamps) == 1 else 's'
+    output.append(f'Snapshot capture timestamp{plural}')
+    for timepoint in timestamps.timestamps:
+        time = timestamp_snapshot_analyzer(timepoint)
+        clock_epoch_type = timepoint.clock_parameters.epoch_type
+        if clock_epoch_type == _TIME_SINCE_BOOT:
+            output.append(f'  Time since boot:   {time}')
+        elif clock_epoch_type == _UTC_WALL_CLOCK:
+            utc_time = time + _UTC_EPOCH
+            output.append(f'  UTC time:   {utc_time}')
+        else:
+            output.append(f'  Time since unknown epoch {_UNKNOWN}:   unknown')
+
+    return '\n'.join(output)
+
+
+def timestamp_snapshot_analyzer(
+        captured_timepoint: chrono_pb2.TimePoint) -> datetime.timedelta:
+    ticks = captured_timepoint.timestamp
+    clock_period = (
+        captured_timepoint.clock_parameters.tick_period_seconds_numerator /
+        captured_timepoint.clock_parameters.tick_period_seconds_denominator)
+    elapsed_seconds = ticks * clock_period
+
+    time_delta = datetime.timedelta(seconds=elapsed_seconds)
+
+    return time_delta
diff --git a/pw_chrono/py/timestamp_analyzer_test.py b/pw_chrono/py/timestamp_analyzer_test.py
new file mode 100644
index 0000000..3153516
--- /dev/null
+++ b/pw_chrono/py/timestamp_analyzer_test.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for the timestamp analyzer."""
+
+import unittest
+from pw_chrono.timestamp_analyzer import process_snapshot
+from pw_chrono_protos import chrono_pb2
+
+
+class TimestampTest(unittest.TestCase):
+    """Test for the timestamp analyzer."""
+    def test_no_timepoint(self):
+        time_stamps = chrono_pb2.SnapshotTimestamps()
+        self.assertEqual('', str(process_snapshot(time_stamps)))
+
+    def test_timestamp_unknown_epoch_type(self):
+        time_stamps = chrono_pb2.SnapshotTimestamps()
+
+        time_point = chrono_pb2.TimePoint()
+        unkown = chrono_pb2.EpochType.Enum.UNKNOWN
+        time_point.clock_parameters.epoch_type = unkown
+
+        time_stamps.timestamps.append(time_point)
+
+        expected = '\n'.join(('Snapshot capture timestamp',
+                              '    Time since unknown epoch 0:   unknown'))
+
+        self.assertEqual(expected, str(process_snapshot(time_stamps)))
+
+    def test_timestamp_with_time_since_boot(self):
+        time_stamps = chrono_pb2.SnapshotTimestamps()
+
+        time_point = chrono_pb2.TimePoint()
+        time_since_boot = chrono_pb2.EpochType.Enum.TIME_SINCE_BOOT
+        time_point.clock_parameters.epoch_type = time_since_boot
+        time_point.timestamp = 100
+        time_point.clock_parameters.tick_period_seconds_numerator = 1
+        time_point.clock_parameters.tick_period_seconds_denominator = 1000
+
+        time_stamps.timestamps.append(time_point)
+
+        expected = '\n'.join(
+            ('Snapshot capture timestamp', '  Time since boot:   2:24:00'))
+
+        self.assertEqual(expected, str(process_snapshot(time_stamps)))
+
+    def test_timestamp_with_utc_wall_clock(self):
+        time_stamps = chrono_pb2.SnapshotTimestamps()
+
+        time_point = chrono_pb2.TimePoint()
+        utc_wall_clock = chrono_pb2.EpochType.Enum.UTC_WALL_CLOCK
+        time_point.clock_parameters.epoch_type = utc_wall_clock
+        time_point.timestamp = 100
+        time_point.clock_parameters.tick_period_seconds_numerator = 1
+        time_point.clock_parameters.tick_period_seconds_denominator = 1000
+
+        time_stamps.timestamps.append(time_point)
+
+        expected = '\n'.join(('Snapshot capture timestamp',
+                              '  UTC time:   1970-01-01 02:24:00'))
+
+        self.assertEqual(expected, str(process_snapshot(time_stamps)))
+
+    def test_timestamp_with_time_since_boot_and_utc_wall_clock(self):
+        time_stamps = chrono_pb2.SnapshotTimestamps()
+
+        time_point = chrono_pb2.TimePoint()
+        time_since_boot = chrono_pb2.EpochType.Enum.TIME_SINCE_BOOT
+        time_point.clock_parameters.epoch_type = time_since_boot
+        time_point.timestamp = 100
+        time_point.clock_parameters.tick_period_seconds_numerator = 1
+        time_point.clock_parameters.tick_period_seconds_denominator = 1000
+        time_stamps.timestamps.append(time_point)
+
+        time_point = chrono_pb2.TimePoint()
+        utc_wall_clock = chrono_pb2.EpochType.Enum.UTC_WALL_CLOCK
+        time_point.clock_parameters.epoch_type = utc_wall_clock
+        time_point.timestamp = 100
+        time_point.clock_parameters.tick_period_seconds_numerator = 1
+        time_point.clock_parameters.tick_period_seconds_denominator = 1000
+        time_stamps.timestamps.append(time_point)
+
+        expected = '\n'.join(
+            ('Snapshot capture timestamps', '  Time since boot:   2:24:00',
+             '  UTC time:   1970-01-01 02:24:00'))
+
+        self.assertEqual(expected, str(process_snapshot(time_stamps)))
diff --git a/pw_env_setup/BUILD.gn b/pw_env_setup/BUILD.gn
index f7c2969..2e57404 100644
--- a/pw_env_setup/BUILD.gn
+++ b/pw_env_setup/BUILD.gn
@@ -34,6 +34,7 @@
     "$dir_pw_build/py",
     "$dir_pw_build_info/py",
     "$dir_pw_build_mcuxpresso/py",
+    "$dir_pw_chrono/py",
     "$dir_pw_cli/py",
     "$dir_pw_compilation_testing/py",
     "$dir_pw_console/py",
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index 3af3933..f18f956 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -420,6 +420,8 @@
     '-//pw_blob_store/...:all',
     '-//pw_boot/...:all',
     '-//pw_cpu_exception_cortex_m/...:all',
+    '-//pw_chrono:chrono_proto_pb2',
+    '-//pw_chrono/py/...:all',
     '-//pw_crypto/...:all',  # TODO(b/236321905) Remove when passing.
     '-//pw_file/...:all',
     '-//pw_function:function_test',  # TODO(b/241821115) Remove when passing.
diff --git a/pw_snapshot/pw_snapshot_protos/snapshot.proto b/pw_snapshot/pw_snapshot_protos/snapshot.proto
index 6c0277e..789400c 100644
--- a/pw_snapshot/pw_snapshot_protos/snapshot.proto
+++ b/pw_snapshot/pw_snapshot_protos/snapshot.proto
@@ -18,7 +18,7 @@
 option java_package = "pw.snapshot.proto";
 option java_outer_classname = "Snapshot";
 
-import "pw_chrono/chrono.proto";
+import "pw_chrono_protos/chrono.proto";
 import "pw_cpu_exception_cortex_m_protos/cpu_state.proto";
 import "pw_log/proto/log.proto";
 import "pw_thread_protos/thread.proto";
diff --git a/pw_snapshot/py/BUILD.bazel b/pw_snapshot/py/BUILD.bazel
index 30b6566..e8a4a83 100644
--- a/pw_snapshot/py/BUILD.bazel
+++ b/pw_snapshot/py/BUILD.bazel
@@ -25,6 +25,7 @@
     deps = [
         ":pw_snapshot_metadata",
         "//pw_build_info/py:pw_build_info",
+        "//pw_chrono/py:pw_chrono",
         "//pw_cpu_exception_cortex_m/py:exception_analyzer",
         "//pw_snapshot:snapshot_proto_py_pb2",
         "//pw_symbolizer/py:pw_symbolizer",
diff --git a/pw_snapshot/py/BUILD.gn b/pw_snapshot/py/BUILD.gn
index c5b731f..644cb05 100644
--- a/pw_snapshot/py/BUILD.gn
+++ b/pw_snapshot/py/BUILD.gn
@@ -52,6 +52,8 @@
   python_deps = [
     ":pw_snapshot_metadata",
     "$dir_pw_build_info/py",
+    "$dir_pw_chrono:protos.python",
+    "$dir_pw_chrono/py",
     "$dir_pw_cpu_exception_cortex_m/py",
     "$dir_pw_symbolizer/py",
     "$dir_pw_thread:protos.python",
diff --git a/pw_snapshot/py/pw_snapshot/processor.py b/pw_snapshot/py/pw_snapshot/processor.py
index d96691028..faa6f79 100644
--- a/pw_snapshot/py/pw_snapshot/processor.py
+++ b/pw_snapshot/py/pw_snapshot/processor.py
@@ -26,6 +26,7 @@
 from pw_snapshot_protos import snapshot_pb2
 from pw_symbolizer import LlvmSymbolizer, Symbolizer
 from pw_thread import thread_analyzer
+from pw_chrono import timestamp_analyzer
 
 _LOG = logging.getLogger('snapshot_processor')
 
@@ -85,9 +86,15 @@
 
     thread_info = thread_analyzer.process_snapshot(serialized_snapshot,
                                                    detokenizer, symbolizer)
+
     if thread_info:
         output.append(thread_info)
 
+    timestamp_info = timestamp_analyzer.process_snapshot(serialized_snapshot)
+
+    if timestamp_info:
+        output.append(timestamp_info)
+
     # Check and emit the number of related snapshots embedded in this snapshot.
     if snapshot.related_snapshots:
         snapshot_count = len(snapshot.related_snapshots)