%description
This package provides stability monitoring daemon.
+%package tests
+Summary: Stability monitor tests/specification
+
+%description tests
+Tests for stability monitoring tool
+
%prep
%setup -q
%define KMOD_PATH /%{_libdir}/modules/linux/kernel/drivers/misc/stability-monitor/proc-tsm.ko
make clean
make all
+cd ../tests
+make all
+
%install
make install INSTALL_PREFIX=%{buildroot}/%{_sbindir}
install -D config/default.conf %{buildroot}/%{_libdir}/stability-monitor/default.conf
mkdir -p %{buildroot}/%{_unitdir}/multi-user.target.wants
ln -s ../stability-monitor.service %{buildroot}/%{_unitdir}/multi-user.target.wants/stability-monitor.service
+# install -D test-stability-fg-bg %{buildroot}%{_libexecdir}/stability-tests/test-stability-fg-bg
+install -D tests/test-stability-cpu %{buildroot}%{_libexecdir}/stability-tests/test-stability-cpu
+install -D tests/test-stability-mem %{buildroot}%{_libexecdir}/stability-tests/test-stability-mem
+install -D tests/test-stability-io %{buildroot}%{_libexecdir}/stability-tests/test-stability-io
+install -D tests/test-stability-fd %{buildroot}%{_libexecdir}/stability-tests/test-stability-fd
+
+install -D tests/config.json %{buildroot}/etc/stability-monitor.d/test-stability.conf
+
%files
%license COPYING
%{_sbindir}/stability-monitor
%KMOD_PATH
%{_unitdir}/stability-monitor.service
%{_unitdir}/multi-user.target.wants/stability-monitor.service
+
+%files tests
+# %{_libexecdir}/stability-tests/test-stability-fg-bg
+%{_libexecdir}/stability-tests/test-stability-cpu
+%{_libexecdir}/stability-tests/test-stability-mem
+%{_libexecdir}/stability-tests/test-stability-io
+%{_libexecdir}/stability-tests/test-stability-fd
+/etc/stability-monitor.d/test-stability.conf
--- /dev/null
+all:
+ # Temporarily disabled, needs an overhaul
+ # g++ -std=c++14 -o test-stability-fg-bg -pthread `pkg-config --cflags glib-2.0 gio-2.0` test-stability.cpp test-stability-cpu-utils.cpp test-stability-fg-bg.cpp `pkg-config --libs glib-2.0 gio-2.0`
+ g++ -std=c++14 -o test-stability-cpu -pthread `pkg-config --cflags glib-2.0 gio-2.0` test-stability.cpp test-stability-cpu-utils.cpp test-stability-cpu.cpp `pkg-config --libs glib-2.0 gio-2.0`
+ g++ -std=c++14 -o test-stability-mem -pthread `pkg-config --cflags glib-2.0 gio-2.0` test-stability.cpp test-stability-mem.cpp `pkg-config --libs glib-2.0 gio-2.0`
+ g++ -std=c++14 -o test-stability-io -pthread `pkg-config --cflags glib-2.0 gio-2.0` test-stability.cpp test-stability-io.cpp `pkg-config --libs glib-2.0 gio-2.0`
+ g++ -std=c++14 -o test-stability-fd -pthread `pkg-config --cflags glib-2.0 gio-2.0` test-stability.cpp test-stability-fd.cpp `pkg-config --libs glib-2.0 gio-2.0`
--- /dev/null
+{
+ "global" : {
+ "sampling_rate" : 1.0
+ },
+ "test-stability-cpu" : {
+ "monitor" : 1,
+ "print_current": 1,
+ "kill" : 0,
+ "report" : 0,
+ "sampling_rate": 1.0,
+ "apply_to_children" : 0,
+ "cpu_limit_avg" : 0.40,
+ "cpu_limit_peak" : 0.90,
+ "cpu_avg_period" : 3
+ },
+ "test-stability-mem" : {
+ "monitor" : 1,
+ "print_current": 1,
+ "kill" : 0,
+ "report" : 0,
+ "sampling_rate": 1.0,
+ "apply_to_children" : 0,
+ "mem_limit_avg" : 0.30,
+ "mem_limit_peak" : 0.60,
+ "comment_to_above" : "% of reference target (1 GB on rpi3)",
+ "mem_limit_period" : 3
+ },
+ "test-stability-fd" : {
+ "monitor" : 1,
+ "print_current": 1,
+ "kill" : 0,
+ "report" : 0,
+ "sampling_rate": 1.0,
+ "apply_to_children" : 0,
+ "fd_limit" : 200
+ },
+ "test-stability-io" : {
+ "monitor" : 1,
+ "print_current": 1,
+ "kill" : 0,
+ "report" : 0,
+ "sampling_rate": 1.0,
+ "apply_to_children" : 0,
+ "io_limit_avg" : 10.0,
+ "io_limit_peak" : 30.0,
+ "comment_to_above" : "in megabytes (on reference target)",
+ "io_limit_period" : 3
+ }
+}
+
--- /dev/null
+#include "test-stability-cpu-utils.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <sys/sysinfo.h>
+
+size_t get_processor_count ()
+{
+ /* Single-threaded applications may look like they are not using much CPU
+ * power from the system's point of view while actually using an entire core
+ * in a multi-core setup. For example, a program using 25% looks okay at a
+ * glance, but could be a hogging 100% of one of 4 available cores.
+ *
+ * Therefore, we'd like to be able to test both heavy usage of a single core
+ * and of the entire system (that is, all cores). */
+
+ const auto processor_count = get_nprocs ();
+ if (processor_count <= 0)
+ throw std::runtime_error ("Couldn't get processor count");
+ if (processor_count > CPU_SETSIZE)
+ throw std::runtime_error ("Too many processors");
+ return static_cast <size_t> (processor_count);
+}
+
+void * cpu_heavy (void * arg)
+{
+ /* Do some intensive numerical calculations while avoiding system calls and
+ * general I/O to maximize CPU usage. The loop is designed never to end but
+ * be complicated enough that the compiler can't tell and optimize it out.
+ * Note that `cpu_light` looks the same except the sleep on one line, but
+ * putting it in a conditional branch could (would!) ruin CPU usage whether
+ * that branch were taken or not, which is why it's a separate function.
+ * `if constexpr` sounds like it would allow to merge them but is C++17. */
+
+ float number = 123.4 + 567 * reinterpret_cast <intptr_t> (arg);
+ while (number > 0.1337 && !global_stop) {
+ for (size_t i = 0; i < 2; ++i) {
+ float half = number * 0.5F;
+ long tmp = 0x5F3759DF - ((* reinterpret_cast <long *> (& number)) >> 1);
+ number = * reinterpret_cast <float *> (& tmp);
+ number *= 1.5F - (half * number * number);
+ if (number < 0)
+ number = - number;
+ }
+ number *= number;
+ number *= number;
+
+ // no sleep (cf. `cpu_light` below)
+ }
+ return reinterpret_cast <void *> (static_cast <int> (number));
+}
+
+void * cpu_light (void * arg)
+{
+ /* This function is similar to the above, except stability monitor should
+ * consider it kosher because it has an extra sleep (that can't be put in
+ * a conditional, see the comments in `cpu_heavy`) which removes most of
+ * its CPU usage. */
+
+ float number = 123.4 + 567 * reinterpret_cast <intptr_t> (arg);
+ while (number > 0.1337 && !global_stop) {
+ for (size_t i = 0; i < 2; ++i) {
+ float half = number * 0.5F;
+ long tmp = 0x5F3759DF - ((* reinterpret_cast <long *> (& number)) >> 1);
+ number = * reinterpret_cast <float *> (& tmp);
+ number *= 1.5F - (half * number * number);
+ if (number < 0)
+ number = - number;
+ }
+ number *= number;
+ number *= number;
+
+ std::this_thread::sleep_for (std::chrono::milliseconds (1));
+ }
+ return reinterpret_cast <void *> (static_cast <int> (number));
+}
+
+bool run_cpu_test (thread_func_t * func, size_t processors, size_t test_time, dbus_signal_handler * handler)
+{ return run_test (func, processors, handler, test_time); }
+
--- /dev/null
+#pragma once
+
+#include "test-stability.hpp"
+
+void * cpu_light (void * arg);
+void * cpu_heavy (void * arg);
+size_t get_processor_count ();
+bool run_cpu_test (thread_func_t * func, size_t processors, size_t test_time, dbus_signal_handler * handler);
+
+struct cpu_handler : public dbus_signal_handler {
+ const std::string limitType;
+ static constexpr const char * const gtype = "(isdda{sv})";
+
+ cpu_handler (const char * lt) : limitType (lt) { }
+
+ bool match (const gchar * objpath, const gchar * iface, GVariant * parameters) const override {
+ if ("/Org/Tizen/StabilityMonitor/tsm_cpu"s != objpath
+ || "org.tizen.abnormality.cpu.relative"s != iface
+ || !g_variant_is_of_type (parameters, G_VARIANT_TYPE(gtype)))
+ return false;
+
+ const char * paramType = nullptr;
+ int pid = -1;
+ g_variant_get (parameters, gtype, & pid, & paramType, nullptr, nullptr, nullptr);
+ return pid == getpid() && limitType == paramType;
+ }
+};
--- /dev/null
+
+// stability-monitor
+#include "test-stability-cpu-utils.hpp"
+#include "test-stability.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <sys/sysinfo.h>
+
+cpu_handler handler_avg ("avg"), handler_peak ("peak");
+
+int main (int argc, char ** argv)
+{
+ const std::vector <std::pair <bool (*) (), std::string>> test_cases
+ { { []() {
+ /* Do some light calculation for the test period.
+ * Expect no signal during that time.
+ * This test has no multi-core variante since
+ * all cores have little load regardless, and no
+ * peak variante because . */
+ return run_cpu_test (cpu_light, 1, TEST_TIME_PEAK, & handler_peak);
+ } , "no CPU load, expecting no signal"
+ } , { []() {
+ /* Do heavy calculation on 1 core for the test period.
+ * Expect a signal. */
+ return ! run_cpu_test (cpu_heavy, 1, TEST_TIME_PEAK, & handler_peak);
+ } , "heavy 1 CPU load, expecting peak signal"
+ } , { []() {
+ /* Same as above, except on all cores. */
+ return ! run_cpu_test (cpu_heavy, get_processor_count (), TEST_TIME_PEAK, & handler_peak);
+ } , "heavy all CPU load, expecting peak signal"
+ } , { []() {
+ /* Run heavy calculation on 1 core, expect a signal.
+ * Wait a bit to make sure any straggler signals are gone.
+ * Run light calculation on 1 core, expect no signal.
+ * Switch to heavy calculation on 1 core again, expect a signal. */
+ if (run_cpu_test (cpu_heavy, 1, TEST_TIME_PEAK, & handler_peak))
+ return false;
+ std::this_thread::sleep_for (CATCH_STRAGGLER_SIGNALS_TIME);
+ if (! run_cpu_test (cpu_light, 1, TEST_TIME_PEAK, & handler_peak))
+ return false;
+ return ! run_cpu_test (cpu_heavy, 1, TEST_TIME_PEAK, & handler_peak);
+ } , "heavy 1 CPU load, expecting peak signal; then toning down to light load, expecting no signal"
+ } , { []() {
+ /* Same as above, except on all cores. */
+ const size_t procs = get_processor_count ();
+ if (run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak))
+ return false;
+ std::this_thread::sleep_for (CATCH_STRAGGLER_SIGNALS_TIME);
+ if (! run_cpu_test (cpu_light, 1, TEST_TIME_PEAK, & handler_peak))
+ return false;
+ return ! run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak);
+ } , "heavy all CPU load, expecting peak signal; then toning down to light load, expecting no signal"
+ } , { []() {
+ /* Run for a longer time (for avg) at half speed (so as
+ * not to trigger peak detection). */
+ return run_throttled (60
+ , [] () {
+ if (! run_cpu_test (cpu_heavy, 1, TEST_TIME_PEAK, & handler_peak))
+ return false;
+ return ! run_cpu_test (cpu_heavy, 1, TEST_TIME_AVG - TEST_TIME_PEAK, & handler_avg);
+ }
+ );
+ } , "running at ~60% CPU (using SIGSTOP/SIGCONT from child), expecting average signal (but not peak)"
+ }
+ };
+
+ return standard_main (test_cases, argc, argv);
+}
--- /dev/null
+#include "test-stability.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <fstream>
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <fcntl.h>
+
+static void * open_fds (size_t count)
+{
+ std::vector <int> fds (count, -1);
+ for (auto & fd : fds)
+ fd = open ("/dev/null", O_RDONLY);
+
+ while (!global_stop)
+ std::this_thread::sleep_for (std::chrono::seconds (1));
+
+ for (const auto fd : fds)
+ if (fd != -1)
+ close (fd);
+
+ return nullptr;
+}
+
+static void * fd_heavy (void * arg)
+{ return open_fds (729); }
+static void * fd_light (void * arg)
+{ return open_fds (3); }
+
+struct fd_handler : public dbus_signal_handler {
+ const std::string limitType;
+ static constexpr const char * const gtype = "(isiia{sv})";
+
+ fd_handler (const char * lt) : limitType (lt) { }
+
+ bool match (const gchar * objpath, const gchar * iface, GVariant * parameters) const override {
+ if ("/Org/Tizen/StabilityMonitor/tsm_fd"s != objpath
+ || "org.tizen.abnormality.fd.absolute"s != iface
+ || !g_variant_is_of_type (parameters, G_VARIANT_TYPE(gtype)))
+ return false;
+
+ const char * paramType = nullptr;
+ int pid = -1;
+ g_variant_get (parameters, gtype, & pid, & paramType, nullptr, nullptr, nullptr);
+ return pid == getpid() && limitType == paramType;
+ }
+} handler_avg ("avg"), handler_peak ("peak");
+
+bool run_fd_test (thread_func_t * func, size_t test_time, dbus_signal_handler * handler)
+{ return run_test (func, 1, handler, test_time); }
+
+int main (int argc, char ** argv)
+{
+ const std::vector <std::pair <bool (*) (), std::string>> test_cases
+ { { []() {
+ /* Open a handful of FDs. Expect no signal. */
+ return run_fd_test (fd_light, TEST_TIME_PEAK, & handler_peak);
+ } , "3 FDs (+ defaults), expecting no signal"
+ } , { []() {
+ /* Open a whole lot of FDs. Expect a signal. */
+ return ! run_fd_test (fd_heavy, TEST_TIME_PEAK, & handler_peak);
+ } , "729 FDs (+ defaults), expecting a signal"
+ }
+ };
+
+ return standard_main (test_cases, argc, argv);
+}
--- /dev/null
+#include "test-stability.hpp"
+#include "test-stability-cpu-utils.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <sys/sysinfo.h>
+
+static void unref_gvariant (GVariant * gv)
+{
+ if (!gv)
+ return;
+ g_variant_unref (gv);
+}
+static void unref_obj (gpointer ptr)
+{
+ if (!ptr)
+ return;
+ g_object_unref (ptr);
+}
+
+static void change_self_status (const char * status)
+{
+ managed_GError err;
+
+ std::unique_ptr <GVariant, void (*) (GVariant *)> params
+ ( g_variant_new ( "(i&s&s&s&s)"
+ , (int) getpid ()
+ , "stability-monitor-appID"
+ , "stability-monitor-pkgID"
+ , "widget" // svc, widget, watch, gui
+ , status // fg, bg
+ ) ?: throw std::runtime_error ("g_variant_new failed")
+ , unref_gvariant
+ );
+
+ std::unique_ptr <GDBusConnection, void (*) (gpointer)> connection
+ ( g_bus_get_sync (G_BUS_TYPE_SYSTEM, nullptr, & err.err)
+ ?: throw std::runtime_error (err.err->message)
+ , unref_obj
+ );
+
+ g_dbus_connection_emit_signal
+ ( connection.get ()
+ , nullptr
+ , "/Org/Tizen/Aul/AppStatus"
+ , "org.tizen.aul.AppStatus"
+ , "AppStatusChange"
+ , params.release () /* `g_variant_new` returns a "floating" reference.
+ * This means that unless it is "sunk" explicitly
+ * using `g_variant_ref_sink`, various GIO functions
+ * which accept a GVariant, such as this one, take
+ * over its ownership. This is why the pointer is
+ * release'd instead of just get'd. */
+ , & err.err
+ ) ?: throw std::runtime_error (err.err->message);
+
+ /* Unlike most GIO functions, `g_dbus_connection_emit_signal` doesn't seem
+ * to have a synchronous version so the signal has to be flushed manually. */
+ g_dbus_connection_flush_sync
+ ( connection.get ()
+ , nullptr
+ , & err.err
+ ) ?: throw std::runtime_error (err.err->message);
+}
+
+static void switch_to_foreground ()
+{ change_self_status("fg"); }
+
+static void switch_to_background ()
+{ change_self_status("bg"); }
+
+cpu_handler handler_avg ("avg"), handler_peak ("peak");
+
+int main (int argc, char ** argv)
+{
+ std::cout << "test removed for pressing ceremonial reasons" << std::endl;
+ return EXIT_SUCCESS;
+
+ const std::vector <std::pair <bool (*) (), std::string>> test_cases
+ { { []() {
+ /* Switch to foreground and do heavy processing. Expect no
+ * signal. Then switch to background and restart processing,
+ * expecting a signal this time. */
+ const size_t procs = get_processor_count ();
+
+ switch_to_foreground ();
+ if (! run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak))
+ return false;
+ switch_to_background ();
+ return ! run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak);
+ } , "~100% FG CPU load (no signal expected), then switch to BG while keeping load (expecting a signal)"
+ } , { []() {
+ /* Similar to the above, but the other way around: start in
+ * the background and see if switching to the foreground
+ * stops the signals. */
+ const size_t procs = get_processor_count ();
+
+ switch_to_background ();
+ if (run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak))
+ return false;
+ switch_to_foreground ();
+ std::this_thread::sleep_for (CATCH_STRAGGLER_SIGNALS_TIME);
+ return run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak);
+ } , "~100% BG CPU load (signal expected), then switch to FG while keeping load (expecting signals to stop)"
+ }
+ };
+
+ return standard_main (test_cases, argc, argv);
+}
--- /dev/null
+#include "test-stability.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <fstream>
+#include <thread>
+#include <vector>
+
+static void * do_io (size_t delay)
+{
+ /* Write to an actual file on the disk (i.e. not block devices
+ * nor tmpfs files). The read can be from anywhere since only
+ * the sum of both matters and it's good not to have to keep
+ * extra files on the disk to read from. */
+
+ static constexpr const char * IN_FILE_PATH = "/dev/zero";
+ static constexpr const char * OUT_FILE_PATH = "/opt/usr/stability-test.out";
+
+ std::ifstream in ( IN_FILE_PATH, std::ifstream::binary);
+ std::ofstream out (OUT_FILE_PATH, std::ofstream::binary);
+ if (! in.good ()
+ || ! out.good ())
+ throw std::runtime_error (std::string("Couldn't open ") + IN_FILE_PATH + " and " + OUT_FILE_PATH);
+
+ /* Buffering is undesirable here; it affects the underlying I/O
+ * in an unpredictable way, ruining measurements. */
+ in.rdbuf ()-> pubsetbuf (nullptr, 0);
+ out.rdbuf ()-> pubsetbuf (nullptr, 0);
+
+ while (!global_stop) {
+ for (size_t i = 0; i < 64; ++i) {
+ char buffer [16384];
+ in.read (buffer, sizeof buffer);
+ out.write (buffer, sizeof buffer);
+ }
+
+ std::this_thread::sleep_for (std::chrono::milliseconds (delay));
+ }
+
+ in.close ();
+ out.close ();
+
+ std::remove (OUT_FILE_PATH);
+
+ return nullptr;
+}
+
+static void * io_heavy (void * arg)
+{ do_io (10); }
+static void * io_light (void * arg)
+{ do_io (1000); }
+static void * io_medium (void * arg)
+{ do_io (40); }
+
+struct io_handler : public dbus_signal_handler {
+ const std::string limitType;
+ static constexpr const char * const gtype = "(isdda{sv})";
+
+ io_handler (const char * lt) : limitType (lt) { }
+
+ bool match (const gchar * objpath, const gchar * iface, GVariant * parameters) const override {
+ if ("/Org/Tizen/StabilityMonitor/tsm_io"s != objpath
+ || "org.tizen.abnormality.io.absolute"s != iface
+ || !g_variant_is_of_type (parameters, G_VARIANT_TYPE(gtype)))
+ return false;
+
+ const char * paramType = nullptr;
+ int pid = -1;
+ g_variant_get (parameters, gtype, & pid, & paramType, nullptr, nullptr, nullptr);
+ return pid == getpid() && limitType == paramType;
+ }
+} handler_avg ("avg"), handler_peak ("peak");
+
+bool run_io_test (thread_func_t * func, size_t test_time, dbus_signal_handler * handler)
+{ return run_test (func, 1, handler, test_time); }
+
+int main (int argc, char ** argv)
+{
+ const std::vector <std::pair <bool (*) (), std::string>> test_cases
+ { { []() {
+ /* Light, periodic I/O. Expect no signal. */
+ return run_io_test (io_light, TEST_TIME_PEAK, & handler_peak);
+ } , "light I/O, expecting no signal"
+ } , { []() {
+ /* Constant, heavy I/O. Expect a signal. */
+ return ! run_io_test (io_heavy, TEST_TIME_PEAK, & handler_peak);
+ } , "heavy I/O, expecting peak signal"
+ } , { []() {
+ /* Heavy (but limited) I/O distributed over a longer time.
+ * Expect a signal (but not too early, would mean peak). */
+ if (! run_io_test (io_medium, TEST_TIME_PEAK, & handler_peak))
+ return false;
+ return ! run_io_test (io_medium, TEST_TIME_AVG - TEST_TIME_PEAK, & handler_avg);
+ } , "medium I/O over time, expecting average signal but not peak"
+ }
+ };
+
+ return standard_main (test_cases, argc, argv);
+}
--- /dev/null
+#include "test-stability.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <sys/sysinfo.h>
+
+static void * do_mem (size_t to_alloc)
+{
+ /* Allocate memory and pretend we're using it (so as to make it actually
+ * count to RSS use). The allocated amount stays constant. The memory is
+ * volatile to prevent smart compilers from optimizing stuff out. */
+
+ std::unique_ptr <volatile char []> ptr (new volatile char [to_alloc]);
+ for (size_t i = 0; i < to_alloc; i += 4096) // FIXME: 4096 -> page size
+ ptr[i] = 'a' + (i % ('z' - 'a'));
+
+ while (!global_stop)
+ std::this_thread::sleep_for (std::chrono::seconds (1));
+
+ return nullptr;
+}
+
+unsigned long long operator "" _MB (unsigned long long n)
+{ return 1024 * 1024 * n; }
+
+static void * mem_heavy_peak (void * arg)
+{ return do_mem (750_MB); }
+
+static void * mem_heavy_avg (void * arg)
+{ return do_mem (400_MB); }
+
+static void * mem_light (void * arg)
+{ return do_mem (25_MB); }
+
+struct mem_handler : public dbus_signal_handler {
+ const std::string limitType;
+ static constexpr const char * const gtype = "(isdda{sv})";
+
+ mem_handler (const char * lt) : limitType (lt) { }
+
+ bool match (const gchar * objpath, const gchar * iface, GVariant * parameters) const override {
+ if ("/Org/Tizen/StabilityMonitor/tsm_mem"s != objpath
+ || "org.tizen.abnormality.mem.relative"s != iface
+ || !g_variant_is_of_type (parameters, G_VARIANT_TYPE(gtype)))
+ return false;
+
+ const char * paramType = nullptr;
+ int pid = -1;
+ g_variant_get (parameters, gtype, & pid, & paramType, nullptr, nullptr, nullptr);
+ return pid == getpid() && limitType == paramType;
+ }
+} handler_avg ("avg"), handler_peak ("peak");
+
+bool run_mem_test (thread_func_t * func, size_t test_time, dbus_signal_handler * handler)
+{ return run_test (func, 1, handler, test_time); }
+
+int main (int argc, char ** argv)
+{
+ const std::vector <std::pair <bool (*) (), std::string>> test_cases
+ { { []() {
+ /* Do a reasonable amount of memory (re)allocation.
+ * Expect no signal. */
+ return run_mem_test (mem_light, TEST_TIME_PEAK, & handler_peak);
+ } , "small allocation (25 MB + overhead), expecting no signal"
+ } , { []() {
+ /* Allocate and keep lots of memory.
+ * Expect a signal. */
+ return ! run_mem_test (mem_heavy_peak, TEST_TIME_PEAK, & handler_peak);
+ } , "heavy allocation (750 MB + overhead), expecting peak signal"
+ } , { []() {
+ /* Allocate an amount of memory not high enough
+ * to trigger peak, but keep it long enough to
+ * trigger average. */
+ return ! run_mem_test (mem_heavy_avg, TEST_TIME_AVG, & handler_avg);
+ } , "medium allocation (400 MB + overhead), expecting avg signal"
+ }
+ };
+
+ return standard_main (test_cases, argc, argv);
+}
--- /dev/null
+/*
+ * This file is a part of stability-monitor.
+ *
+ * Copyright © 2019 Samsung Electronics
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+#include "test-stability.hpp"
+
+// C
+#include <cassert>
+#include <cstdio>
+#include <cstdlib>
+
+// C++
+#include <chrono>
+#include <exception>
+#include <iostream>
+#include <fstream>
+#include <map>
+#include <memory>
+#include <numeric>
+#include <system_error>
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <fcntl.h>
+#include <pthread.h>
+#include <unistd.h>
+
+static inline void throw_errno (const std::string & what)
+{ throw std::system_error (errno, std::generic_category (), what); }
+
+/* Globals because passing them around would be a massive hassle. */
+bool timed_out = false;
+bool global_stop = false;
+GMainLoop * loop = nullptr;
+
+/* Can't use a typedef for function declarations, so a macro it is. */
+#define SIGNAL_ARGS \
+ GDBusConnection * connection \
+ , const gchar * sender_name \
+ , const gchar * object_path \
+ , const gchar * interface_name \
+ , const gchar * signal_name \
+ , GVariant * parameters \
+ , gpointer user_data
+
+typedef void signal_cb_t (SIGNAL_ARGS);
+
+static void signal_cb (SIGNAL_ARGS)
+{
+ assert (user_data);
+ if (signal_name != SIGNAL_NAME)
+ return; // would ideally be assert but I don't trust dbus enough
+
+ const auto handler = reinterpret_cast <const dbus_signal_handler *> (user_data);
+ if (handler->match (object_path, interface_name, parameters))
+ g_main_loop_quit (loop);
+}
+
+struct dbus_signal_listener {
+ managed_GError err;
+ GDBusConnection * connection;
+ guint signal_subscription;
+
+ dbus_signal_listener (const dbus_signal_handler * handler)
+ : err ()
+ , connection (g_bus_get_sync (G_BUS_TYPE_SYSTEM, nullptr, & err.err)
+ ?: throw std::runtime_error (err.err->message)
+ )
+ , signal_subscription (g_dbus_connection_signal_subscribe
+ ( connection
+ , nullptr // sender
+ , nullptr // interface
+ , SIGNAL_NAME.c_str ()
+ , nullptr // object_path
+ , nullptr // arg0
+ , G_DBUS_SIGNAL_FLAGS_NONE
+ , signal_cb
+ , const_cast <void *> (reinterpret_cast <const void *> (handler))
+ , nullptr
+ ) // apparently (according to the docs), signal_subscribe can't fail
+ ) { }
+
+ ~ dbus_signal_listener ()
+ {
+ g_dbus_connection_signal_unsubscribe (connection, signal_subscription);
+ g_object_unref (connection);
+ }
+};
+
+void * timed_quit (void * userdata)
+{
+ /* Split the wait into smaller periods so we can notice along
+ * the way that we've received a signal and interrupt the wait. */
+
+ const auto timeout = reinterpret_cast <intptr_t> (userdata);
+ for (auto i = 0; i < timeout; ++i) {
+ std::this_thread::sleep_for (std::chrono::seconds (1));
+ if (global_stop)
+ return nullptr;
+ }
+
+ timed_out = true;
+ g_main_loop_quit (loop);
+ return nullptr;
+}
+
+static void set_affinity (pthread_t pt, size_t affinity)
+{
+ /* We're setting specific affinities to make sure that in the scenario
+ * where we want to use the entire system's CPU resources, we actually
+ * have them assigned to us. The OS is not guaranteed to do this on its
+ * own (even if we spawn as many threads as the system has cores) and
+ * needs to be asked. Linux seems to give a guarantee that this wish
+ * is respected. */
+
+ cpu_set_t cpu_set;
+ CPU_ZERO (& cpu_set);
+ CPU_SET (affinity, & cpu_set);
+ pthread_setaffinity_np (pt, sizeof cpu_set, & cpu_set);
+}
+
+static inline pthread_t spawn_thread (thread_func_t * func, int param)
+{
+ pthread_t pt;
+ if (pthread_create(& pt, nullptr, func, reinterpret_cast <void *> (param)))
+ throw_errno ("pthread_create");
+ return pt;
+}
+
+static std::vector <pthread_t> spawn_threads (size_t processor_count, thread_func_t * func, size_t test_time)
+{
+ /* We're using pthread_t directly instead of std::thread (which on systems
+ * compliant with POSIX is also usually just a wrapper around pthread_t) as
+ * we want to control thread CPU affinity - a platform-specific feature that
+ * std::thread doesn't expose. Doesn't stop us from using std::this_thread
+ * later where convenient though. */
+
+ std::vector <pthread_t> threads;
+ threads.reserve (processor_count + 1);
+ for (size_t i = 0; i < processor_count; ++i) {
+ threads.push_back (spawn_thread (func, i));
+ set_affinity (threads.back (), i);
+ }
+
+ /* Give stability monitor a limited time to notice our resource usage.
+ * It should detect anomalies without excessive delay. */
+ threads.push_back (spawn_thread (timed_quit, test_time));
+
+ return threads;
+}
+
+bool run_test (thread_func_t * func, size_t processors, const dbus_signal_handler * handler, size_t test_time)
+{
+ global_stop = false;
+ timed_out = false;
+
+ auto threads = spawn_threads (processors, func, test_time);
+ dbus_signal_listener dsl (handler);
+
+ loop = g_main_loop_new (nullptr, false);
+ if (!loop)
+ throw std::runtime_error ("g_main_loop_new failed");
+ g_main_loop_run (loop);
+ global_stop = true;
+
+ for (auto thread : threads)
+ pthread_join (thread, nullptr);
+ loop = nullptr;
+
+ return timed_out;
+}
+
+// FIXME: throttling doesn't end so only works correctly on the last test in a batch
+void throttle (size_t percent, pid_t pid)
+{
+ int current_sig = SIGSTOP;
+ while (true) {
+ std::this_thread::sleep_for (std::chrono::microseconds (100 * percent));
+ if (kill (pid, current_sig) == -1)
+ break;
+ percent = 100 - percent;
+ current_sig = (SIGSTOP + SIGCONT) - current_sig;
+ }
+}
+
+bool run_throttled (size_t percent, bool (*func) ())
+{
+ /* The child throttles the parent, not the other way around.
+ * This is because the parent is the one to return a relevant
+ * value to the caller. */
+ pid_t parent_pid = getpid ();
+ int child_pid = fork ();
+ if (child_pid == -1)
+ throw_errno ("fork failed");
+
+ if (!child_pid) {
+ throttle (percent, parent_pid);
+ exit (EXIT_SUCCESS);
+ } else
+ return func ();
+}
+
+static bool run_single_testcase (size_t index, const std::pair <bool (*)(), std::string> & test_case)
+{
+ std::cout << '#' << index << ": " << test_case.second << "... " << std::flush;
+ std::this_thread::sleep_for (std::chrono::seconds (2)); // let resource usage from initialisation or previous attempts dissipate
+
+ bool result;
+ try {
+ result = test_case.first ();
+ } catch (const std::exception & ex) {
+ std::cerr << ex.what () << std::endl;
+ result = false;
+ }
+ std::cout << (result ? "PASS" : "FAIL") << std::endl;
+ return result;
+}
+
+static int print_usage (const char * progname, size_t max_case)
+{
+ std::cout << "Usage: " << progname << " [1-" << max_case << ']' << std::endl;
+ return EXIT_FAILURE;
+}
+
+int standard_main (const std::vector <std::pair <bool (*)(), std::string>> & test_cases, int argc, char ** argv)
+{
+ auto print_usage = [&] () { std::cout << "Usage: " << argv[0] << " [1-" << test_cases.size () << ']' << std::endl; return EXIT_FAILURE; };
+
+ if (argc > 2)
+ return print_usage ();
+
+ if (argc == 2) {
+ size_t i;
+ try {
+ i = std::stoi (argv[1]) - 1;
+ } catch (std::invalid_argument & ex) {
+ return print_usage ();
+ }
+ if (i >= test_cases.size ())
+ return print_usage ();
+ return run_single_testcase (i + 1, test_cases.at(i)) ? EXIT_SUCCESS : EXIT_FAILURE;
+ }
+
+ size_t i = 0, pass = 0, fail = 0;
+ for (const auto & test_case : test_cases)
+ ++ (run_single_testcase (++i, test_case) ? pass : fail);
+ std::cout << std::endl
+ << pass << '/' << i << " tests passed" << std::endl;
+
+ return (pass == i) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
--- /dev/null
+#pragma once
+
+// C
+#include <cstdlib>
+
+// C++
+#include <chrono>
+#include <string>
+#include <vector>
+
+// POSIX
+#include <unistd.h>
+
+// Gnome
+#include <glib.h>
+#include <gio/gio.h>
+
+using namespace std::string_literals;
+
+static constexpr size_t TEST_TIME_PEAK = 5; // not actually a duration, but the number of 1s periods
+static constexpr size_t TEST_TIME_AVG = 10; // a fair bit longer than PEAK
+static constexpr auto CATCH_STRAGGLER_SIGNALS_TIME = std::chrono::seconds (3);
+
+static const std::string SIGNAL_NAME = "AbnormalityDetected";
+
+struct managed_GError {
+ /* Functions typically work with (GError **) so we can't
+ * use std::unique_ptr or the other <memory> classes for
+ * automated memory management (we only really care about
+ * the destructor though). */
+ GError * err { nullptr };
+ ~ managed_GError () { if (err) g_error_free (err); }
+};
+
+struct dbus_signal_handler {
+ virtual compl dbus_signal_handler ( ) { }
+ virtual bool match (const gchar * objpath, const gchar * iface, GVariant * parameters) const = 0;
+};
+
+extern bool timed_out;
+extern bool global_stop;
+extern GMainLoop * loop;
+
+typedef void * thread_func_t (void *);
+
+bool run_throttled (size_t percent, bool (*func) ());
+bool run_test (thread_func_t * func, size_t processors, const dbus_signal_handler * handler, size_t test_time);
+
+int standard_main (const std::vector <std::pair <bool (*) (), std::string>> & test_cases, int argc, char ** argv);
+