From 8e77bc935580e61d3072189ccf3cd8aa3569afd9 Mon Sep 17 00:00:00 2001 From: Konrad Kuchciak Date: Thu, 22 Aug 2019 12:57:20 +0200 Subject: [PATCH] Add stability tests Testing programs were created by Michal Bloch (m.bloch@samsung.com) Change-Id: Ica86065e0c3771a4a856995c4642674e1a07f86e --- packaging/stability-monitor.spec | 25 +++ tests/Makefile | 7 + tests/config.json | 50 ++++++ tests/test-stability-cpu-utils.cpp | 86 ++++++++++ tests/test-stability-cpu-utils.hpp | 27 +++ tests/test-stability-cpu.cpp | 75 ++++++++ tests/test-stability-fd.cpp | 72 ++++++++ tests/test-stability-fg-bg.cpp | 115 +++++++++++++ tests/test-stability-io.cpp | 101 +++++++++++ tests/test-stability-mem.cpp | 86 ++++++++++ tests/test-stability.cpp | 266 +++++++++++++++++++++++++++++ tests/test-stability.hpp | 50 ++++++ 12 files changed, 960 insertions(+) create mode 100644 tests/Makefile create mode 100644 tests/config.json create mode 100644 tests/test-stability-cpu-utils.cpp create mode 100644 tests/test-stability-cpu-utils.hpp create mode 100644 tests/test-stability-cpu.cpp create mode 100644 tests/test-stability-fd.cpp create mode 100644 tests/test-stability-fg-bg.cpp create mode 100644 tests/test-stability-io.cpp create mode 100644 tests/test-stability-mem.cpp create mode 100644 tests/test-stability.cpp create mode 100644 tests/test-stability.hpp diff --git a/packaging/stability-monitor.spec b/packaging/stability-monitor.spec index bc77df6..9c52249 100644 --- a/packaging/stability-monitor.spec +++ b/packaging/stability-monitor.spec @@ -15,6 +15,12 @@ BuildRequires: arm-rpi3-linux-kernel-devel %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 @@ -26,6 +32,9 @@ cd kernel 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 @@ -36,6 +45,14 @@ install -D config/stability-monitor.service %{buildroot}/%{_unitdir}/stability-m 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 @@ -44,3 +61,11 @@ ln -s ../stability-monitor.service %{buildroot}/%{_unitdir}/multi-user.target.wa %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 diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..e29c7a1 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,7 @@ +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` diff --git a/tests/config.json b/tests/config.json new file mode 100644 index 0000000..ff472b4 --- /dev/null +++ b/tests/config.json @@ -0,0 +1,50 @@ +{ + "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 + } +} + diff --git a/tests/test-stability-cpu-utils.cpp b/tests/test-stability-cpu-utils.cpp new file mode 100644 index 0000000..b1c0017 --- /dev/null +++ b/tests/test-stability-cpu-utils.cpp @@ -0,0 +1,86 @@ +#include "test-stability-cpu-utils.hpp" + +// C +#include + +// C++ +#include +#include + +// POSIX +#include + +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 (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 (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 (& number)) >> 1); + number = * reinterpret_cast (& 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 (static_cast (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 (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 (& number)) >> 1); + number = * reinterpret_cast (& 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 (static_cast (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); } + diff --git a/tests/test-stability-cpu-utils.hpp b/tests/test-stability-cpu-utils.hpp new file mode 100644 index 0000000..c508276 --- /dev/null +++ b/tests/test-stability-cpu-utils.hpp @@ -0,0 +1,27 @@ +#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; + } +}; diff --git a/tests/test-stability-cpu.cpp b/tests/test-stability-cpu.cpp new file mode 100644 index 0000000..d0563ee --- /dev/null +++ b/tests/test-stability-cpu.cpp @@ -0,0 +1,75 @@ + +// stability-monitor +#include "test-stability-cpu-utils.hpp" +#include "test-stability.hpp" + +// C +#include + +// C++ +#include +#include + +// POSIX +#include + +cpu_handler handler_avg ("avg"), handler_peak ("peak"); + +int main (int argc, char ** argv) +{ + const std::vector > 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); +} diff --git a/tests/test-stability-fd.cpp b/tests/test-stability-fd.cpp new file mode 100644 index 0000000..47ffa0c --- /dev/null +++ b/tests/test-stability-fd.cpp @@ -0,0 +1,72 @@ +#include "test-stability.hpp" + +// C +#include + +// C++ +#include +#include +#include + +// POSIX +#include + +static void * open_fds (size_t count) +{ + std::vector 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 > 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); +} diff --git a/tests/test-stability-fg-bg.cpp b/tests/test-stability-fg-bg.cpp new file mode 100644 index 0000000..eefa89a --- /dev/null +++ b/tests/test-stability-fg-bg.cpp @@ -0,0 +1,115 @@ +#include "test-stability.hpp" +#include "test-stability-cpu-utils.hpp" + +// C +#include + +// C++ +#include +#include + +// POSIX +#include + +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 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 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 > 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); +} diff --git a/tests/test-stability-io.cpp b/tests/test-stability-io.cpp new file mode 100644 index 0000000..629c91c --- /dev/null +++ b/tests/test-stability-io.cpp @@ -0,0 +1,101 @@ +#include "test-stability.hpp" + +// C +#include + +// C++ +#include +#include +#include + +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 > 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); +} diff --git a/tests/test-stability-mem.cpp b/tests/test-stability-mem.cpp new file mode 100644 index 0000000..f0e413d --- /dev/null +++ b/tests/test-stability-mem.cpp @@ -0,0 +1,86 @@ +#include "test-stability.hpp" + +// C +#include + +// C++ +#include +#include + +// POSIX +#include + +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 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 > 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); +} diff --git a/tests/test-stability.cpp b/tests/test-stability.cpp new file mode 100644 index 0000000..b8abbfe --- /dev/null +++ b/tests/test-stability.cpp @@ -0,0 +1,266 @@ +/* + * 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 +#include +#include + +// C++ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// POSIX +#include +#include +#include + +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 (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 (reinterpret_cast (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 (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 (param))) + throw_errno ("pthread_create"); + return pt; +} + +static std::vector 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 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 & 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 > & 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; +} diff --git a/tests/test-stability.hpp b/tests/test-stability.hpp new file mode 100644 index 0000000..8546de3 --- /dev/null +++ b/tests/test-stability.hpp @@ -0,0 +1,50 @@ +#pragma once + +// C +#include + +// C++ +#include +#include +#include + +// POSIX +#include + +// Gnome +#include +#include + +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 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 > & test_cases, int argc, char ** argv); + -- 2.34.1