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.
- libmempressure-0.1.0/LICENSE +21 -0
- libmempressure-0.1.0/MANIFEST.in +5 -0
- libmempressure-0.1.0/PKG-INFO +168 -0
- libmempressure-0.1.0/README.md +145 -0
- libmempressure-0.1.0/bindings/python/mempressure_module.c +193 -0
- libmempressure-0.1.0/include/mempressure.h +81 -0
- libmempressure-0.1.0/pyproject.toml +28 -0
- libmempressure-0.1.0/setup.cfg +4 -0
- libmempressure-0.1.0/setup.py +15 -0
- libmempressure-0.1.0/src/libmempressure.egg-info/PKG-INFO +168 -0
- libmempressure-0.1.0/src/libmempressure.egg-info/SOURCES.txt +12 -0
- libmempressure-0.1.0/src/libmempressure.egg-info/dependency_links.txt +1 -0
- libmempressure-0.1.0/src/libmempressure.egg-info/top_level.txt +1 -0
- libmempressure-0.1.0/src/mempressure.c +341 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mempressure
|
|
@@ -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
|
+
}
|