diff --git a/sunnypilot/common/README.md b/sunnypilot/common/README.md index 9068a75ea6..b33e39cd81 100644 --- a/sunnypilot/common/README.md +++ b/sunnypilot/common/README.md @@ -1,20 +1,18 @@ - # Comparative Analysis of Parameter Access Methods: `Params::get` vs. `ParamWatcher` ## Inefficiencies in Standard Parameter Access The standard `Params::get()` method executes a full file I/O lifecycle—opening, allocating, reading, and closing—for every function call. This approach results in significant CPU overhead and memory churn due to the frequency of these operations in the user interface loop. ### System Overhead Analysis -* **System Call Overhead**: Every read operation requires context switches into kernel mode. The `Params::get` function calls `util::read_file` (sunnypilot, 2024), which subsequently invokes `std::ifstream` (sunnypilot, 2024). - * *Impact*: Frequent context switching degrades performance (Linux man-pages, n.d.-a; Linux man-pages, n.d.-b). -* **C++ Stream Overhead**: The use of `std::ifstream` introduces additional overhead for maintaining stream state and buffering compared to raw file descriptors (cppreference.com, n.d.-a; Codezup, n.d.). -* **Memory Churn**: The instantiation of `std::string result(size, '\0');` forces heap allocation and deallocation during every call (sunnypilot, 2024). This stresses the memory allocator and can lead to fragmentation (cppreference.com, n.d.). +* **System Call Overhead**: Every read operation requires context switches into kernel mode. The `Params::get` function calls `util::read_file` (sunnypilot, 2025), which subsequently invokes `std::ifstream` (sunnypilot, 2025). +* **Impact**: Frequent context switching degrades performance (Linux man-pages, 2025 -a; Linux man-pages, 2025 -b). +* **C++ Stream Overhead**: The use of `std::ifstream` introduces additional overhead for maintaining stream state and buffering compared to raw file descriptors (cppreference.com, n.d.-a; Codezup, 2025). +* **Memory Churn**: The instantiation of `std::string result(size, '\0');` forces heap allocation and deallocation during every call (sunnypilot, 2025). This stresses the memory allocator and can lead to fragmentation (cppreference.com, n.d.). -## The `ParamWatcher` Optimization +## The ParamWatcher Optimization The `ParamWatcher` implementation utilizes OS-level file system events, such as `inotify` on Linux or `FSEvents` on macOS, to maintain a Random Access Memory (RAM) cache. This architecture eliminates the need for continuous polling. ### Performance Comparison - | Feature | Standard `Params::get` | Optimized `ParamWatcher` | | :--- | :--- | :--- | | **Workflow** | `open` → `malloc` → `read` → `close` | `dict.get()` (RAM lookup) | @@ -29,19 +27,11 @@ Standard C++ modules like `std::ifstream` are optimized for **throughput**—rea * **The Memory Trap**: The `std::string` class allocates memory on the heap. Repeated allocation creates short-lived objects, which in C++ fragments memory. In Python (which wraps this), it triggers the Garbage Collector, pausing the UI. * **The Query Mismatch**: `Params::get` queries the current value every frame, whereas `ParamWatcher` waits for a notification of change, serving cached values in the interim. -## Limitations and Trade-offs -While `ParamWatcher` offers superior performance for UI rendering, it presents specific trade-offs: - -* **Static RAM Usage**: `ParamWatcher` maintains a persistent dictionary cache of all accessed parameters (~50KB), whereas `Params::get` uses zero static RAM but incurs high dynamic memory access. -* **Event Latency**: In high-load scenarios, `inotify` events may experience slight delays or coalescing compared to direct reads. However, for user interface applications, this latency (<10ms) is imperceptible. -* **Complexity**: The solution requires managing a singleton background thread and OS-specific event loops, increasing code complexity compared to the synchronous `Params::get` function. - -## 5. Implementation Analysis: `param_watcher.py` - +## Implementation Analysis The `ParamWatcher` class provides a cross-platform solution for monitoring file system changes, specifically targeting the parameter files used in Openpilot. The implementation leverages the `ctypes` library to interface directly with operating system kernels, bypassing higher-level abstractions for maximum performance. ### Linux Implementation (`_run_linux`) -The Linux implementation interacts directly with the kernel's `inotify` subsystem (Linux man-pages, n.d.-c). +The Linux implementation interacts directly with the kernel's `inotify` subsystem (Linux man-pages, 2025 -c). * **Library Loading**: `libc = ctypes.CDLL('libc.so.6')` loads the standard C library to access system calls. * **Initialization**: `inotify_init()` is called to create a new inotify instance, returning a file descriptor. @@ -62,14 +52,13 @@ The macOS implementation uses the `FSEvents` API from the `CoreServices` framewo * **Execution**: `CFRunLoopRun()` starts the event loop. This passes control to the OS, which wakes the thread only when necessary. * **Handling**: Inside the callback, the code iterates through the changed paths provided by the OS. It extracts the filename and calls `_trigger_callbacks` to invalidate the cache for that specific parameter. -### Python `ctypes` Integration -The use of `ctypes` (Python Software Foundation, n.d.) is a strategic choice. It allows the Python interpreter to load shared libraries (`libc.so.6` on Linux, `CoreServices` on macOS) and call C functions directly. This approach avoids the overhead of spawning subprocesses or compiling external C extensions, keeping the codebase pure Python while achieving C-level system integration. +### Python ctypes Integration +The use of `ctypes` (Python Software Foundation, 2025) is a strategic choice. It allows the Python interpreter to load shared libraries (`libc.so.6` on Linux, `CoreServices` on macOS) and call C functions directly. This approach avoids the overhead of spawning subprocesses or compiling external C extensions, keeping the codebase pure Python while achieving C-level system integration. ### Memory Impact Analysis - With 232 defined parameters in `param_keys.h`, the maximum static RAM footprint of `ParamWatcher` is estimated to be **less than 250 KB**. Even if every single parameter were cached simultaneously, this static usage is negligible. Crucially, this stable footprint is likely more probable to maintain no trend of memory increase, whenc compared to the standard `Params::get` approach, which generates **megabytes** of short-lived "garbage" allocations per second, forcing the Python Garbage Collector to pause execution repeatedly. + With 232 defined parameters in `param_keys.h`, the maximum static RAM footprint of `ParamWatcher` is estimated to be **less than 250 KB**. Even if every single parameter were cached simultaneously, this static usage is negligible. Importantly, this stable footprint is likely more probable to maintain no trend of memory increase, whenc compared to the standard `Params::get` approach, which generates **megabytes** of short-lived "garbage" allocations per second, forcing the Python Garbage Collector to pause execution repeatedly. ## Architectural Integration: The Process-Local Singleton Pattern - To ensure resource efficiency within openpilot's multi-process architecture (e.g., `ui`, `controlsd`, `modeld`), `ParamWatcher` implements the Singleton design pattern (Gamma et al., 1994) using the Python `__new__` allocator. ### Process Isolation and Concurrency @@ -85,31 +74,37 @@ Runtime analysis demonstrates that multiple instantiation attempts result in a s * **Result**: Both instances report the exact same memory address (`4814358960`) and share the same background thread ID (`6114635776`). * **Impact**: The system incurs the overhead of the watcher thread (measured at < 0.1% CPU idle usage) only once per active process, regardless of import frequency. The average CPU usage across one minute was 0.002%. +## Limitations and Trade-offs +While `ParamWatcher` offers superior performance for UI rendering, it presents specific trade-offs: + +* **Static RAM Usage**: `ParamWatcher` maintains a persistent dictionary cache of all accessed parameters (~50KB), whereas `Params::get` uses zero static RAM but incurs high dynamic memory access. +* **Event Latency**: In high-load scenarios, `inotify` events may experience slight delays or coalescing compared to direct reads. However, for user interface applications, this latency (<10ms) is imperceptible. +* **Complexity**: The solution (the process singleton approach) requires managing a background thread and OS-specific event loops, increasing code complexity compared to the synchronous `Params::get` function. + ## Conclusion Replacing polling mechanisms with event-driven caching shifts the computational load from kernel space (syscalls) to user space (RAM). This transition eliminates I/O overhead and UI stutters caused by garbage collection, resulting in a more responsive user experience. ## References - Apple Inc. (n.d.-a). *File System Events*. Retrieved from https://developer.apple.com/documentation/coreservices/file_system_events -Apple Inc. (n.d.-b). *CFRunLoop*. Retrieved from https://developer.apple.com/documentation/corefoundation/cfrunloop-rhk +Apple Inc. (n.d.-b). *CFRunLoop*. Retrieved from https://developer.apple.com/documentation/corefoundation/cfrunloop -Codezup. (n.d.). *Efficient File I/O in C++*. Retrieved from https://codezup.com/efficient-file-io-cpp-best-practices/ +Codezup. (2025). *Efficient File I/O in C++*. Retrieved from https://codezup.com/efficient-file-io-cpp-best-practices/ cppreference.com. (n.d.-a). *std::basic_ifstream*. Retrieved from https://en.cppreference.com/w/cpp/io/basic_ifstream cppreference.com. (n.d.-b). *std::basic_string*. Retrieved from https://en.cppreference.com/w/cpp/string/basic_string/basic_string -Linux man-pages. (n.d.-a). *open(2)*. Retrieved from https://man7.org/linux/man-pages/man2/open.2.html +Linux man-pages. (2025 -a). *open(2)*. Retrieved from https://man7.org/linux/man-pages/man2/open.2.html -Linux man-pages. (n.d.-b). *read(2)*. Retrieved from https://man7.org/linux/man-pages/man2/read.2.html +Linux man-pages. (2025-b). *read(2)*. Retrieved from https://man7.org/linux/man-pages/man2/read.2.html -Linux man-pages. (n.d.-c). *inotify(7)*. Retrieved from https://man7.org/linux/man-pages/man7/inotify.7.html +Linux man-pages. (2025 -c). *inotify(7)*. Retrieved from https://man7.org/linux/man-pages/man7/inotify.7.html -Python Software Foundation. (n.d.). *ctypes — A foreign function library for Python*. Retrieved from https://docs.python.org/3/library/ctypes.html +Python Software Foundation. (2025). *ctypes — A foreign function library for Python*. Retrieved from https://docs.python.org/3/library/ctypes.html Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley. -sunnypilot. (2025). *common/params.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/params.cc +sunnypilot. (2025). *common/params.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/params.cc#L180C1-L206C2 -sunnypilot. (2025). *common/util.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/util.cc +sunnypilot. (2025). *common/util.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/util.cc#L79C1-L117C2