TimeCheck: Dump TimerThread tasks on timeout

On timeout, shows pending and retired tasks.
Fix race condition on TimerThread construction.
Split mutexes for better concurrency.
Track tasks that don't have timeout.
Enable dumping of pending requests.
Enable dumping of recent retired requests.
Format time in HH:MM:SS.MSc system_clock easier debugging.
Add default TimeCheck constructor that does nothing.

Test: atest libmediautils_test
Test: atest methodstatistics_tests
Test: atest timecheck_tests
Test: adb shell dumpsys media.audio_flinger
Test: adb shell dumpsys media.audio_policy
Test: adb shell dumpsys media.metrics
Bug: 219958414
Change-Id: I25742006578448813e557cdd20e304b9be286964
diff --git a/media/utils/include/mediautils/TimerThread.h b/media/utils/include/mediautils/TimerThread.h
index acf0b16..2dfe499 100644
--- a/media/utils/include/mediautils/TimerThread.h
+++ b/media/utils/include/mediautils/TimerThread.h
@@ -16,53 +16,225 @@
 
 #pragma once
 
+#include <atomic>
 #include <condition_variable>
+#include <deque>
 #include <functional>
 #include <map>
 #include <mutex>
+#include <string>
 #include <thread>
 
 #include <android-base/thread_annotations.h>
 
-namespace android {
+namespace android::mediautils {
 
 /**
  * A thread for deferred execution of tasks, with cancellation.
  */
 class TimerThread {
   public:
+    // A Handle is a time_point that serves as a unique key.  It is ordered.
     using Handle = std::chrono::steady_clock::time_point;
 
-    TimerThread();
-    ~TimerThread();
+    static inline constexpr Handle INVALID_HANDLE =
+            std::chrono::steady_clock::time_point::min();
 
     /**
-     * Schedule a task to be executed in the future (`timeout` duration from now).
-     * Returns a handle that can be used for cancellation.
+     * Schedules a task to be executed in the future (`timeout` duration from now).
+     *
+     * \param tag     string associated with the task.  This need not be unique,
+     *                as the Handle returned is used for cancelling.
+     * \param func    callback function that is invoked at the timeout.
+     * \param timeout timeout duration which is converted to milliseconds with at
+     *                least 45 integer bits.
+     *                A timeout of 0 (or negative) means the timer never expires
+     *                so func() is never called. These tasks are stored internally
+     *                and reported in the toString() until manually cancelled.
+     * \returns       a handle that can be used for cancellation.
      */
-    template <typename R, typename P>
-    Handle scheduleTask(std::function<void()>&& func, std::chrono::duration<R, P> timeout) {
-        auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout);
-        return scheduleTaskAtDeadline(std::move(func), deadline);
-    }
+    Handle scheduleTask(
+            std::string tag, std::function<void()>&& func, std::chrono::milliseconds timeout);
 
     /**
-     * Cancel a task, previously scheduled with scheduleTask().
-     * If the task has already executed, this is a no-op and returns false.
+     * Tracks a task that shows up on toString() until cancelled.
+     *
+     * \param tag     string associated with the task.
+     * \returns       a handle that can be used for cancellation.
+     */
+    Handle trackTask(std::string tag);
+
+    /**
+     * Cancels a task previously scheduled with scheduleTask()
+     * or trackTask().
+     *
+     * \returns true if cancelled. If the task has already executed
+     *          or if the handle doesn't exist, this is a no-op
+     *          and returns false.
      */
     bool cancelTask(Handle handle);
 
+    std::string toString(size_t retiredCount = SIZE_MAX) const;
+
+    /**
+     * Returns a string representation of the TimerThread queue.
+     *
+     * The queue is dumped in order of scheduling (not deadline).
+     */
+    std::string pendingToString() const;
+
+    /**
+     * Returns a string representation of the last retired tasks.
+     *
+     * These tasks from trackTask() or scheduleTask() are
+     * cancelled.
+     *
+     * These are ordered when the task was retired.
+     *
+     * \param n is maximum number of tasks to dump.
+     */
+    std::string retiredToString(size_t n = SIZE_MAX) const;
+
+
+    /**
+     * Returns a string representation of the last timeout tasks.
+     *
+     * These tasks from scheduleTask() which have  timed-out.
+     *
+     * These are ordered when the task had timed-out.
+     *
+     * \param n is maximum number of tasks to dump.
+     */
+    std::string timeoutToString(size_t n = SIZE_MAX) const;
+
+    /**
+     * Dumps a container with SmartPointer<Request> to a string.
+     *
+     * "{ Request1 } { Request2} ...{ RequestN }"
+     */
+    template <typename T>
+    static std::string requestsToString(const T& containerRequests) {
+        std::string s;
+        // append seems to be faster than stringstream.
+        // https://stackoverflow.com/questions/18892281/most-optimized-way-of-concatenation-in-strings
+        for (const auto& request : containerRequests) {
+            s.append("{ ").append(request->toString()).append(" } ");
+        }
+        // If not empty, there's an extra space at the end, so we trim it off.
+        if (!s.empty()) s.pop_back();
+        return s;
+    }
+
   private:
-    using TimePoint = std::chrono::steady_clock::time_point;
+    // To minimize movement of data, we pass around shared_ptrs to Requests.
+    // These are allocated and deallocated outside of the lock.
+    struct Request {
+        const std::chrono::system_clock::time_point scheduled;
+        const std::chrono::system_clock::time_point deadline; // deadline := scheduled + timeout
+                                                              // if deadline == scheduled, no
+                                                              // timeout, task not executed.
+        const pid_t tid;
+        const std::string tag;
 
-    std::condition_variable mCond;
-    std::mutex mMutex;
-    std::thread mThread;
-    std::map<TimePoint, std::function<void()>> mMonitorRequests GUARDED_BY(mMutex);
-    bool mShouldExit GUARDED_BY(mMutex) = false;
+        std::string toString() const;
+    };
 
-    void threadFunc();
-    Handle scheduleTaskAtDeadline(std::function<void()>&& func, TimePoint deadline);
+    // Deque of requests, in order of add().
+    // This class is thread-safe.
+    class RequestQueue {
+      public:
+        explicit RequestQueue(size_t maxSize)
+            : mRequestQueueMax(maxSize) {}
+
+        void add(std::shared_ptr<const Request>);
+
+        // return up to the last "n" requests retired.
+        void copyRequests(std::vector<std::shared_ptr<const Request>>& requests,
+            size_t n = SIZE_MAX) const;
+
+      private:
+        const size_t mRequestQueueMax;
+        mutable std::mutex mRQMutex;
+        std::deque<std::pair<std::chrono::system_clock::time_point,
+                             std::shared_ptr<const Request>>>
+                mRequestQueue GUARDED_BY(mRQMutex);
+    };
+
+    // A storage map of tasks without timeouts.  There is no std::function<void()>
+    // required, it just tracks the tasks with the tag, scheduled time and the tid.
+    // These tasks show up on a pendingToString() until manually cancelled.
+    class NoTimeoutMap {
+        // This a counter of the requests that have no timeout (timeout == 0).
+        std::atomic<size_t> mNoTimeoutRequests{};
+
+        mutable std::mutex mNTMutex;
+        std::map<Handle, std::shared_ptr<const Request>> mMap GUARDED_BY(mNTMutex);
+
+      public:
+        bool isValidHandle(Handle handle) const; // lock free
+        Handle add(std::shared_ptr<const Request> request);
+        std::shared_ptr<const Request> remove(Handle handle);
+        void copyRequests(std::vector<std::shared_ptr<const Request>>& requests) const;
+    };
+
+    // Monitor thread.
+    // This thread manages shared pointers to Requests and a function to
+    // call on timeout.
+    // This class is thread-safe.
+    class MonitorThread {
+        mutable std::mutex mMutex;
+        mutable std::condition_variable mCond;
+
+        // Ordered map of requests based on time of deadline.
+        //
+        std::map<Handle, std::pair<std::shared_ptr<const Request>, std::function<void()>>>
+                mMonitorRequests GUARDED_BY(mMutex);
+
+        RequestQueue& mTimeoutQueue; // locked internally, added to when request times out.
+
+        // Worker thread variables
+        bool mShouldExit GUARDED_BY(mMutex) = false;
+
+        // To avoid race with initialization,
+        // mThread should be initialized last as the thread is launched immediately.
+        std::thread mThread;
+
+        void threadFunc();
+        Handle getUniqueHandle_l(std::chrono::milliseconds timeout) REQUIRES(mMutex);
+
+      public:
+        MonitorThread(RequestQueue &timeoutQueue);
+        ~MonitorThread();
+
+        Handle add(std::shared_ptr<const Request> request, std::function<void()>&& func,
+                std::chrono::milliseconds timeout);
+        std::shared_ptr<const Request> remove(Handle handle);
+        void copyRequests(std::vector<std::shared_ptr<const Request>>& requests) const;
+    };
+
+    std::vector<std::shared_ptr<const Request>> getPendingRequests() const;
+
+    // A no-timeout request is represented by a handles at the end of steady_clock time,
+    // counting down by the number of no timeout requests previously requested.
+    // We manage them on the NoTimeoutMap, but conceptually they could be scheduled
+    // on the MonitorThread because those time handles won't expire in
+    // the lifetime of the device.
+    static inline Handle getIndexedHandle(size_t index) {
+        return std::chrono::time_point<std::chrono::steady_clock>::max() -
+                    std::chrono::time_point<std::chrono::steady_clock>::duration(index);
+    }
+
+    static constexpr size_t kRetiredQueueMax = 16;
+    RequestQueue mRetiredQueue{kRetiredQueueMax};  // locked internally
+
+    static constexpr size_t kTimeoutQueueMax = 16;
+    RequestQueue mTimeoutQueue{kTimeoutQueueMax};  // locked internally
+
+    NoTimeoutMap mNoTimeoutMap;  // locked internally
+
+    MonitorThread mMonitorThread{mTimeoutQueue};  // This should be initialized last because
+                                                  // the thread is launched immediately.
+                                                  // Locked internally.
 };
 
-}  // namespace android
+}  // namespace android::mediautils