pw_thread: Add estimated max stack usage support

This adds a estimated max or stack usage to the thread snapshot proto.
For the embOS thread backend, store the max stack pointer in the new
field.

Fix: b/199208763
No-Docs-Update-Reason: Minor enhancement
Testing: Able to for verify by running production SW, forcing a crash,
and retrieving snapshot.
Unit tests are updated and pass.

Change-Id: Iaafe540201d8e7e16b2bfa4e83458893f2ba8dd6
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/60246
Commit-Queue: Jennifer Silva <jennifersilva@google.com>
Commit-Queue: Keir Mierle <keir@google.com>
Reviewed-by: Armando Montanez <amontanez@google.com>
diff --git a/pw_cpu_exception_cortex_m/snapshot.cc b/pw_cpu_exception_cortex_m/snapshot.cc
index db64016..d350bac 100644
--- a/pw_cpu_exception_cortex_m/snapshot.cc
+++ b/pw_cpu_exception_cortex_m/snapshot.cc
@@ -67,6 +67,7 @@
       .stack_low_addr = stack_low_addr,
       .stack_high_addr = stack_high_addr,
       .stack_pointer = stack_pointer,
+      .stack_pointer_est_peak = std::nullopt,
   };
   return thread::SnapshotStack(thread_ctx, encoder, thread_stack_callback);
 }
diff --git a/pw_thread/public/pw_thread/snapshot.h b/pw_thread/public/pw_thread/snapshot.h
index ed3f4ef..215429f 100644
--- a/pw_thread/public/pw_thread/snapshot.h
+++ b/pw_thread/public/pw_thread/snapshot.h
@@ -34,6 +34,7 @@
   uintptr_t stack_low_addr;
   uintptr_t stack_high_addr;
   uintptr_t stack_pointer;
+  std::optional<uintptr_t> stack_pointer_est_peak;
 };
 
 // Takes the provided StackContext, and writes stack context to the provided
diff --git a/pw_thread/pw_thread_protos/thread.proto b/pw_thread/pw_thread_protos/thread.proto
index 32b1df7..230a515 100644
--- a/pw_thread/pw_thread_protos/thread.proto
+++ b/pw_thread/pw_thread_protos/thread.proto
@@ -95,6 +95,12 @@
   // CPU usage info. This is the percentage of CPU time the thread has been
   // active in hundredths of a percent. (e.g. 5.00% = 500u)
   optional uint32 cpu_usage_hundredths = 10;
+
+  // The address of highest estimated currently used in the thread stack.
+  // Percentage of bytes used can be calculated by:
+  // (stack_estimate_max_addr-stack_start_pointer) /
+  // (stack_end_pointer-stack_start_pointer) * 100%
+  optional uint64 stack_pointer_est_peak = 11;
 }
 
 // This message overlays the pw.snapshot.Snapshot proto. It's valid to encode
diff --git a/pw_thread/py/pw_thread/thread_analyzer.py b/pw_thread/py/pw_thread/thread_analyzer.py
index 3ca209a..1bd8a38 100644
--- a/pw_thread/py/pw_thread/thread_analyzer.py
+++ b/pw_thread/py/pw_thread/thread_analyzer.py
@@ -67,6 +67,18 @@
         used_str += f', {100*self.stack_used()/self.stack_size_limit():.2f}%'
         return used_str
 
+    def _stack_pointer_est_peak_str(self) -> str:
+        if not self.has_stack_pointer_est_peak():
+            return 'size unknown'
+
+        high_used_str = f'{self.stack_pointer_est_peak()} bytes'
+        if not self.has_stack_size_limit():
+            return high_used_str
+        high_water_mark_percent = (100 * self.stack_pointer_est_peak() /
+                                   self.stack_size_limit())
+        high_used_str += f', {high_water_mark_percent:.2f}%'
+        return high_used_str
+
     def _stack_used_range_str(self) -> str:
         start_str = (f'0x{self._thread.stack_start_pointer:08x}'
                      if self._thread.HasField('stack_start_pointer') else
@@ -124,11 +136,29 @@
         return abs(self._thread.stack_start_pointer -
                    self._thread.stack_pointer)
 
+    def has_stack_pointer_est_peak(self) -> bool:
+        """Returns true if there's enough info to calculate estimate
+        used stack.
+        """
+        return (self._thread.HasField('stack_start_pointer')
+                and self._thread.HasField('stack_pointer_est_peak'))
+
+    def stack_pointer_est_peak(self) -> int:
+        """Returns the max estimated used stack usage in bytes.
+
+        Precondition:
+            has_stack_estimated_used_bytes() must be true.
+        """
+        assert self.has_stack_pointer_est_peak(), 'Missing stack est. peak'
+        return abs(self._thread.stack_start_pointer -
+                   self._thread.stack_pointer_est_peak)
+
     def __str__(self) -> str:
         output = [
             'Stack info',
-            f'  Stack used:   {self._stack_used_range_str()}',
-            f'  Stack limits: {self._stack_limit_range_str()}',
+            f'  Stack cur used:  {self._stack_used_range_str()}',
+            f'  Stack max used:  {self._stack_pointer_est_peak_str()}',
+            f'  Stack limits:    {self._stack_limit_range_str()}',
         ]
         return '\n'.join(output)
 
diff --git a/pw_thread/py/thread_analyzer_test.py b/pw_thread/py/thread_analyzer_test.py
index 431f97e..59918fe 100644
--- a/pw_thread/py/thread_analyzer_test.py
+++ b/pw_thread/py/thread_analyzer_test.py
@@ -25,8 +25,9 @@
         stack_info = StackInfo(thread_pb2.Thread())
         expected = '\n'.join(
             ('Stack info',
-             '  Stack used:   0x???????? - 0x???????? (size unknown)',
-             '  Stack limits: 0x???????? - 0x???????? (size unknown)'))
+             '  Stack cur used:  0x???????? - 0x???????? (size unknown)',
+             '  Stack max used:  size unknown',
+             '  Stack limits:    0x???????? - 0x???????? (size unknown)'))
         self.assertFalse(stack_info.has_stack_size_limit())
         self.assertFalse(stack_info.has_stack_used())
         self.assertEqual(expected, str(stack_info))
@@ -38,8 +39,9 @@
 
         expected = '\n'.join(
             ('Stack info',
-             '  Stack used:   0x???????? - 0x5ac6a86c (size unknown)',
-             '  Stack limits: 0x???????? - 0x???????? (size unknown)'))
+             '  Stack cur used:  0x???????? - 0x5ac6a86c (size unknown)',
+             '  Stack max used:  size unknown',
+             '  Stack limits:    0x???????? - 0x???????? (size unknown)'))
         self.assertFalse(stack_info.has_stack_size_limit())
         self.assertFalse(stack_info.has_stack_used())
         self.assertEqual(expected, str(stack_info))
@@ -52,8 +54,9 @@
 
         expected = '\n'.join(
             ('Stack info',
-             '  Stack used:   0x5ac6b86c - 0x5ac6a86c (4096 bytes)',
-             '  Stack limits: 0x5ac6b86c - 0x???????? (size unknown)'))
+             '  Stack cur used:  0x5ac6b86c - 0x5ac6a86c (4096 bytes)',
+             '  Stack max used:  size unknown',
+             '  Stack limits:    0x5ac6b86c - 0x???????? (size unknown)'))
         self.assertFalse(stack_info.has_stack_size_limit())
         self.assertTrue(stack_info.has_stack_used())
         self.assertEqual(expected, str(stack_info))
@@ -67,8 +70,9 @@
 
         expected = '\n'.join(
             ('Stack info',
-             '  Stack used:   0x5ac6b86c - 0x5ac6a86c (4096 bytes, 50.00%)',
-             '  Stack limits: 0x5ac6b86c - 0x5ac6986c (8192 bytes)'))
+             '  Stack cur used:  0x5ac6b86c - 0x5ac6a86c (4096 bytes, 50.00%)',
+             '  Stack max used:  size unknown',
+             '  Stack limits:    0x5ac6b86c - 0x5ac6986c (8192 bytes)'))
         self.assertTrue(stack_info.has_stack_size_limit())
         self.assertTrue(stack_info.has_stack_used())
         self.assertEqual(expected, str(stack_info))
@@ -89,8 +93,9 @@
             '',
             'Thread (UNKNOWN): [unnamed thread]',
             'Stack info',
-            '  Stack used:   0x???????? - 0x???????? (size unknown)',
-            '  Stack limits: 0x???????? - 0x???????? (size unknown)',
+            '  Stack cur used:  0x???????? - 0x???????? (size unknown)',
+            '  Stack max used:  size unknown',
+            '  Stack limits:    0x???????? - 0x???????? (size unknown)',
             '',
         ))
         analyzer = ThreadSnapshotAnalyzer(snapshot)
@@ -107,6 +112,7 @@
         temp_thread.stack_start_pointer = 0x2001ac00
         temp_thread.stack_end_pointer = 0x2001aa00
         temp_thread.stack_pointer = 0x2001ab0c
+        temp_thread.stack_pointer_est_peak = 0x2001aa00
         snapshot.threads.append(temp_thread)
 
         temp_thread = thread_pb2.Thread()
@@ -122,13 +128,15 @@
             '',
             'Thread (READY): Idle',
             'Stack info',
-            '  Stack used:   0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)',
-            '  Stack limits: 0x2001ac00 - 0x2001aa00 (512 bytes)',
+            '  Stack cur used:  0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)',
+            '  Stack max used:  512 bytes, 100.00%',
+            '  Stack limits:    0x2001ac00 - 0x2001aa00 (512 bytes)',
             '',
             'Thread (INTERRUPT_HANDLER): Main/Handler',
             'Stack info',
-            '  Stack used:   0x2001b000 - 0x2001ae20 (480 bytes)',
-            '  Stack limits: 0x2001b000 - 0x???????? (size unknown)',
+            '  Stack cur used:  0x2001b000 - 0x2001ae20 (480 bytes)',
+            '  Stack max used:  size unknown',
+            '  Stack limits:    0x2001b000 - 0x???????? (size unknown)',
             '',
         ))
         analyzer = ThreadSnapshotAnalyzer(snapshot)
@@ -145,6 +153,7 @@
         temp_thread.stack_start_pointer = 0x2001ac00
         temp_thread.stack_end_pointer = 0x2001aa00
         temp_thread.stack_pointer = 0x2001ab0c
+        temp_thread.stack_pointer_est_peak = 0x2001ac00 + 0x100
         snapshot.threads.append(temp_thread)
 
         temp_thread = thread_pb2.Thread()
@@ -152,6 +161,7 @@
         temp_thread.active = True
         temp_thread.stack_start_pointer = 0x2001b000
         temp_thread.stack_pointer = 0x2001ae20
+        temp_thread.stack_pointer_est_peak = 0x2001b000 + 0x200
         temp_thread.state = thread_pb2.ThreadState.Enum.INTERRUPT_HANDLER
         snapshot.threads.append(temp_thread)
 
@@ -163,13 +173,15 @@
             # Ensure the active thread is moved to the top of the list.
             'Thread (INTERRUPT_HANDLER): Main/Handler <-- [ACTIVE]',
             'Stack info',
-            '  Stack used:   0x2001b000 - 0x2001ae20 (480 bytes)',
-            '  Stack limits: 0x2001b000 - 0x???????? (size unknown)',
+            '  Stack cur used:  0x2001b000 - 0x2001ae20 (480 bytes)',
+            '  Stack max used:  512 bytes',
+            '  Stack limits:    0x2001b000 - 0x???????? (size unknown)',
             '',
             'Thread (READY): Idle',
             'Stack info',
-            '  Stack used:   0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)',
-            '  Stack limits: 0x2001ac00 - 0x2001aa00 (512 bytes)',
+            '  Stack cur used:  0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)',
+            '  Stack max used:  256 bytes, 50.00%',
+            '  Stack limits:    0x2001ac00 - 0x2001aa00 (512 bytes)',
             '',
         ))
         analyzer = ThreadSnapshotAnalyzer(snapshot)
diff --git a/pw_thread/snapshot.cc b/pw_thread/snapshot.cc
index 67fb8b7..ec59ed9 100644
--- a/pw_thread/snapshot.cc
+++ b/pw_thread/snapshot.cc
@@ -35,6 +35,9 @@
   encoder.WriteStackStartPointer(stack.stack_high_addr);
   encoder.WriteStackEndPointer(stack.stack_low_addr);
   encoder.WriteStackPointer(stack.stack_pointer);
+  if (stack.stack_pointer_est_peak.has_value()) {
+    encoder.WriteStackPointerEstPeak(stack.stack_pointer_est_peak.value());
+  }
   PW_LOG_DEBUG("Active stack: 0x%08x-0x%08x (%ld bytes)",
                stack.stack_high_addr,
                stack.stack_pointer,
diff --git a/pw_thread_embos/snapshot.cc b/pw_thread_embos/snapshot.cc
index d535ab5..1a9e17d 100644
--- a/pw_thread_embos/snapshot.cc
+++ b/pw_thread_embos/snapshot.cc
@@ -132,6 +132,8 @@
       .stack_pointer = reinterpret_cast<uintptr_t>(
           ThreadIsRunning(thread) ? running_thread_stack_pointer
                                   : thread.pStack),
+      .stack_pointer_est_peak = reinterpret_cast<uintptr_t>(thread.pStackBot) +
+                                thread.StackSize - OS_GetStackUsed(&thread),
   };
 
   return SnapshotStack(thread_ctx, encoder, thread_stack_callback);
diff --git a/pw_thread_freertos/snapshot.cc b/pw_thread_freertos/snapshot.cc
index 3c2b98b..d597715 100644
--- a/pw_thread_freertos/snapshot.cc
+++ b/pw_thread_freertos/snapshot.cc
@@ -132,6 +132,7 @@
       .stack_low_addr = stack_low_addr,
       .stack_high_addr = reinterpret_cast<uintptr_t>(tcb.pxEndOfStack),
       .stack_pointer = stack_pointer,
+      .stack_pointer_est_peak = std::nullopt,
   };
   return SnapshotStack(thread_ctx, encoder, thread_stack_callback);
 #else
diff --git a/pw_thread_threadx/snapshot.cc b/pw_thread_threadx/snapshot.cc
index cfd79a4..f38e16d 100644
--- a/pw_thread_threadx/snapshot.cc
+++ b/pw_thread_threadx/snapshot.cc
@@ -144,6 +144,7 @@
       .stack_pointer = reinterpret_cast<uintptr_t>(
           ThreadIsRunning(thread) ? running_thread_stack_pointer
                                   : thread.tx_thread_stack_ptr),
+      .stack_pointer_est_peak = std::nullopt,
   };
 
   return SnapshotStack(thread_ctx, encoder, thread_stack_callback);