From f99643e04514621195d5ddcfd931dbfccffd1804 Mon Sep 17 00:00:00 2001 From: maniek2332 Date: Mon, 15 Feb 2021 18:46:12 +0100 Subject: [PATCH 1/4] exposed statistics interface, added stats_graph tool --- kaacore | 2 +- src/kaa/CMakeLists.txt | 2 + src/kaa/_kaa.pyx | 1 + src/kaa/kaacore/statistics.pxd | 15 +++ src/kaa/statistics.pxi | 32 +++++ src/kaa/statistics.py | 1 + src/kaa/tools/__init__.py | 0 src/kaa/tools/stats_graph.py | 212 +++++++++++++++++++++++++++++++++ 8 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 src/kaa/kaacore/statistics.pxd create mode 100644 src/kaa/statistics.pxi create mode 100644 src/kaa/statistics.py create mode 100644 src/kaa/tools/__init__.py create mode 100644 src/kaa/tools/stats_graph.py diff --git a/kaacore b/kaacore index 0c32cf4e..b8549f07 160000 --- a/kaacore +++ b/kaacore @@ -1 +1 @@ -Subproject commit 0c32cf4e321daeef37f4a65a15c215e26fcf8835 +Subproject commit b8549f07b90ebf6b14b34932f8f99b37c2ea9a6e diff --git a/src/kaa/CMakeLists.txt b/src/kaa/CMakeLists.txt index a3de74e5..98612a31 100644 --- a/src/kaa/CMakeLists.txt +++ b/src/kaa/CMakeLists.txt @@ -27,6 +27,7 @@ set(CYTHON_FILES log.pxi easings.pxi spatial_index.pxi + statistics.pxi kaacore/__init__.pxd kaacore/engine.pxd @@ -53,6 +54,7 @@ set(CYTHON_FILES kaacore/easings.pxd kaacore/spatial_index.pxd kaacore/clock.pxd + kaacore/statistics.pxd extra/include/pythonic_callback.h extra/include/python_exceptions_wrapper.h diff --git a/src/kaa/_kaa.pyx b/src/kaa/_kaa.pyx index 25168625..97d0a883 100644 --- a/src/kaa/_kaa.pyx +++ b/src/kaa/_kaa.pyx @@ -25,4 +25,5 @@ include "display.pxi" include "window.pxi" include "audio.pxi" include "timers.pxi" +include "statistics.pxi" include "engine.pxi" diff --git a/src/kaa/kaacore/statistics.pxd b/src/kaa/kaacore/statistics.pxd new file mode 100644 index 00000000..61cd46bd --- /dev/null +++ b/src/kaa/kaacore/statistics.pxd @@ -0,0 +1,15 @@ +from libcpp.vector cimport vector +from libcpp.pair cimport pair +from libcpp.string cimport string + +from .exceptions cimport raise_py_error + + +ctypedef pair[string, double] CPairStatisticLastValue + + +cdef extern from "kaacore/statistics.h" nogil: + cdef cppclass CStatisticsManager "kaacore::StatisticsManager": + vector[CPairStatisticLastValue] get_last_all() except +raise_py_error + + cdef CStatisticsManager& c_get_global_statistics_manager "kaacore::get_global_statistics_manager"() diff --git a/src/kaa/statistics.pxi b/src/kaa/statistics.pxi new file mode 100644 index 00000000..9f7dabf0 --- /dev/null +++ b/src/kaa/statistics.pxi @@ -0,0 +1,32 @@ +from .kaacore.statistics cimport ( + CStatisticsManager, c_get_global_statistics_manager, CPairStatisticLastValue, +) + + +cdef class StatisticsManager: + cdef CStatisticsManager* c_statistics_manager + + @staticmethod + cdef create(CStatisticsManager* c_statistics_manager): + assert c_statistics_manager + cdef StatisticsManager statistics_manager = \ + StatisticsManager.__new__(StatisticsManager) + statistics_manager.c_statistics_manager = c_statistics_manager + return statistics_manager + + def get_last_all(self): + cdef list results = [] + cdef CPairStatisticLastValue pair + + for pair in self.c_statistics_manager.get_last_all(): + results.append((pair.first.decode(), pair.second)) + return results + + +cdef StatisticsManager _global_statistics_manager = StatisticsManager.create( + &c_get_global_statistics_manager() +) + + +def get_global_statistics_manager(): + return _global_statistics_manager diff --git a/src/kaa/statistics.py b/src/kaa/statistics.py new file mode 100644 index 00000000..f5cf0e24 --- /dev/null +++ b/src/kaa/statistics.py @@ -0,0 +1 @@ +from ._kaa import get_global_statistics_manager diff --git a/src/kaa/tools/__init__.py b/src/kaa/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/kaa/tools/stats_graph.py b/src/kaa/tools/stats_graph.py new file mode 100644 index 00000000..d94b164c --- /dev/null +++ b/src/kaa/tools/stats_graph.py @@ -0,0 +1,212 @@ +import re +import typing +import queue +import threading +import socket +import socketserver +import struct +from collections import defaultdict + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation + +DEFAULT_SERVER_HOST = '0.0.0.0' +DEFAULT_SERVER_PORT = 9771 + +SUBPLOT_PATTERNS = [ + re.compile(r':time$'), + re.compile(r':memory$'), +] +PLOT_ANIMATION_INTERVAL = 30 # ms +PLOT_FULL_REDRAW_INTERVAL = 100 # frames +PLOT_DISPLAYED_VALUES = 500 + +STAT_NAMES_ENCODING = 'utf-8' + + +class BinaryDatagramFormatter: + HEADER_STRUCT = struct.Struct('=12sHH16x') + STAT_SEGMENT_STRUCT = struct.Struct('=40sd') + + @classmethod + def parse(cls, datagram: bytes) \ + -> typing.List[typing.Tuple[str, float]]: + results: typing.List[typing.Tuple[str, float]] = [] + assert len(datagram) > cls.HEADER_STRUCT.size + + (magic_string, version, segments_count) = cls.HEADER_STRUCT.unpack_from(datagram) + assert magic_string == b'KAACOREstats' + assert version == 0x01 + assert segments_count > 0 + + expected_message_size = ( + cls.HEADER_STRUCT.size + + segments_count * cls.STAT_SEGMENT_STRUCT.size + ) + for i in range(segments_count): + stat_name, value = cls.STAT_SEGMENT_STRUCT.unpack_from( + datagram, offset=(cls.HEADER_STRUCT.size + + i * cls.STAT_SEGMENT_STRUCT.size) + ) + if (null_pos := stat_name.find(b'\0')) != -1: + stat_name = stat_name[:null_pos] + results.append((stat_name.decode(STAT_NAMES_ENCODING), value)) + + return results + + +class GraphingUDPServer(socketserver.UDPServer): + def __init__(self, server_address, RequestHandlerClass, *, + formatter_class, data_queue, bind_and_activate=True): + super().__init__(server_address=server_address, + RequestHandlerClass=RequestHandlerClass, + bind_and_activate=bind_and_activate) + self.data_queue = data_queue + self.formatter_class = formatter_class + + +class ServerRequestHandler(socketserver.DatagramRequestHandler): + def handle(self): + parsed_stats = self.server.formatter_class.parse(self.packet) + if parsed_stats: + self.server.data_queue.put_nowait(parsed_stats) + + +class GraphingServer: + def __init__(self, server_host: str = DEFAULT_SERVER_HOST, + server_port: int = DEFAULT_SERVER_PORT, + subplot_patterns=SUBPLOT_PATTERNS): + self.data_queue = queue.Queue() + self.graph = Graph( + data_queue=self.data_queue, + subplot_patterns=subplot_patterns, + ) + + self.udp_server = GraphingUDPServer( + (server_host, server_port), ServerRequestHandler, + formatter_class=BinaryDatagramFormatter, + data_queue=self.data_queue, + ) + self.udp_server_thread = threading.Thread( + target=self.udp_server.serve_forever, + ) + self.udp_server_thread.daemon = True + self.udp_server_thread.start() + + +class Graph: + def __init__(self, data_queue: queue.Queue, + subplot_patterns): + self.data_queue = data_queue + self.subplot_patterns = subplot_patterns + + needed_subplots_count = len(self.subplot_patterns) + 1 + self.figure, self.axes = plt.subplots(needed_subplots_count, 1) + plt.subplots_adjust(left=0.1, right=0.99, top=0.95, bottom=0.05) + for ax in self.axes: + ax.set_xlim(-PLOT_DISPLAYED_VALUES, 0) + ax.minorticks_on() + ax.grid(which='major', axis='y') + ax.grid(which='minor', axis='y', color='gray', linestyle='dotted', alpha=0.5) + ax.get_xaxis().set_visible(False) + + self.needs_redraw = False + + self.x_data = np.arange(-PLOT_DISPLAYED_VALUES, 0) + 1 + self.y_data = np.ndarray((0, PLOT_DISPLAYED_VALUES * 2)) + self.y_data.fill(float('nan')) + self.y_data_offset = PLOT_DISPLAYED_VALUES + + self.stat_name_metadata_map = {} + + self.default_ax, *other_axes = self.axes + self.pattern_subplots_map = { + pattern: ax for pattern, ax in zip(self.subplot_patterns, other_axes) + } + + def _add_new_stat(self, stat_name): + next_index, _ = self.y_data.shape + for pattern, ax in self.pattern_subplots_map.items(): + if pattern.search(stat_name): + break + else: + ax = self.default_ax + new_line, *_ = ax.plot([], [], label=stat_name, lw=1) + + self.stat_name_metadata_map[stat_name] = (next_index, new_line) + + new_y_row = np.ndarray((1, PLOT_DISPLAYED_VALUES * 2)) + new_y_row.fill(float('nan')) + self.y_data = np.vstack((self.y_data, new_y_row)) + + def _add_values(self, stats): + for stat_name, value in stats: + if stat_name not in self.stat_name_metadata_map: + self._add_new_stat(stat_name) + y_index, _ = self.stat_name_metadata_map[stat_name] + self.y_data[y_index, self.y_data_offset] = value + + self.y_data_offset += 1 + if self.y_data_offset == PLOT_DISPLAYED_VALUES * 2: + self.y_data_offset = PLOT_DISPLAYED_VALUES + self.y_data = np.roll(self.y_data, -PLOT_DISPLAYED_VALUES) + self.needs_redraw = True + + def _plot_frame(self, frame_num): + try: + while frame_stats := self.data_queue.get_nowait(): + self._add_values(frame_stats) + except queue.Empty: + pass + + # TODO detect changes? + changed_lines = [] + if self.needs_redraw: + for y_index, line in self.stat_name_metadata_map.values(): + line.set_data( + self.x_data, + self.y_data[y_index, + self.y_data_offset - PLOT_DISPLAYED_VALUES + :self.y_data_offset] + ) + changed_lines.append(line) + + if frame_num % PLOT_FULL_REDRAW_INTERVAL == 0: + for ax in self.axes: + if ax.has_data(): + ax.relim(visible_only=True) + ax.autoscale_view() + ax.legend(loc='upper left') + self.figure.canvas.draw() + return changed_lines + + def start_plotting(self): + self.animation = FuncAnimation(self.figure, self._plot_frame, + interval=PLOT_ANIMATION_INTERVAL, blit=True) + plt.show() + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('-H', '--host', type=str, + help="Bind to specific address (default: {})" + .format(DEFAULT_SERVER_HOST), + default=DEFAULT_SERVER_HOST) + parser.add_argument('-P', '--port', type=int, + help="Port to listen on (default: {})" + .format(DEFAULT_SERVER_PORT), + default=DEFAULT_SERVER_PORT) + + args = parser.parse_args() + + graphing_server = GraphingServer( + server_host=args.host, server_port=args.port, + ) + print("Graph server is listening for UDP on: {}:{}" + .format(args.host, args.port)) + print("Run your application with env: KAACORE_STATS_EXPORT_UDP={}:{} ..." + .format(args.host, args.port)) + graphing_server.graph.start_plotting() From 33874e8f8a08be86e793c10706b06674a61ef330 Mon Sep 17 00:00:00 2001 From: maniek2332 Date: Tue, 16 Feb 2021 18:42:50 +0100 Subject: [PATCH 2/4] added more options to stats_graph --- src/kaa/tools/stats_graph.py | 54 ++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/src/kaa/tools/stats_graph.py b/src/kaa/tools/stats_graph.py index d94b164c..ae5623c3 100644 --- a/src/kaa/tools/stats_graph.py +++ b/src/kaa/tools/stats_graph.py @@ -1,4 +1,5 @@ import re +import dataclasses import typing import queue import threading @@ -73,14 +74,22 @@ def handle(self): self.server.data_queue.put_nowait(parsed_stats) +@dataclasses.dataclass +class GraphSettings: + subplot_patterns: typing.List[re.Pattern] = \ + dataclasses.field(default_factory=lambda: SUBPLOT_PATTERNS) + selected_stats: typing.List[re.Pattern] = \ + dataclasses.field(default_factory=list) + + class GraphingServer: def __init__(self, server_host: str = DEFAULT_SERVER_HOST, server_port: int = DEFAULT_SERVER_PORT, - subplot_patterns=SUBPLOT_PATTERNS): + graph_settings: typing.Optional[GraphSettings] = None): self.data_queue = queue.Queue() self.graph = Graph( data_queue=self.data_queue, - subplot_patterns=subplot_patterns, + settings=graph_settings or GraphSettings(), ) self.udp_server = GraphingUDPServer( @@ -97,12 +106,15 @@ def __init__(self, server_host: str = DEFAULT_SERVER_HOST, class Graph: def __init__(self, data_queue: queue.Queue, - subplot_patterns): + settings: GraphSettings): self.data_queue = data_queue - self.subplot_patterns = subplot_patterns + self.settings = settings - needed_subplots_count = len(self.subplot_patterns) + 1 + needed_subplots_count = len(self.settings.subplot_patterns) + 1 self.figure, self.axes = plt.subplots(needed_subplots_count, 1) + # for single plot returned axes will not be an iterable + if not isinstance(self.axes, np.ndarray): + self.axes = np.ndarray([self.axes]) plt.subplots_adjust(left=0.1, right=0.99, top=0.95, bottom=0.05) for ax in self.axes: ax.set_xlim(-PLOT_DISPLAYED_VALUES, 0) @@ -119,13 +131,22 @@ def __init__(self, data_queue: queue.Queue, self.y_data_offset = PLOT_DISPLAYED_VALUES self.stat_name_metadata_map = {} + self.ignored_stats = set() self.default_ax, *other_axes = self.axes self.pattern_subplots_map = { - pattern: ax for pattern, ax in zip(self.subplot_patterns, other_axes) + pattern: ax for pattern, ax in zip(self.settings.subplot_patterns, + other_axes) } def _add_new_stat(self, stat_name): + if self.settings.selected_stats: + for selected_stat_pattern in self.settings.selected_stats: + if selected_stat_pattern.match(stat_name): + break + else: + self.ignored_stats.add(stat_name) + return False next_index, _ = self.y_data.shape for pattern, ax in self.pattern_subplots_map.items(): if pattern.search(stat_name): @@ -139,11 +160,15 @@ def _add_new_stat(self, stat_name): new_y_row = np.ndarray((1, PLOT_DISPLAYED_VALUES * 2)) new_y_row.fill(float('nan')) self.y_data = np.vstack((self.y_data, new_y_row)) + return True def _add_values(self, stats): for stat_name, value in stats: if stat_name not in self.stat_name_metadata_map: - self._add_new_stat(stat_name) + if stat_name in self.ignored_stats: + continue + elif not self._add_new_stat(stat_name): + continue y_index, _ = self.stat_name_metadata_map[stat_name] self.y_data[y_index, self.y_data_offset] = value @@ -160,7 +185,6 @@ def _plot_frame(self, frame_num): except queue.Empty: pass - # TODO detect changes? changed_lines = [] if self.needs_redraw: for y_index, line in self.stat_name_metadata_map.values(): @@ -199,11 +223,23 @@ def start_plotting(self): help="Port to listen on (default: {})" .format(DEFAULT_SERVER_PORT), default=DEFAULT_SERVER_PORT) + parser.add_argument('-1', '--single-plot', action='store_true', dest='single_plot', + help="Don't use subplots") + parser.add_argument('-s', '--select', action='append', dest='selected_stats', + type=str, + help="Select only specified stats, supports" + " python regex syntax, can be used multiple times") args = parser.parse_args() + graph_settings = GraphSettings() + if args.single_plot: + graph_settings.subplot_patterns = [] + if args.selected_stats: + graph_settings.selected_stats = [re.compile(s) for s in args.selected_stats] + graphing_server = GraphingServer( - server_host=args.host, server_port=args.port, + server_host=args.host, server_port=args.port, graph_settings=graph_settings ) print("Graph server is listening for UDP on: {}:{}" .format(args.host, args.port)) From c572a83ed042c190471cd14354f6bbc11a9d25ef Mon Sep 17 00:00:00 2001 From: maniek2332 Date: Tue, 16 Feb 2021 20:40:10 +0100 Subject: [PATCH 3/4] exposed statistics functions --- src/kaa/kaacore/statistics.pxd | 14 ++++++-- src/kaa/statistics.pxi | 62 ++++++++++++++++++++++++++++++++++ src/kaa/statistics.py | 2 +- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/kaa/kaacore/statistics.pxd b/src/kaa/kaacore/statistics.pxd index 61cd46bd..12b59454 100644 --- a/src/kaa/kaacore/statistics.pxd +++ b/src/kaa/kaacore/statistics.pxd @@ -1,15 +1,25 @@ +from libc.stdint cimport uint32_t from libcpp.vector cimport vector from libcpp.pair cimport pair from libcpp.string cimport string from .exceptions cimport raise_py_error - ctypedef pair[string, double] CPairStatisticLastValue - +ctypedef pair[string, CStatisticAnalysis] CPairStatisticAnalysis cdef extern from "kaacore/statistics.h" nogil: + cdef cppclass CStatisticAnalysis "kaacore::StatisticAnalysis": + uint32_t samples_count + double last_value + double mean_value + double max_value + double min_value + double standard_deviation + cdef cppclass CStatisticsManager "kaacore::StatisticsManager": vector[CPairStatisticLastValue] get_last_all() except +raise_py_error + vector[CPairStatisticAnalysis] get_analysis_all() except +raise_py_error + void push_value(const string& name, const double value) except +raise_py_error cdef CStatisticsManager& c_get_global_statistics_manager "kaacore::get_global_statistics_manager"() diff --git a/src/kaa/statistics.pxi b/src/kaa/statistics.pxi index 9f7dabf0..f107ebe4 100644 --- a/src/kaa/statistics.pxi +++ b/src/kaa/statistics.pxi @@ -1,11 +1,61 @@ from .kaacore.statistics cimport ( CStatisticsManager, c_get_global_statistics_manager, CPairStatisticLastValue, + CStatisticAnalysis, CPairStatisticAnalysis, ) +cdef class StatisticAnalysis: + cdef CStatisticAnalysis c_statistic_analysis + + def __init__(self): + raise RuntimeError(f'{self.__class__} must not be instantiated manually!') + + @staticmethod + cdef create(CStatisticAnalysis c_statistic_analysis): + cdef StatisticAnalysis statistic_analysis = \ + StatisticAnalysis.__new__(StatisticAnalysis) + statistic_analysis.c_statistic_analysis = c_statistic_analysis + return statistic_analysis + + def __repr__(self): + return (" Date: Wed, 17 Feb 2021 07:39:06 +0100 Subject: [PATCH 4/4] added extras_require to setup.py for automated installation of matplotlib (for stats_graph) --- setup.py | 5 +++++ src/kaa/statistics.pxi | 14 ++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 7aa4f95b..1bd2ad72 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,11 @@ entry_points={ 'console_scripts': ['shaderc=kaa.cli:shaderc'] }, + extras_require={ + 'stats_graph': [ + 'matplotlib>=3.1.1', + ] + }, license="MIT", classifiers=[ "Development Status :: 4 - Beta", diff --git a/src/kaa/statistics.pxi b/src/kaa/statistics.pxi index f107ebe4..646c8f3c 100644 --- a/src/kaa/statistics.pxi +++ b/src/kaa/statistics.pxi @@ -18,12 +18,14 @@ cdef class StatisticAnalysis: return statistic_analysis def __repr__(self): - return ("