libmempressure 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kutu OS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include LICENSE
2
+ include README.md
3
+ include src/mempressure.c
4
+ include include/mempressure.h
5
+ include bindings/python/mempressure_module.c
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: libmempressure
3
+ Version: 0.1.0
4
+ Summary: Memory-pressure events from kernel PSI: an onTrimMemory for Linux (Python binding)
5
+ Author: kutu OS contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://kutu.so
8
+ Project-URL: Repository, https://github.com/kutuso/libmempressure
9
+ Project-URL: Documentation, https://kutu.so/#/LIBMEMPRESSURE.md
10
+ Keywords: memory,psi,pressure,linux,oom,swap,performance
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: C
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: System :: Operating System Kernels :: Linux
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # libmempressure — a Linux answer to Android's onTrimMemory
25
+
26
+ Apps on Android never get OOM-killed without warning: the OS sends them
27
+ `onTrimMemory()` callbacks and they shed caches gracefully. Desktop Linux
28
+ just kills them. `libmempressure` closes that gap: it watches the kernel's
29
+ [Pressure Stall Information](https://docs.kernel.org/admin-guide/perf/psi.html)
30
+ and delivers memory-pressure events to your application — in **C, C++, Java
31
+ (JVM) and Python** — so instead of dying under pressure, your app trims.
32
+
33
+ ```
34
+ level some avg10 what your app should do
35
+ ----------- ------------ ------------------------------------
36
+ none < 5% business as usual
37
+ low >= 5% stop prefetching, trim opportunistically
38
+ moderate >= 15% drop object caches, shrink pools
39
+ critical >= 40% shed everything, survive
40
+ ```
41
+
42
+ Events are level *changes* only, filtered by hysteresis (consecutive
43
+ readings) so your callback doesn't flap. The monitor reads
44
+ `/proc/pressure/memory` on its own thread; callbacks fire from that thread.
45
+
46
+ ## The C API
47
+
48
+ ```c
49
+ #include <mempressure.h>
50
+
51
+ static void on_pressure(mp_level_t level, void *userdata) {
52
+ if (level >= MP_LEVEL_MODERATE) cache_drop_all();
53
+ }
54
+
55
+ mp_init(NULL); /* defaults; zero fields fall back too */
56
+ mp_subscribe(on_pressure, my_app);
57
+ /* ... */
58
+ mp_shutdown();
59
+ ```
60
+
61
+ `mp_config_t` tunes thresholds (5/15/40%), poll interval (0.5s) and
62
+ hysteresis (2 readings); any zero field falls back to its default. See
63
+ [`include/mempressure.h`](include/mempressure.h) for the full contract —
64
+ threading rules, error codes, and the `psi_path` override (used by the test
65
+ suites to drive the monitor from fixture files).
66
+
67
+ ## C++
68
+
69
+ Header-only RAII wrapper — [`bindings/c++/mempressure.hpp`](bindings/c++/mempressure.hpp):
70
+
71
+ ```cpp
72
+ mp::Monitor monitor; /* starts the monitor */
73
+ monitor.subscribe([](mp::Level level) {
74
+ if (level >= mp::Level::Moderate) cache_drop_all();
75
+ }); /* std::function, refcounted lifetime */
76
+ ```
77
+
78
+ ## JVM (JNI)
79
+
80
+ ```java
81
+ import io.kutu.mempressure.MemPressure;
82
+
83
+ MemPressure.start(new MemPressure.Config());
84
+ int handle = MemPressure.subscribe(level -> {
85
+ if (level >= 2) cache.dropAll(); // 0=none 1=low 2=moderate 3=critical
86
+ });
87
+ MemPressure.unsubscribe(handle);
88
+ MemPressure.stop();
89
+ ```
90
+
91
+ Native side: `bindings/jvm/jni/mempressure_jni.c` (`libmempressure_jni.so`),
92
+ built automatically when a JDK is present. Callbacks arrive on a daemon
93
+ thread attached to the JVM.
94
+
95
+ ## Python
96
+
97
+ ```python
98
+ import mempressure as mp
99
+
100
+ mp.start() # defaults
101
+ mp.subscribe(lambda level: level >= 2 and cache.drop_all())
102
+ print(mp.psi()) # {'some_avg10': 0.12, ...}
103
+ mp.stop()
104
+ ```
105
+
106
+ CPython extension (`mempressure` module) built against your interpreter;
107
+ callbacks fire with the GIL acquired from the monitor thread.
108
+
109
+ ## Build and test
110
+
111
+ ```sh
112
+ make test # cmake build + ctest (C core, C++ binding, JVM binding)
113
+ # + python binding tests (needs a venv with pytest, see below)
114
+ ```
115
+
116
+ Details:
117
+
118
+ ```sh
119
+ cmake -S . -B build # auto-detects Python dev + JDK; skips gracefully
120
+ cmake --build build
121
+ ctest --test-dir build --output-on-failure
122
+ python3 -m venv .venv && .venv/bin/pip install pytest
123
+ .venv/bin/pytest tests/python -q # (build with -DPython3_EXECUTABLE=.venv/bin/python
124
+ # if your system python differs from the venv's)
125
+ ```
126
+
127
+ The test suites never require real memory pressure: they point the monitor at
128
+ fixture files via `psi_path` and mutate them to drive level changes. The
129
+ example binary (`mp_example_c`) runs against your real
130
+ `/proc/pressure/memory`.
131
+
132
+ ## Distro packaging (RPM / DEB)
133
+
134
+ The library is plain C11 + pthreads and installs via CMake's GNUInstallDirs,
135
+ so it lands correctly on any FHS distro (including `/usr/lib64` RPM
136
+ convention). Upstream ships packaging metadata:
137
+
138
+ - **RPM** (Fedora/RHEL/openSUSE): [`packaging/rpm/libmempressure.spec`](packaging/rpm/libmempressure.spec).
139
+ Build a tarball and rpmbuild it, or point a COPR at the spec:
140
+
141
+ ```sh
142
+ git archive --prefix=libmempressure-0.1.0/ -o libmempressure-0.1.0.tar.gz HEAD
143
+ rpmbuild -bb packaging/rpm/libmempressure.spec --define "_sourcedir $PWD"
144
+ ```
145
+
146
+ - **DEB** (Debian/Ubuntu): a `debian/` directory with `libmempressure0`
147
+ (runtime) and `libmempressure-dev` packages, native-format source:
148
+
149
+ ```sh
150
+ sudo apt install build-essential cmake debhelper pkg-config
151
+ dpkg-buildpackage -us -uc -b
152
+ ```
153
+
154
+ Distro packages ship the C core and the header-only C++ binding
155
+ (`-DMP_PYTHON=OFF -DMP_JAVA=OFF` in the build); the Python and JVM bindings
156
+ are built from the same source wherever those toolchains live. CI exercises
157
+ both paths on every push: `package-rpm` (fedora container) and
158
+ `package-deb` (debian container) build and inspect the actual packages.
159
+
160
+ ## Relationship to kutu OS
161
+
162
+ This is the M3 building block of
163
+ [kutu OS](https://github.com/kutuso/os): the `mempressured` policy daemon and
164
+ eventually applications themselves use these events to shed memory gracefully
165
+ under pressure instead of being killed. The C core has no dependencies beyond
166
+ libc/pthreads, so it ships anywhere — including inside the kutu ISO.
167
+
168
+ MIT licensed — see [LICENSE](LICENSE).
@@ -0,0 +1,145 @@
1
+ # libmempressure — a Linux answer to Android's onTrimMemory
2
+
3
+ Apps on Android never get OOM-killed without warning: the OS sends them
4
+ `onTrimMemory()` callbacks and they shed caches gracefully. Desktop Linux
5
+ just kills them. `libmempressure` closes that gap: it watches the kernel's
6
+ [Pressure Stall Information](https://docs.kernel.org/admin-guide/perf/psi.html)
7
+ and delivers memory-pressure events to your application — in **C, C++, Java
8
+ (JVM) and Python** — so instead of dying under pressure, your app trims.
9
+
10
+ ```
11
+ level some avg10 what your app should do
12
+ ----------- ------------ ------------------------------------
13
+ none < 5% business as usual
14
+ low >= 5% stop prefetching, trim opportunistically
15
+ moderate >= 15% drop object caches, shrink pools
16
+ critical >= 40% shed everything, survive
17
+ ```
18
+
19
+ Events are level *changes* only, filtered by hysteresis (consecutive
20
+ readings) so your callback doesn't flap. The monitor reads
21
+ `/proc/pressure/memory` on its own thread; callbacks fire from that thread.
22
+
23
+ ## The C API
24
+
25
+ ```c
26
+ #include <mempressure.h>
27
+
28
+ static void on_pressure(mp_level_t level, void *userdata) {
29
+ if (level >= MP_LEVEL_MODERATE) cache_drop_all();
30
+ }
31
+
32
+ mp_init(NULL); /* defaults; zero fields fall back too */
33
+ mp_subscribe(on_pressure, my_app);
34
+ /* ... */
35
+ mp_shutdown();
36
+ ```
37
+
38
+ `mp_config_t` tunes thresholds (5/15/40%), poll interval (0.5s) and
39
+ hysteresis (2 readings); any zero field falls back to its default. See
40
+ [`include/mempressure.h`](include/mempressure.h) for the full contract —
41
+ threading rules, error codes, and the `psi_path` override (used by the test
42
+ suites to drive the monitor from fixture files).
43
+
44
+ ## C++
45
+
46
+ Header-only RAII wrapper — [`bindings/c++/mempressure.hpp`](bindings/c++/mempressure.hpp):
47
+
48
+ ```cpp
49
+ mp::Monitor monitor; /* starts the monitor */
50
+ monitor.subscribe([](mp::Level level) {
51
+ if (level >= mp::Level::Moderate) cache_drop_all();
52
+ }); /* std::function, refcounted lifetime */
53
+ ```
54
+
55
+ ## JVM (JNI)
56
+
57
+ ```java
58
+ import io.kutu.mempressure.MemPressure;
59
+
60
+ MemPressure.start(new MemPressure.Config());
61
+ int handle = MemPressure.subscribe(level -> {
62
+ if (level >= 2) cache.dropAll(); // 0=none 1=low 2=moderate 3=critical
63
+ });
64
+ MemPressure.unsubscribe(handle);
65
+ MemPressure.stop();
66
+ ```
67
+
68
+ Native side: `bindings/jvm/jni/mempressure_jni.c` (`libmempressure_jni.so`),
69
+ built automatically when a JDK is present. Callbacks arrive on a daemon
70
+ thread attached to the JVM.
71
+
72
+ ## Python
73
+
74
+ ```python
75
+ import mempressure as mp
76
+
77
+ mp.start() # defaults
78
+ mp.subscribe(lambda level: level >= 2 and cache.drop_all())
79
+ print(mp.psi()) # {'some_avg10': 0.12, ...}
80
+ mp.stop()
81
+ ```
82
+
83
+ CPython extension (`mempressure` module) built against your interpreter;
84
+ callbacks fire with the GIL acquired from the monitor thread.
85
+
86
+ ## Build and test
87
+
88
+ ```sh
89
+ make test # cmake build + ctest (C core, C++ binding, JVM binding)
90
+ # + python binding tests (needs a venv with pytest, see below)
91
+ ```
92
+
93
+ Details:
94
+
95
+ ```sh
96
+ cmake -S . -B build # auto-detects Python dev + JDK; skips gracefully
97
+ cmake --build build
98
+ ctest --test-dir build --output-on-failure
99
+ python3 -m venv .venv && .venv/bin/pip install pytest
100
+ .venv/bin/pytest tests/python -q # (build with -DPython3_EXECUTABLE=.venv/bin/python
101
+ # if your system python differs from the venv's)
102
+ ```
103
+
104
+ The test suites never require real memory pressure: they point the monitor at
105
+ fixture files via `psi_path` and mutate them to drive level changes. The
106
+ example binary (`mp_example_c`) runs against your real
107
+ `/proc/pressure/memory`.
108
+
109
+ ## Distro packaging (RPM / DEB)
110
+
111
+ The library is plain C11 + pthreads and installs via CMake's GNUInstallDirs,
112
+ so it lands correctly on any FHS distro (including `/usr/lib64` RPM
113
+ convention). Upstream ships packaging metadata:
114
+
115
+ - **RPM** (Fedora/RHEL/openSUSE): [`packaging/rpm/libmempressure.spec`](packaging/rpm/libmempressure.spec).
116
+ Build a tarball and rpmbuild it, or point a COPR at the spec:
117
+
118
+ ```sh
119
+ git archive --prefix=libmempressure-0.1.0/ -o libmempressure-0.1.0.tar.gz HEAD
120
+ rpmbuild -bb packaging/rpm/libmempressure.spec --define "_sourcedir $PWD"
121
+ ```
122
+
123
+ - **DEB** (Debian/Ubuntu): a `debian/` directory with `libmempressure0`
124
+ (runtime) and `libmempressure-dev` packages, native-format source:
125
+
126
+ ```sh
127
+ sudo apt install build-essential cmake debhelper pkg-config
128
+ dpkg-buildpackage -us -uc -b
129
+ ```
130
+
131
+ Distro packages ship the C core and the header-only C++ binding
132
+ (`-DMP_PYTHON=OFF -DMP_JAVA=OFF` in the build); the Python and JVM bindings
133
+ are built from the same source wherever those toolchains live. CI exercises
134
+ both paths on every push: `package-rpm` (fedora container) and
135
+ `package-deb` (debian container) build and inspect the actual packages.
136
+
137
+ ## Relationship to kutu OS
138
+
139
+ This is the M3 building block of
140
+ [kutu OS](https://github.com/kutuso/os): the `mempressured` policy daemon and
141
+ eventually applications themselves use these events to shed memory gracefully
142
+ under pressure instead of being killed. The C core has no dependencies beyond
143
+ libc/pthreads, so it ships anywhere — including inside the kutu ISO.
144
+
145
+ MIT licensed — see [LICENSE](LICENSE).
@@ -0,0 +1,193 @@
1
+ #define PY_SSIZE_T_CLEAN
2
+
3
+ #include "mempressure.h"
4
+
5
+ #include <Python.h>
6
+ #include <errno.h>
7
+
8
+ static PyObject *g_handles = NULL;
9
+ static int g_started = 0;
10
+
11
+ static void py_trampoline(mp_level_t level, void *userdata) {
12
+ PyGILState_STATE gstate = PyGILState_Ensure();
13
+ PyObject *cb = (PyObject *)userdata;
14
+ PyObject *arg = PyLong_FromLong((long)level);
15
+ if (arg) {
16
+ PyObject *result = PyObject_CallFunctionObjArgs(cb, arg, NULL);
17
+ if (!result) {
18
+ PyErr_Print();
19
+ }
20
+ Py_XDECREF(result);
21
+ Py_DECREF(arg);
22
+ }
23
+ PyGILState_Release(gstate);
24
+ }
25
+
26
+ static PyObject *py_start(PyObject *self, PyObject *args, PyObject *kwds) {
27
+ (void)self;
28
+ double low = 5.0, moderate = 15.0, critical = 40.0, interval = 0.5;
29
+ int hysteresis = 2;
30
+ const char *psi_path = NULL;
31
+ static char *kwlist[] = {"low", "moderate", "critical", "interval", "hysteresis", "psi_path", NULL};
32
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ddddis", kwlist, &low, &moderate, &critical,
33
+ &interval, &hysteresis, &psi_path)) {
34
+ return NULL;
35
+ }
36
+ mp_config_t cfg = {0};
37
+ cfg.low_threshold = low;
38
+ cfg.moderate_threshold = moderate;
39
+ cfg.critical_threshold = critical;
40
+ cfg.poll_interval_sec = interval;
41
+ cfg.hysteresis = hysteresis;
42
+ cfg.psi_path = psi_path;
43
+ int rc = mp_init(&cfg);
44
+ if (rc == -EALREADY) {
45
+ Py_RETURN_TRUE;
46
+ }
47
+ if (rc != 0) {
48
+ PyErr_Format(PyExc_RuntimeError, "mp_init failed: %d", rc);
49
+ return NULL;
50
+ }
51
+ g_started = 1;
52
+ Py_RETURN_TRUE;
53
+ }
54
+
55
+ static PyObject *py_stop(PyObject *self, PyObject *args) {
56
+ (void)self;
57
+ (void)args;
58
+ mp_shutdown();
59
+ g_started = 0;
60
+ Py_RETURN_NONE;
61
+ }
62
+
63
+ static PyObject *py_current_level(PyObject *self, PyObject *args) {
64
+ (void)self;
65
+ (void)args;
66
+ return PyLong_FromLong((long)mp_current_level());
67
+ }
68
+
69
+ static PyObject *py_level_name(PyObject *self, PyObject *args) {
70
+ (void)self;
71
+ int level = 0;
72
+ if (!PyArg_ParseTuple(args, "i", &level)) {
73
+ return NULL;
74
+ }
75
+ return PyUnicode_FromString(mp_level_name((mp_level_t)level));
76
+ }
77
+
78
+ static PyObject *py_psi(PyObject *self, PyObject *args) {
79
+ (void)self;
80
+ (void)args;
81
+ mp_psi_t psi = {0};
82
+ int rc = mp_psi(&psi);
83
+ if (rc != 0) {
84
+ PyErr_Format(PyExc_RuntimeError, "mp_psi failed: %d (not started?)", rc);
85
+ return NULL;
86
+ }
87
+ return Py_BuildValue("{s:d,s:d,s:d,s:d,s:d,s:d}", "some_avg10", psi.some_avg10,
88
+ "some_avg60", psi.some_avg60, "some_avg300", psi.some_avg300,
89
+ "full_avg10", psi.full_avg10, "full_avg60", psi.full_avg60,
90
+ "full_avg300", psi.full_avg300);
91
+ }
92
+
93
+ static PyObject *py_subscribe(PyObject *self, PyObject *callback) {
94
+ (void)self;
95
+ if (!PyCallable_Check(callback)) {
96
+ PyErr_SetString(PyExc_TypeError, "callback must be callable");
97
+ return NULL;
98
+ }
99
+ if (!g_started) {
100
+ int rc = mp_init(NULL);
101
+ if (rc != 0 && rc != -EALREADY) {
102
+ PyErr_Format(PyExc_RuntimeError, "mp_init failed: %d", rc);
103
+ return NULL;
104
+ }
105
+ g_started = 1;
106
+ }
107
+ Py_INCREF(callback);
108
+ int handle = mp_subscribe(py_trampoline, callback);
109
+ if (handle < 0) {
110
+ Py_DECREF(callback);
111
+ PyErr_Format(PyExc_RuntimeError, "mp_subscribe failed: %d", handle);
112
+ return NULL;
113
+ }
114
+ PyObject *key = PyLong_FromLong(handle);
115
+ PyDict_SetItem(g_handles, key, callback);
116
+ Py_DECREF(key);
117
+ Py_DECREF(callback); // dict owns the strong ref now
118
+ return PyLong_FromLong(handle);
119
+ }
120
+
121
+ static PyObject *py_unsubscribe(PyObject *self, PyObject *args) {
122
+ (void)self;
123
+ int handle = 0;
124
+ if (!PyArg_ParseTuple(args, "i", &handle)) {
125
+ return NULL;
126
+ }
127
+ int rc = mp_unsubscribe(handle);
128
+ if (rc == -ENOENT) {
129
+ PyErr_SetString(PyExc_ValueError, "unknown handle");
130
+ return NULL;
131
+ }
132
+ PyObject *key = PyLong_FromLong(handle);
133
+ PyObject *cb = PyDict_GetItemWithError(g_handles, key);
134
+ if (cb) {
135
+ Py_INCREF(cb);
136
+ PyDict_DelItem(g_handles, key);
137
+ Py_DECREF(cb);
138
+ } else if (PyErr_Occurred()) {
139
+ PyErr_Clear();
140
+ }
141
+ Py_DECREF(key);
142
+ Py_RETURN_NONE;
143
+ }
144
+
145
+ static PyMethodDef methods[] = {
146
+ {"start", (PyCFunction)py_start, METH_VARARGS | METH_KEYWORDS,
147
+ "start(low=5.0, moderate=15.0, critical=40.0, interval=0.5, hysteresis=2, psi_path=None)\n\n"
148
+ "Start the pressure monitor. Re-starting with a live monitor is a no-op."},
149
+ {"stop", py_stop, METH_NOARGS, "Stop the monitor and release everything."},
150
+ {"current_level", py_current_level, METH_NOARGS, "Current pressure level (0..3)."},
151
+ {"level_name", py_level_name, METH_VARARGS, "Name of a level code."},
152
+ {"psi", py_psi, METH_NOARGS, "Last observed PSI values as a dict."},
153
+ {"subscribe", py_subscribe, METH_O, "subscribe(callback) -> handle; fires on level change."},
154
+ {"unsubscribe", py_unsubscribe, METH_VARARGS, "unsubscribe(handle)."},
155
+ {NULL, NULL, 0, NULL},
156
+ };
157
+
158
+ static void free_module_state(void *module) {
159
+ (void)module;
160
+ Py_CLEAR(g_handles);
161
+ }
162
+
163
+ static struct PyModuleDef module_def = {
164
+ PyModuleDef_HEAD_INIT,
165
+ "mempressure",
166
+ "Memory-pressure events from the kernel's PSI, an onTrimMemory for Linux.",
167
+ -1,
168
+ methods,
169
+ NULL,
170
+ NULL,
171
+ NULL,
172
+ free_module_state,
173
+ };
174
+
175
+ PyMODINIT_FUNC PyInit_mempressure(void) {
176
+ g_handles = PyDict_New();
177
+ if (!g_handles) {
178
+ return NULL;
179
+ }
180
+ PyObject *module = PyModule_Create(&module_def);
181
+ if (!module) {
182
+ Py_DECREF(g_handles);
183
+ return NULL;
184
+ }
185
+ PyModule_AddStringConstant(module, "__version__", MP_VERSION);
186
+ PyObject *levels = PyDict_New();
187
+ PyDict_SetItemString(levels, "NONE", PyLong_FromLong(MP_LEVEL_NONE));
188
+ PyDict_SetItemString(levels, "LOW", PyLong_FromLong(MP_LEVEL_LOW));
189
+ PyDict_SetItemString(levels, "MODERATE", PyLong_FromLong(MP_LEVEL_MODERATE));
190
+ PyDict_SetItemString(levels, "CRITICAL", PyLong_FromLong(MP_LEVEL_CRITICAL));
191
+ PyModule_AddObject(module, "LEVELS", levels);
192
+ return module;
193
+ }
@@ -0,0 +1,81 @@
1
+ /*
2
+ * mempressure - a Linux answer to Android's onTrimMemory
3
+ *
4
+ * Subscribe your application to memory-pressure events derived from the
5
+ * kernel's Pressure Stall Information (PSI). Instead of being killed by the
6
+ * OOM killer (or userspace oomd), applications learn that memory is getting
7
+ * tight and shed caches gracefully.
8
+ */
9
+ #ifndef MEMPRESSURE_H
10
+ #define MEMPRESSURE_H
11
+
12
+ #include <stddef.h>
13
+
14
+ #ifdef __cplusplus
15
+ extern "C" {
16
+ #endif
17
+
18
+ #define MP_VERSION "0.1.0"
19
+ #define MP_PSI_PATH_DEFAULT "/proc/pressure/memory"
20
+
21
+ typedef enum {
22
+ MP_LEVEL_NONE = 0, /* no meaningful stall time */
23
+ MP_LEVEL_LOW = 1, /* some reclaim happening; trim opportunistically */
24
+ MP_LEVEL_MODERATE = 2, /* tasks visibly stalled; drop caches */
25
+ MP_LEVEL_CRITICAL = 3 /* heavy stalls; shed everything, survive */
26
+ } mp_level_t;
27
+
28
+ typedef struct {
29
+ double some_avg10;
30
+ double some_avg60;
31
+ double some_avg300;
32
+ double full_avg10;
33
+ double full_avg60;
34
+ double full_avg300;
35
+ } mp_psi_t;
36
+
37
+ typedef void (*mp_callback_t)(mp_level_t level, void *userdata);
38
+
39
+ typedef struct {
40
+ double low_threshold; /* "some" avg10 in percent; default 5.0 */
41
+ double moderate_threshold; /* default 15.0 */
42
+ double critical_threshold; /* default 40.0 */
43
+ double poll_interval_sec; /* monitor tick; default 0.5 */
44
+ int hysteresis; /* consecutive readings before a level change
45
+ fires; default 2 */
46
+ const char *psi_path; /* default /proc/pressure/memory; override for
47
+ tests or containers */
48
+ } mp_config_t;
49
+
50
+ /* Start the monitor thread. cfg may be NULL for all defaults; zero fields in
51
+ * cfg also fall back to their defaults (so partial overrides just work).
52
+ * Returns 0, or -EALREADY if already running, -EINVAL for a bad config. */
53
+ int mp_init(const mp_config_t *cfg);
54
+
55
+ /* Stop the monitor thread and release everything. Idempotent. */
56
+ int mp_shutdown(void);
57
+
58
+ /* Current (post-hysteresis) pressure level. Valid after mp_init. */
59
+ mp_level_t mp_current_level(void);
60
+
61
+ /* Last observed PSI values; returns 0, or -EPERM if mp_init was not called. */
62
+ int mp_psi(mp_psi_t *out);
63
+
64
+ /* Register a callback fired from the monitor thread whenever the level
65
+ * changes. Returns a handle (> 0), or -EINVAL (NULL cb), -EPERM (not
66
+ * initialized). Callbacks must not block; do not call mp_shutdown from a
67
+ * callback. */
68
+ int mp_subscribe(mp_callback_t cb, void *userdata);
69
+
70
+ /* Remove a subscription. Returns 0, or -ENOENT. Safe to call from within a
71
+ * callback for any handle except the one currently being dispatched. */
72
+ int mp_unsubscribe(int handle);
73
+
74
+ const char *mp_level_name(mp_level_t level);
75
+ const char *mp_version(void);
76
+
77
+ #ifdef __cplusplus
78
+ }
79
+ #endif
80
+
81
+ #endif /* MEMPRESSURE_H */
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "libmempressure"
7
+ version = "0.1.0"
8
+ description = "Memory-pressure events from kernel PSI: an onTrimMemory for Linux (Python binding)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{ name = "kutu OS contributors" }]
12
+ requires-python = ">=3.10"
13
+ keywords = ["memory", "psi", "pressure", "linux", "oom", "swap", "performance"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: C",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Topic :: Software Development :: Libraries",
22
+ "Topic :: System :: Operating System Kernels :: Linux",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://kutu.so"
27
+ Repository = "https://github.com/kutuso/libmempressure"
28
+ Documentation = "https://kutu.so/#/LIBMEMPRESSURE.md"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ from setuptools import Extension, setup
2
+
3
+ setup(
4
+ ext_modules=[
5
+ Extension(
6
+ "mempressure",
7
+ sources=[
8
+ "src/mempressure.c",
9
+ "bindings/python/mempressure_module.c",
10
+ ],
11
+ include_dirs=["include"],
12
+ define_macros=[("_GNU_SOURCE", "1")],
13
+ )
14
+ ]
15
+ )
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: libmempressure
3
+ Version: 0.1.0
4
+ Summary: Memory-pressure events from kernel PSI: an onTrimMemory for Linux (Python binding)
5
+ Author: kutu OS contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://kutu.so
8
+ Project-URL: Repository, https://github.com/kutuso/libmempressure
9
+ Project-URL: Documentation, https://kutu.so/#/LIBMEMPRESSURE.md
10
+ Keywords: memory,psi,pressure,linux,oom,swap,performance
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: C
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: System :: Operating System Kernels :: Linux
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # libmempressure — a Linux answer to Android's onTrimMemory
25
+
26
+ Apps on Android never get OOM-killed without warning: the OS sends them
27
+ `onTrimMemory()` callbacks and they shed caches gracefully. Desktop Linux
28
+ just kills them. `libmempressure` closes that gap: it watches the kernel's
29
+ [Pressure Stall Information](https://docs.kernel.org/admin-guide/perf/psi.html)
30
+ and delivers memory-pressure events to your application — in **C, C++, Java
31
+ (JVM) and Python** — so instead of dying under pressure, your app trims.
32
+
33
+ ```
34
+ level some avg10 what your app should do
35
+ ----------- ------------ ------------------------------------
36
+ none < 5% business as usual
37
+ low >= 5% stop prefetching, trim opportunistically
38
+ moderate >= 15% drop object caches, shrink pools
39
+ critical >= 40% shed everything, survive
40
+ ```
41
+
42
+ Events are level *changes* only, filtered by hysteresis (consecutive
43
+ readings) so your callback doesn't flap. The monitor reads
44
+ `/proc/pressure/memory` on its own thread; callbacks fire from that thread.
45
+
46
+ ## The C API
47
+
48
+ ```c
49
+ #include <mempressure.h>
50
+
51
+ static void on_pressure(mp_level_t level, void *userdata) {
52
+ if (level >= MP_LEVEL_MODERATE) cache_drop_all();
53
+ }
54
+
55
+ mp_init(NULL); /* defaults; zero fields fall back too */
56
+ mp_subscribe(on_pressure, my_app);
57
+ /* ... */
58
+ mp_shutdown();
59
+ ```
60
+
61
+ `mp_config_t` tunes thresholds (5/15/40%), poll interval (0.5s) and
62
+ hysteresis (2 readings); any zero field falls back to its default. See
63
+ [`include/mempressure.h`](include/mempressure.h) for the full contract —
64
+ threading rules, error codes, and the `psi_path` override (used by the test
65
+ suites to drive the monitor from fixture files).
66
+
67
+ ## C++
68
+
69
+ Header-only RAII wrapper — [`bindings/c++/mempressure.hpp`](bindings/c++/mempressure.hpp):
70
+
71
+ ```cpp
72
+ mp::Monitor monitor; /* starts the monitor */
73
+ monitor.subscribe([](mp::Level level) {
74
+ if (level >= mp::Level::Moderate) cache_drop_all();
75
+ }); /* std::function, refcounted lifetime */
76
+ ```
77
+
78
+ ## JVM (JNI)
79
+
80
+ ```java
81
+ import io.kutu.mempressure.MemPressure;
82
+
83
+ MemPressure.start(new MemPressure.Config());
84
+ int handle = MemPressure.subscribe(level -> {
85
+ if (level >= 2) cache.dropAll(); // 0=none 1=low 2=moderate 3=critical
86
+ });
87
+ MemPressure.unsubscribe(handle);
88
+ MemPressure.stop();
89
+ ```
90
+
91
+ Native side: `bindings/jvm/jni/mempressure_jni.c` (`libmempressure_jni.so`),
92
+ built automatically when a JDK is present. Callbacks arrive on a daemon
93
+ thread attached to the JVM.
94
+
95
+ ## Python
96
+
97
+ ```python
98
+ import mempressure as mp
99
+
100
+ mp.start() # defaults
101
+ mp.subscribe(lambda level: level >= 2 and cache.drop_all())
102
+ print(mp.psi()) # {'some_avg10': 0.12, ...}
103
+ mp.stop()
104
+ ```
105
+
106
+ CPython extension (`mempressure` module) built against your interpreter;
107
+ callbacks fire with the GIL acquired from the monitor thread.
108
+
109
+ ## Build and test
110
+
111
+ ```sh
112
+ make test # cmake build + ctest (C core, C++ binding, JVM binding)
113
+ # + python binding tests (needs a venv with pytest, see below)
114
+ ```
115
+
116
+ Details:
117
+
118
+ ```sh
119
+ cmake -S . -B build # auto-detects Python dev + JDK; skips gracefully
120
+ cmake --build build
121
+ ctest --test-dir build --output-on-failure
122
+ python3 -m venv .venv && .venv/bin/pip install pytest
123
+ .venv/bin/pytest tests/python -q # (build with -DPython3_EXECUTABLE=.venv/bin/python
124
+ # if your system python differs from the venv's)
125
+ ```
126
+
127
+ The test suites never require real memory pressure: they point the monitor at
128
+ fixture files via `psi_path` and mutate them to drive level changes. The
129
+ example binary (`mp_example_c`) runs against your real
130
+ `/proc/pressure/memory`.
131
+
132
+ ## Distro packaging (RPM / DEB)
133
+
134
+ The library is plain C11 + pthreads and installs via CMake's GNUInstallDirs,
135
+ so it lands correctly on any FHS distro (including `/usr/lib64` RPM
136
+ convention). Upstream ships packaging metadata:
137
+
138
+ - **RPM** (Fedora/RHEL/openSUSE): [`packaging/rpm/libmempressure.spec`](packaging/rpm/libmempressure.spec).
139
+ Build a tarball and rpmbuild it, or point a COPR at the spec:
140
+
141
+ ```sh
142
+ git archive --prefix=libmempressure-0.1.0/ -o libmempressure-0.1.0.tar.gz HEAD
143
+ rpmbuild -bb packaging/rpm/libmempressure.spec --define "_sourcedir $PWD"
144
+ ```
145
+
146
+ - **DEB** (Debian/Ubuntu): a `debian/` directory with `libmempressure0`
147
+ (runtime) and `libmempressure-dev` packages, native-format source:
148
+
149
+ ```sh
150
+ sudo apt install build-essential cmake debhelper pkg-config
151
+ dpkg-buildpackage -us -uc -b
152
+ ```
153
+
154
+ Distro packages ship the C core and the header-only C++ binding
155
+ (`-DMP_PYTHON=OFF -DMP_JAVA=OFF` in the build); the Python and JVM bindings
156
+ are built from the same source wherever those toolchains live. CI exercises
157
+ both paths on every push: `package-rpm` (fedora container) and
158
+ `package-deb` (debian container) build and inspect the actual packages.
159
+
160
+ ## Relationship to kutu OS
161
+
162
+ This is the M3 building block of
163
+ [kutu OS](https://github.com/kutuso/os): the `mempressured` policy daemon and
164
+ eventually applications themselves use these events to shed memory gracefully
165
+ under pressure instead of being killed. The C core has no dependencies beyond
166
+ libc/pthreads, so it ships anywhere — including inside the kutu ISO.
167
+
168
+ MIT licensed — see [LICENSE](LICENSE).
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ bindings/python/mempressure_module.c
7
+ include/mempressure.h
8
+ src/mempressure.c
9
+ src/libmempressure.egg-info/PKG-INFO
10
+ src/libmempressure.egg-info/SOURCES.txt
11
+ src/libmempressure.egg-info/dependency_links.txt
12
+ src/libmempressure.egg-info/top_level.txt
@@ -0,0 +1,341 @@
1
+ #define _GNU_SOURCE
2
+
3
+ #include "mempressure.h"
4
+
5
+ #include <errno.h>
6
+ #include <pthread.h>
7
+ #include <stdio.h>
8
+ #include <stdlib.h>
9
+ #include <string.h>
10
+ #include <time.h>
11
+
12
+ #define MP_DEFAULT_PSI_PATH MP_PSI_PATH_DEFAULT
13
+
14
+ typedef struct sub {
15
+ int handle;
16
+ mp_callback_t cb;
17
+ void *userdata;
18
+ struct sub *next;
19
+ } sub_t;
20
+
21
+ typedef struct {
22
+ mp_callback_t cb;
23
+ void *userdata;
24
+ } sub_snapshot_t;
25
+
26
+ static struct {
27
+ pthread_mutex_t lock;
28
+ pthread_t thread;
29
+ int started;
30
+ int running;
31
+ mp_config_t cfg;
32
+ char *psi_path;
33
+ mp_psi_t psi;
34
+ mp_level_t level;
35
+ mp_level_t pending;
36
+ int pending_count;
37
+ int next_handle;
38
+ sub_t *subs;
39
+ } g = {
40
+ .lock = PTHREAD_MUTEX_INITIALIZER,
41
+ };
42
+
43
+ static void sleep_sec(double seconds) {
44
+ struct timespec ts = {
45
+ .tv_sec = (time_t)seconds,
46
+ .tv_nsec = (long)((seconds - (double)(time_t)seconds) * 1e9),
47
+ };
48
+ nanosleep(&ts, NULL);
49
+ }
50
+
51
+ static int parse_line(const char *line, const char *kind, mp_psi_t *out) {
52
+ if (strncmp(line, kind, strlen(kind)) != 0) {
53
+ return 0;
54
+ }
55
+ const char *cursor = line + strlen(kind);
56
+ while (*cursor == ' ') cursor++;
57
+ char key[8];
58
+ double value;
59
+ int matched = 0;
60
+ while (sscanf(cursor, "%7[a-z0-9_]=%lf", key, &value) == 2) {
61
+ if (strcmp(kind, "some") == 0) {
62
+ if (strcmp(key, "avg10") == 0) { out->some_avg10 = value; matched++; }
63
+ else if (strcmp(key, "avg60") == 0) { out->some_avg60 = value; matched++; }
64
+ else if (strcmp(key, "avg300") == 0) { out->some_avg300 = value; matched++; }
65
+ } else {
66
+ if (strcmp(key, "avg10") == 0) { out->full_avg10 = value; matched++; }
67
+ else if (strcmp(key, "avg60") == 0) { out->full_avg60 = value; matched++; }
68
+ else if (strcmp(key, "avg300") == 0) { out->full_avg300 = value; matched++; }
69
+ }
70
+ while (*cursor && *cursor != ' ') cursor++;
71
+ while (*cursor == ' ') cursor++;
72
+ if (!*cursor) break;
73
+ }
74
+ return matched == 3;
75
+ }
76
+
77
+ static int read_psi(const char *path, mp_psi_t *out) {
78
+ FILE *fh = fopen(path, "r");
79
+ if (!fh) {
80
+ return -errno;
81
+ }
82
+ memset(out, 0, sizeof *out);
83
+ char line[256];
84
+ int have_some = 0;
85
+ int have_full = 0;
86
+ while (fgets(line, sizeof line, fh)) {
87
+ if (parse_line(line, "some", out)) have_some = 1;
88
+ if (parse_line(line, "full", out)) have_full = 1;
89
+ }
90
+ fclose(fh);
91
+ return (have_some && have_full) ? 0 : -EIO;
92
+ }
93
+
94
+ static mp_level_t level_for(const mp_psi_t *psi, const mp_config_t *cfg) {
95
+ double some = psi->some_avg10;
96
+ if (some >= cfg->critical_threshold) return MP_LEVEL_CRITICAL;
97
+ if (some >= cfg->moderate_threshold) return MP_LEVEL_MODERATE;
98
+ if (some >= cfg->low_threshold) return MP_LEVEL_LOW;
99
+ return MP_LEVEL_NONE;
100
+ }
101
+
102
+ typedef struct fire {
103
+ sub_snapshot_t *subs;
104
+ int count;
105
+ mp_level_t level;
106
+ } fire_t;
107
+
108
+ static void *monitor_main(void *arg) {
109
+ (void)arg;
110
+ const mp_config_t cfg = g.cfg;
111
+ const char *psi_path = g.psi_path;
112
+ while (1) {
113
+ pthread_mutex_lock(&g.lock);
114
+ int running = g.running;
115
+ pthread_mutex_unlock(&g.lock);
116
+ if (!running) {
117
+ break;
118
+ }
119
+
120
+ mp_psi_t psi;
121
+ if (read_psi(psi_path, &psi) == 0) {
122
+ mp_level_t reading = level_for(&psi, &cfg);
123
+
124
+ pthread_mutex_lock(&g.lock);
125
+ g.psi = psi;
126
+ if (reading != g.pending) {
127
+ g.pending = reading;
128
+ g.pending_count = 1;
129
+ } else if (g.pending_count < cfg.hysteresis) {
130
+ g.pending_count++;
131
+ }
132
+
133
+ int changed = 0;
134
+ mp_level_t fire_level = g.level;
135
+ if (g.pending_count >= cfg.hysteresis && g.pending != g.level) {
136
+ g.level = g.pending;
137
+ fire_level = g.level;
138
+ changed = 1;
139
+ }
140
+
141
+ fire_t fire = {NULL, 0, fire_level};
142
+ if (changed) {
143
+ int n = 0;
144
+ for (sub_t *s = g.subs; s; s = s->next) n++;
145
+ if (n > 0) {
146
+ fire.subs = malloc((size_t)n * sizeof *fire.subs);
147
+ if (fire.subs) {
148
+ fire.count = n;
149
+ int i = 0;
150
+ for (sub_t *s = g.subs; s; s = s->next) {
151
+ fire.subs[i].cb = s->cb;
152
+ fire.subs[i].userdata = s->userdata;
153
+ i++;
154
+ }
155
+ }
156
+ }
157
+ }
158
+ pthread_mutex_unlock(&g.lock);
159
+
160
+ if (fire.subs) {
161
+ for (int i = 0; i < fire.count; i++) {
162
+ fire.subs[i].cb(fire.level, fire.subs[i].userdata);
163
+ }
164
+ free(fire.subs);
165
+ }
166
+ }
167
+ sleep_sec(cfg.poll_interval_sec);
168
+ }
169
+ return NULL;
170
+ }
171
+
172
+ static int config_valid(const mp_config_t *cfg) {
173
+ if (cfg->low_threshold < 0 || cfg->moderate_threshold <= cfg->low_threshold ||
174
+ cfg->critical_threshold <= cfg->moderate_threshold || cfg->critical_threshold > 100) {
175
+ return 0;
176
+ }
177
+ if (!(cfg->poll_interval_sec > 0) || cfg->hysteresis < 1) {
178
+ return 0;
179
+ }
180
+ return 1;
181
+ }
182
+
183
+ static void config_fill_defaults(mp_config_t *cfg) {
184
+ if (cfg->low_threshold == 0 && cfg->moderate_threshold == 0 && cfg->critical_threshold == 0) {
185
+ cfg->low_threshold = 5.0;
186
+ cfg->moderate_threshold = 15.0;
187
+ cfg->critical_threshold = 40.0;
188
+ }
189
+ if (!(cfg->poll_interval_sec > 0)) {
190
+ cfg->poll_interval_sec = 0.5;
191
+ }
192
+ if (cfg->hysteresis < 1) {
193
+ cfg->hysteresis = 2;
194
+ }
195
+ }
196
+
197
+ int mp_init(const mp_config_t *cfg) {
198
+ pthread_mutex_lock(&g.lock);
199
+ if (g.started) {
200
+ pthread_mutex_unlock(&g.lock);
201
+ return -EALREADY;
202
+ }
203
+ memset(&g.cfg, 0, sizeof g.cfg);
204
+ if (cfg) {
205
+ g.cfg = *cfg;
206
+ }
207
+ config_fill_defaults(&g.cfg);
208
+ if (!config_valid(&g.cfg)) {
209
+ pthread_mutex_unlock(&g.lock);
210
+ return -EINVAL;
211
+ }
212
+ g.psi_path = strdup(cfg && cfg->psi_path ? cfg->psi_path : MP_DEFAULT_PSI_PATH);
213
+ if (!g.psi_path) {
214
+ pthread_mutex_unlock(&g.lock);
215
+ return -ENOMEM;
216
+ }
217
+ g.psi = (mp_psi_t){0};
218
+ g.level = MP_LEVEL_NONE;
219
+ g.pending = MP_LEVEL_NONE;
220
+ g.pending_count = 0;
221
+ g.next_handle = 1;
222
+ g.subs = NULL;
223
+ g.running = 1;
224
+ g.started = 1;
225
+ if (pthread_create(&g.thread, NULL, monitor_main, NULL) != 0) {
226
+ free(g.psi_path);
227
+ g.psi_path = NULL;
228
+ g.started = 0;
229
+ g.running = 0;
230
+ pthread_mutex_unlock(&g.lock);
231
+ return -errno;
232
+ }
233
+ pthread_mutex_unlock(&g.lock);
234
+ return 0;
235
+ }
236
+
237
+ int mp_shutdown(void) {
238
+ pthread_mutex_lock(&g.lock);
239
+ if (!g.started) {
240
+ pthread_mutex_unlock(&g.lock);
241
+ return 0;
242
+ }
243
+ g.running = 0;
244
+ pthread_t thread = g.thread;
245
+ sub_t *subs = g.subs;
246
+ g.subs = NULL;
247
+ char *psi_path = g.psi_path;
248
+ g.psi_path = NULL;
249
+ g.started = 0;
250
+ pthread_mutex_unlock(&g.lock);
251
+
252
+ pthread_join(thread, NULL);
253
+ while (subs) {
254
+ sub_t *next = subs->next;
255
+ free(subs);
256
+ subs = next;
257
+ }
258
+ free(psi_path);
259
+ return 0;
260
+ }
261
+
262
+ mp_level_t mp_current_level(void) {
263
+ pthread_mutex_lock(&g.lock);
264
+ mp_level_t level = g.level;
265
+ pthread_mutex_unlock(&g.lock);
266
+ return level;
267
+ }
268
+
269
+ int mp_psi(mp_psi_t *out) {
270
+ if (!out) {
271
+ return -EINVAL;
272
+ }
273
+ pthread_mutex_lock(&g.lock);
274
+ if (!g.started) {
275
+ pthread_mutex_unlock(&g.lock);
276
+ return -EPERM;
277
+ }
278
+ *out = g.psi;
279
+ pthread_mutex_unlock(&g.lock);
280
+ return 0;
281
+ }
282
+
283
+ int mp_subscribe(mp_callback_t cb, void *userdata) {
284
+ if (!cb) {
285
+ return -EINVAL;
286
+ }
287
+ pthread_mutex_lock(&g.lock);
288
+ if (!g.started) {
289
+ pthread_mutex_unlock(&g.lock);
290
+ return -EPERM;
291
+ }
292
+ sub_t *sub = malloc(sizeof *sub);
293
+ if (!sub) {
294
+ pthread_mutex_unlock(&g.lock);
295
+ return -ENOMEM;
296
+ }
297
+ sub->handle = g.next_handle++;
298
+ sub->cb = cb;
299
+ sub->userdata = userdata;
300
+ sub->next = g.subs;
301
+ g.subs = sub;
302
+ int handle = sub->handle;
303
+ pthread_mutex_unlock(&g.lock);
304
+ return handle;
305
+ }
306
+
307
+ int mp_unsubscribe(int handle) {
308
+ pthread_mutex_lock(&g.lock);
309
+ sub_t **cursor = &g.subs;
310
+ while (*cursor) {
311
+ if ((*cursor)->handle == handle) {
312
+ sub_t *dead = *cursor;
313
+ *cursor = dead->next;
314
+ free(dead);
315
+ pthread_mutex_unlock(&g.lock);
316
+ return 0;
317
+ }
318
+ cursor = &(*cursor)->next;
319
+ }
320
+ pthread_mutex_unlock(&g.lock);
321
+ return -ENOENT;
322
+ }
323
+
324
+ const char *mp_level_name(mp_level_t level) {
325
+ switch (level) {
326
+ case MP_LEVEL_NONE:
327
+ return "none";
328
+ case MP_LEVEL_LOW:
329
+ return "low";
330
+ case MP_LEVEL_MODERATE:
331
+ return "moderate";
332
+ case MP_LEVEL_CRITICAL:
333
+ return "critical";
334
+ default:
335
+ return "unknown";
336
+ }
337
+ }
338
+
339
+ const char *mp_version(void) {
340
+ return MP_VERSION;
341
+ }