From 94e6da0fcae1be8ca68c93edd43a2b6ca45315fd Mon Sep 17 00:00:00 2001 From: Kate Hsuan Date: Fri, 21 Mar 2025 15:31:58 +0800 Subject: [PATCH 1/5] gio: gmemorymonitorbase: parent class of GMemoryMonitor backends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This class provides the shared functions, such as sending a signal and string and value conversion. The backend classes should inherit this class to get the shared functions. It adds a configure time check for `sysinfo()`, as some systems don’t have it. --- gio/gmemorymonitorbase.c | 153 +++++++++++++++++++++++++++++++++++++++ gio/gmemorymonitorbase.h | 53 ++++++++++++++ gio/meson.build | 1 + meson.build | 1 + 4 files changed, 208 insertions(+) create mode 100644 gio/gmemorymonitorbase.c create mode 100644 gio/gmemorymonitorbase.h diff --git a/gio/gmemorymonitorbase.c b/gio/gmemorymonitorbase.c new file mode 100644 index 000000000..ce28ebe33 --- /dev/null +++ b/gio/gmemorymonitorbase.c @@ -0,0 +1,153 @@ +/* GIO - GLib Input, Output and Streaming Library + * + * Copyright 2025 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see . + */ + +#include "config.h" + +#include "gcancellable.h" +#include "ginitable.h" +#include "gioerror.h" +#include "giomodule-priv.h" +#include "glib/gstdio.h" +#include "glibintl.h" +#include "gmemorymonitor.h" +#include "gmemorymonitorbase.h" + +#ifdef HAVE_SYSINFO +#include +#endif + +/** + * GMemoryMonitorBase: + * + * An abstract base class for implementations of [iface@Gio.MemoryMonitor] which + * provides several defined warning levels (`GLowMemoryLevel`) and tracks how + * often they are notified to the user via [signal@Gio.MemoryMonitor::low-memory-warning] + * to limit the number of signal emissions to one every 15 seconds for each level. + * [method@Gio.MemoryMonitorBase.send_event_to_user] is provided for this purpose. + */ + +/* The interval between sending a signal in second */ +#define RECOVERY_INTERVAL_SEC 15 + +#define G_MEMORY_MONITOR_BASE_GET_INITABLE_IFACE(o) (G_TYPE_INSTANCE_GET_INTERFACE ((o), G_TYPE_INITABLE, GInitable)) + +static void g_memory_monitor_base_iface_init (GMemoryMonitorInterface *iface); +static void g_memory_monitor_base_initable_iface_init (GInitableIface *iface); + +typedef struct +{ + GObject parent_instance; + + guint64 last_trigger_us[G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_COUNT]; +} GMemoryMonitorBasePrivate; + + +G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GMemoryMonitorBase, g_memory_monitor_base, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, + g_memory_monitor_base_initable_iface_init) + G_IMPLEMENT_INTERFACE (G_TYPE_MEMORY_MONITOR, + g_memory_monitor_base_iface_init) + G_ADD_PRIVATE (GMemoryMonitorBase)) + +gdouble +g_memory_monitor_base_query_mem_ratio (void) +{ +#ifdef HAVE_SYSINFO + struct sysinfo info; + + if (sysinfo (&info)) + return -1.0; + + if (info.totalram == 0) + return -1.0; + + return (gdouble) ((gdouble) info.freeram / (gdouble) info.totalram); +#else + return -1.0; +#endif +} + +GMemoryMonitorWarningLevel +g_memory_monitor_base_level_enum_to_byte (GMemoryMonitorLowMemoryLevel level) +{ + const GMemoryMonitorWarningLevel level_bytes[G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_COUNT] = { + [G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_LOW] = 50, + [G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_MEDIUM] = 100, + [G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_CRITICAL] = 255 + }; + + if ((int) level < G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_INVALID || + (int) level >= G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_COUNT) + g_assert_not_reached (); + + if (level == G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_INVALID) + return 0; + + return level_bytes[level]; +} + +void +g_memory_monitor_base_send_event_to_user (GMemoryMonitorBase *monitor, + GMemoryMonitorLowMemoryLevel warning_level) +{ + gint64 current_time; + GMemoryMonitorBasePrivate *priv = g_memory_monitor_base_get_instance_private (monitor); + + current_time = g_get_monotonic_time (); + + if (priv->last_trigger_us[warning_level] == 0 || + (current_time - priv->last_trigger_us[warning_level]) > (RECOVERY_INTERVAL_SEC * G_USEC_PER_SEC)) + { + g_debug ("Send low memory signal with warning level %u", warning_level); + + g_signal_emit_by_name (monitor, "low-memory-warning", + g_memory_monitor_base_level_enum_to_byte (warning_level)); + priv->last_trigger_us[warning_level] = current_time; + } +} + +static gboolean +g_memory_monitor_base_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + return TRUE; +} + +static void +g_memory_monitor_base_init (GMemoryMonitorBase *monitor) +{ +} + +static void +g_memory_monitor_base_class_init (GMemoryMonitorBaseClass *klass) +{ +} + +static void +g_memory_monitor_base_iface_init (GMemoryMonitorInterface *monitor_iface) +{ +} + +static void +g_memory_monitor_base_initable_iface_init (GInitableIface *iface) +{ + iface->init = g_memory_monitor_base_initable_init; +} diff --git a/gio/gmemorymonitorbase.h b/gio/gmemorymonitorbase.h new file mode 100644 index 000000000..847d55515 --- /dev/null +++ b/gio/gmemorymonitorbase.h @@ -0,0 +1,53 @@ +/* GIO - GLib Input, Output and Streaming Library + * + * Copyright 2025 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see . + */ + +#ifndef __G_MEMORY_MONITOR_BASE_H__ +#define __G_MEMORY_MONITOR_BASE_H__ + +#include + +G_BEGIN_DECLS + +#define G_TYPE_MEMORY_MONITOR_BASE (g_memory_monitor_base_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GMemoryMonitorBase, g_memory_monitor_base, G, MEMORY_MONITOR_BASE, GObject) + +typedef enum +{ + G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_INVALID = -1, + G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_LOW = 0, + G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_MEDIUM, + G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_CRITICAL, + G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_COUNT +} GMemoryMonitorLowMemoryLevel; + +struct _GMemoryMonitorBaseClass +{ + GObjectClass parent_class; +}; + +void g_memory_monitor_base_send_event_to_user (GMemoryMonitorBase *monitor, + GMemoryMonitorLowMemoryLevel warning_level); +GMemoryMonitorWarningLevel g_memory_monitor_base_level_enum_to_byte (GMemoryMonitorLowMemoryLevel level); +gdouble g_memory_monitor_base_query_mem_ratio (void); + +G_END_DECLS + +#endif /* __G_MEMORY_MONITOR_BASE_H__ */ diff --git a/gio/meson.build b/gio/meson.build index 854b95afa..2d0c2ee40 100644 --- a/gio/meson.build +++ b/gio/meson.build @@ -528,6 +528,7 @@ gio_base_sources = files( 'gmarshal-internal.c', 'gmount.c', 'gmemorymonitor.c', + 'gmemorymonitorbase.c', 'gmemorymonitordbus.c', 'gmemoryinputstream.c', 'gmemoryoutputstream.c', diff --git a/meson.build b/meson.build index df87f0eed..b569da1d5 100644 --- a/meson.build +++ b/meson.build @@ -759,6 +759,7 @@ functions = [ 'strtoll_l', 'strtoull_l', 'symlink', + 'sysinfo', 'timegm', 'unsetenv', 'uselocale', From 308709b8d949df14aee633e536a76eb785b4b66d Mon Sep 17 00:00:00 2001 From: Kate Hsuan Date: Fri, 21 Mar 2025 15:34:11 +0800 Subject: [PATCH 2/5] gio: gmemorymonitorpsi: Kernel PSI backend for GMemoryMonitor The PSI backend is based on Kernel PSI [1]. It monitors the memory allocation time with in a given time window. If the allocation time is more than the given threshold, Kernel signal the application to deliver the memory pressure events. The current thresholds are: LOW: 70ms out of 2 seconds for partial stall MEDIUM: 100ms out of 2 seconds for partial stall CRITICAL: 100ms out of 2 seconds for full stall [1] https://docs.kernel.org/accounting/psi.html --- gio/giomodule.c | 9 + gio/gmemorymonitorpsi.c | 512 ++++++++++++++++++++++++++++++++++++++++ gio/gmemorymonitorpsi.h | 38 +++ gio/meson.build | 1 + 4 files changed, 560 insertions(+) create mode 100644 gio/gmemorymonitorpsi.c create mode 100644 gio/gmemorymonitorpsi.h diff --git a/gio/giomodule.c b/gio/giomodule.c index 76c202848..7491fd433 100644 --- a/gio/giomodule.c +++ b/gio/giomodule.c @@ -52,6 +52,9 @@ #include "gmemorymonitor.h" #include "gmemorymonitorportal.h" #include "gmemorymonitordbus.h" +#ifdef __linux__ +#include "gmemorymonitorpsi.h" +#endif #include "gpowerprofilemonitor.h" #include "gpowerprofilemonitordbus.h" #include "gpowerprofilemonitorportal.h" @@ -1081,6 +1084,9 @@ extern GType _g_network_monitor_nm_get_type (void); extern GType g_debug_controller_dbus_get_type (void); extern GType g_memory_monitor_dbus_get_type (void); +#ifdef __linux__ +extern GType g_memory_monitor_psi_get_type (void); +#endif extern GType g_memory_monitor_portal_get_type (void); extern GType g_memory_monitor_win32_get_type (void); extern GType g_power_profile_monitor_dbus_get_type (void); @@ -1361,6 +1367,9 @@ _g_io_modules_ensure_loaded (void) g_type_ensure (g_gtk_notification_backend_get_type ()); g_type_ensure (g_portal_notification_backend_get_type ()); g_type_ensure (g_memory_monitor_dbus_get_type ()); +#ifdef __linux__ + g_type_ensure (g_memory_monitor_psi_get_type ()); +#endif g_type_ensure (g_memory_monitor_portal_get_type ()); g_type_ensure (g_network_monitor_portal_get_type ()); g_type_ensure (g_power_profile_monitor_portal_get_type ()); diff --git a/gio/gmemorymonitorpsi.c b/gio/gmemorymonitorpsi.c new file mode 100644 index 000000000..b21f504ea --- /dev/null +++ b/gio/gmemorymonitorpsi.c @@ -0,0 +1,512 @@ +/* GIO - GLib Input, Output and Streaming Library + * + * Copyright 2025 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see . + */ + +#include "config.h" + +#include "gcancellable.h" +#include "gdbusnamewatching.h" +#include "gdbusproxy.h" +#include "ginitable.h" +#include "gioerror.h" +#include "giomodule-priv.h" +#include "glibintl.h" +#include "glib/glib-private.h" +#include "glib/gstdio.h" +#include "gmemorymonitor.h" +#include "gmemorymonitorbase.h" +#include "gmemorymonitorpsi.h" + +#include +#include +#include + +/** + * GMemoryMonitorPsi: + * + * A Linux [iface@Gio.MemoryMonitor] which uses the kernel + * [pressure stall information](https://www.kernel.org/doc/html/latest/accounting/psi.html) (PSI). + * + * When it receives a PSI event, it emits + * [signal@Gio.MemoryMonitor::low-memory-warning] with an appropriate warning + * level. + * + * Since: 2.86 + */ + +/* Unprivileged users can also create monitors, with + * the only limitation that the window size must be a + * `multiple of 2s`, in order to prevent excessive resource usage. + * see: https://www.kernel.org/doc/html/latest/accounting/psi.html*/ +#define PSI_WINDOW_SEC 2 + +typedef enum { + PROP_PROC_PATH = 1, +} GMemoryMonitorPsiProperty; + +typedef enum +{ + MEMORY_PRESSURE_MONITOR_TRIGGER_SOME, + MEMORY_PRESSURE_MONITOR_TRIGGER_FULL, + MEMORY_PRESSURE_MONITOR_TRIGGER_MFD +} MemoryPressureMonitorTriggerType; + +/* Each trigger here results in an open fd for the lifetime + * of the `GMemoryMonitor`, so don’t add too many */ +static const struct +{ + MemoryPressureMonitorTriggerType trigger_type; + int threshold_ms; +} triggers[G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_COUNT] = { + { MEMORY_PRESSURE_MONITOR_TRIGGER_SOME, 70 }, /* 70ms out of 2sec for partial stall */ + { MEMORY_PRESSURE_MONITOR_TRIGGER_SOME, 100 }, /* 100ms out of 2sec for partial stall */ + { MEMORY_PRESSURE_MONITOR_TRIGGER_FULL, 100 }, /* 100ms out of 2sec for complete stall */ +}; + +typedef struct +{ + GSource source; + GPollFD *pollfd; + GMemoryMonitorLowMemoryLevel level_type; + GWeakRef monitor_weak; +} MemoryMonitorSource; + +typedef gboolean (*MemoryMonitorCallbackFunc) (GMemoryMonitorPsi *monitor, + GMemoryMonitorLowMemoryLevel level_type, + void *user_data); + +#define G_MEMORY_MONITOR_PSI_GET_INITABLE_IFACE(o) (G_TYPE_INSTANCE_GET_INTERFACE ((o), G_TYPE_INITABLE, GInitable)) + +static void g_memory_monitor_psi_iface_init (GMemoryMonitorInterface *iface); +static void g_memory_monitor_psi_initable_iface_init (GInitableIface *iface); + +struct _GMemoryMonitorPsi +{ + GMemoryMonitorBase parent_instance; + GMainContext *worker; /* (unowned) */ + GSource *triggers[G_MEMORY_MONITOR_LOW_MEMORY_LEVEL_COUNT]; /* (owned) (nullable) */ + + char *cg_path; + char *proc_path; + + gboolean proc_override; +}; + +G_DEFINE_TYPE_WITH_CODE (GMemoryMonitorPsi, g_memory_monitor_psi, G_TYPE_MEMORY_MONITOR_BASE, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, + g_memory_monitor_psi_initable_iface_init) + G_IMPLEMENT_INTERFACE (G_TYPE_MEMORY_MONITOR, + g_memory_monitor_psi_iface_init) + _g_io_modules_ensure_extension_points_registered (); + g_io_extension_point_implement (G_MEMORY_MONITOR_EXTENSION_POINT_NAME, + g_define_type_id, + "psi", + 20)) + +static void +g_memory_monitor_psi_init (GMemoryMonitorPsi *psi) +{ +} + +static void +g_memory_monitor_psi_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GMemoryMonitorPsi *monitor = G_MEMORY_MONITOR_PSI (object); + + switch ((GMemoryMonitorPsiProperty) prop_id) + { + case PROP_PROC_PATH: + /* Construct only */ + g_assert (monitor->proc_path == NULL); + monitor->proc_path = g_value_dup_string (value); + if (monitor->proc_path != NULL) + monitor->proc_override = TRUE; + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +g_memory_monitor_psi_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GMemoryMonitorPsi *monitor = G_MEMORY_MONITOR_PSI (object); + + switch ((GMemoryMonitorPsiProperty) prop_id) + { + case PROP_PROC_PATH: + g_value_set_string (value, monitor->proc_override ? monitor->proc_path : NULL); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static gboolean +g_memory_monitor_low_trigger_cb (GMemoryMonitorPsi *monitor, + GMemoryMonitorLowMemoryLevel level_type, + void *user_data) +{ + gdouble mem_ratio; + + /* Should be executed in the worker context */ + g_assert (g_main_context_is_owner (monitor->worker)); + + mem_ratio = g_memory_monitor_base_query_mem_ratio (); + + /* if mem free ratio > 0.5, don't signal */ + if (mem_ratio < 0.0) + return G_SOURCE_REMOVE; + else if (mem_ratio > 0.5) + return G_SOURCE_CONTINUE; + + g_memory_monitor_base_send_event_to_user (G_MEMORY_MONITOR_BASE (monitor), level_type); + + return G_SOURCE_CONTINUE; +} + +static gboolean +event_check (GSource *source) +{ + MemoryMonitorSource *ev_source = (MemoryMonitorSource *) source; + + if (ev_source->pollfd->revents) + return G_SOURCE_CONTINUE; + + return G_SOURCE_REMOVE; +} + +static gboolean +event_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + MemoryMonitorSource *ev_source = (MemoryMonitorSource *) source; + GMemoryMonitorPsi *monitor = NULL; + + monitor = g_weak_ref_get (&ev_source->monitor_weak); + if (monitor == NULL) + return G_SOURCE_REMOVE; + + if (monitor->proc_override) + { + if (!(g_source_query_unix_fd (source, ev_source->pollfd) & G_IO_IN)) + { + g_object_unref (monitor); + return G_SOURCE_CONTINUE; + } + } + else + { + if (!(g_source_query_unix_fd (source, ev_source->pollfd) & G_IO_PRI)) + { + g_object_unref (monitor); + return G_SOURCE_CONTINUE; + } + } + + if (callback) + ((MemoryMonitorCallbackFunc) callback) (monitor, ev_source->level_type, user_data); + + g_object_unref (monitor); + + return G_SOURCE_CONTINUE; +} + +static void +event_finalize (GSource *source) +{ + MemoryMonitorSource *ev_source = (MemoryMonitorSource *) source; + + g_weak_ref_clear (&ev_source->monitor_weak); +} + +static GSourceFuncs memory_monitor_event_funcs = { + .check = event_check, + .dispatch = event_dispatch, + .finalize = event_finalize, +}; + +static GSource * +g_memory_monitor_create_source (GMemoryMonitorPsi *monitor, + int fd, + GMemoryMonitorLowMemoryLevel level_type, + gboolean is_path_override) +{ + MemoryMonitorSource *source; + + source = (MemoryMonitorSource *) g_source_new (&memory_monitor_event_funcs, sizeof (MemoryMonitorSource)); + if (is_path_override) + source->pollfd = g_source_add_unix_fd ((GSource *) source, fd, G_IO_IN | G_IO_ERR); + else + source->pollfd = g_source_add_unix_fd ((GSource *) source, fd, G_IO_PRI | G_IO_ERR); + source->level_type = level_type; + g_weak_ref_init (&source->monitor_weak, monitor); + + return (GSource *) source; +} + +static gboolean +g_memory_monitor_psi_calculate_mem_pressure_path (GMemoryMonitorPsi *monitor, + GError **error) +{ + pid_t pid; + gchar *path_read = NULL; + gchar *replacement = NULL; + GRegex *regex = NULL; + + if (!monitor->proc_override) + { + pid = getpid (); + monitor->proc_path = g_strdup_printf ("/proc/%d/cgroup", pid); + } + + if (!g_file_get_contents (monitor->proc_path, &path_read, NULL, error)) + { + g_free (path_read); + return FALSE; + } + + /* cgroupv2 is only supportted and the format is shown as follows: + * ex: 0::/user.slice/user-0.slice/session-c3.scope */ + regex = g_regex_new ("^0::", G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT, error); + + if (!g_regex_match (regex, path_read, G_REGEX_MATCH_DEFAULT, NULL)) + { + g_debug ("Unsupported cgroup path information."); + g_free (path_read); + g_regex_unref (regex); + return FALSE; + } + + /* drop "0::" */ + replacement = g_regex_replace (regex, path_read, + -1, 0, + "", G_REGEX_MATCH_DEFAULT, error); + if (replacement == NULL) + { + g_debug ("Unsupported cgroup path format."); + g_free (path_read); + g_regex_unref (regex); + return FALSE; + } + + replacement = g_strstrip (replacement); + + if (monitor->proc_override) + { + monitor->cg_path = g_steal_pointer (&replacement); + g_free (path_read); + g_regex_unref (regex); + return TRUE; + } + + monitor->cg_path = g_build_filename ("/sys/fs/cgroup", replacement, "memory.pressure", NULL); + g_debug ("cgroup path is %s", monitor->cg_path); + + g_free (path_read); + g_free (replacement); + g_regex_unref (regex); + return g_file_test (monitor->cg_path, G_FILE_TEST_EXISTS); +} + +static GSource * +g_memory_monitor_psi_setup_trigger (GMemoryMonitorPsi *monitor, + GMemoryMonitorLowMemoryLevel level_type, + int threshold_us, + int window_us, + GError **error) +{ + GSource *source; + int fd; + int ret; + size_t wlen; + gchar *trigger = NULL; + + fd = g_open (monitor->cg_path, O_RDWR | O_NONBLOCK | O_CLOEXEC); + if (fd < 0) + { + int errsv = errno; + g_debug ("Error on opening %s: %s", monitor->cg_path, g_strerror (errsv)); + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv), + "Error on opening ‘%s’: %s", monitor->cg_path, g_strerror (errsv)); + return NULL; + } + + /* The kernel PSI [1] trigger format is: + *