diff options
Diffstat (limited to '')
-rw-r--r-- | app/widgets/gimpdashboard.c | 5054 |
1 files changed, 5054 insertions, 0 deletions
diff --git a/app/widgets/gimpdashboard.c b/app/widgets/gimpdashboard.c new file mode 100644 index 0000000..8e8273b --- /dev/null +++ b/app/widgets/gimpdashboard.c @@ -0,0 +1,5054 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995-1999 Spencer Kimball and Peter Mattis + * + * gimpdashboard.c + * Copyright (C) 2017 Ell + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> + +#include <gegl.h> +#include <gio/gio.h> +#include <gtk/gtk.h> + +#ifdef G_OS_WIN32 +#include <windows.h> +#include <psapi.h> +#define HAVE_CPU_GROUP +#define HAVE_MEMORY_GROUP +#elif defined(PLATFORM_OSX) +#include <mach/mach.h> +#include <sys/times.h> +#define HAVE_CPU_GROUP +#define HAVE_MEMORY_GROUP +#else /* ! G_OS_WIN32 && ! PLATFORM_OSX */ +#ifdef HAVE_SYS_TIMES_H +#include <sys/times.h> +#define HAVE_CPU_GROUP +#endif /* HAVE_SYS_TIMES_H */ +#if defined (HAVE_UNISTD_H) && defined (HAVE_FCNTL_H) +#include <unistd.h> +#include <fcntl.h> +#ifdef _SC_PAGE_SIZE +#define HAVE_MEMORY_GROUP +#endif /* _SC_PAGE_SIZE */ +#endif /* HAVE_UNISTD_H && HAVE_FCNTL_H */ +#endif /* ! G_OS_WIN32 */ + +#include "libgimpbase/gimpbase.h" +#include "libgimpmath/gimpmath.h" +#include "libgimpwidgets/gimpwidgets.h" + +#include "widgets-types.h" + +#include "core/gimp.h" +#include "core/gimp-gui.h" +#include "core/gimp-utils.h" +#include "core/gimp-parallel.h" +#include "core/gimpasync.h" +#include "core/gimpbacktrace.h" +#include "core/gimptempbuf.h" +#include "core/gimpwaitable.h" + +#include "gimpactiongroup.h" +#include "gimpdocked.h" +#include "gimpdashboard.h" +#include "gimpdialogfactory.h" +#include "gimphelp-ids.h" +#include "gimphighlightablebutton.h" +#include "gimpmeter.h" +#include "gimpsessioninfo-aux.h" +#include "gimptoggleaction.h" +#include "gimpuimanager.h" +#include "gimpwidgets-utils.h" +#include "gimpwindowstrategy.h" + +#include "gimp-intl.h" +#include "gimp-log.h" +#include "gimp-version.h" + + +#define DEFAULT_UPDATE_INTERVAL GIMP_DASHBOARD_UPDATE_INTERVAL_0_25_SEC +#define DEFAULT_HISTORY_DURATION GIMP_DASHBOARD_HISTORY_DURATION_60_SEC +#define DEFAULT_LOW_SWAP_SPACE_WARNING TRUE + +#define LOW_SWAP_SPACE_WARNING_ON /* swap occupied is above */ 0.90 /* of swap limit */ +#define LOW_SWAP_SPACE_WARNING_OFF /* swap occupied is below */ 0.85 /* of swap limit */ + +#define CPU_ACTIVE_ON /* individual cpu usage is above */ 0.75 +#define CPU_ACTIVE_OFF /* individual cpu usage is below */ 0.25 + +#define LOG_VERSION 1 +#define LOG_SAMPLE_FREQUENCY_MIN 1 /* samples per second */ +#define LOG_SAMPLE_FREQUENCY_MAX 1000 /* samples per second */ +#define LOG_DEFAULT_SAMPLE_FREQUENCY 10 /* samples per second */ +#define LOG_DEFAULT_BACKTRACE TRUE +#define LOG_DEFAULT_MESSAGES TRUE +#define LOG_DEFAULT_PROGRESSIVE FALSE + + +typedef enum +{ + VARIABLE_NONE, + FIRST_VARIABLE, + + + /* cache */ + VARIABLE_CACHE_OCCUPIED = FIRST_VARIABLE, + VARIABLE_CACHE_MAXIMUM, + VARIABLE_CACHE_LIMIT, + + VARIABLE_CACHE_COMPRESSION, + VARIABLE_CACHE_HIT_MISS, + + /* swap */ + VARIABLE_SWAP_OCCUPIED, + VARIABLE_SWAP_SIZE, + VARIABLE_SWAP_LIMIT, + + VARIABLE_SWAP_QUEUED, + VARIABLE_SWAP_QUEUE_STALLS, + VARIABLE_SWAP_QUEUE_FULL, + + VARIABLE_SWAP_READ, + VARIABLE_SWAP_READ_THROUGHPUT, + VARIABLE_SWAP_WRITTEN, + VARIABLE_SWAP_WRITE_THROUGHPUT, + + VARIABLE_SWAP_COMPRESSION, + +#ifdef HAVE_CPU_GROUP + /* cpu */ + VARIABLE_CPU_USAGE, + VARIABLE_CPU_ACTIVE, + VARIABLE_CPU_ACTIVE_TIME, +#endif + +#ifdef HAVE_MEMORY_GROUP + /* memory */ + VARIABLE_MEMORY_USED, + VARIABLE_MEMORY_AVAILABLE, + VARIABLE_MEMORY_SIZE, +#endif + + /* misc */ + VARIABLE_MIPMAPED, + VARIABLE_ASSIGNED_THREADS, + VARIABLE_ACTIVE_THREADS, + VARIABLE_ASYNC_RUNNING, + VARIABLE_TILE_ALLOC_TOTAL, + VARIABLE_SCRATCH_TOTAL, + VARIABLE_TEMP_BUF_TOTAL, + + + N_VARIABLES, + + VARIABLE_SEPARATOR +} Variable; + +typedef enum +{ + VARIABLE_TYPE_BOOLEAN, + VARIABLE_TYPE_INTEGER, + VARIABLE_TYPE_SIZE, + VARIABLE_TYPE_SIZE_RATIO, + VARIABLE_TYPE_INT_RATIO, + VARIABLE_TYPE_PERCENTAGE, + VARIABLE_TYPE_DURATION, + VARIABLE_TYPE_RATE_OF_CHANGE +} VariableType; + +typedef enum +{ + FIRST_GROUP, + + GROUP_CACHE = FIRST_GROUP, + GROUP_SWAP, +#ifdef HAVE_CPU_GROUP + GROUP_CPU, +#endif +#ifdef HAVE_MEMORY_GROUP + GROUP_MEMORY, +#endif + GROUP_MISC, + + N_GROUPS +} Group; + + +typedef struct _VariableInfo VariableInfo; +typedef struct _FieldInfo FieldInfo; +typedef struct _GroupInfo GroupInfo; +typedef struct _VariableData VariableData; +typedef struct _FieldData FieldData; +typedef struct _GroupData GroupData; + +typedef void (* VariableFunc) (GimpDashboard *dashboard, + Variable variable); + + +struct _VariableInfo +{ + const gchar *name; + const gchar *title; + const gchar *description; + VariableType type; + gboolean exclude_from_log; + GimpRGB color; + VariableFunc sample_func; + VariableFunc reset_func; + gconstpointer data; +}; + +struct _FieldInfo +{ + Variable variable; + const gchar *title; + gboolean default_active; + gboolean show_in_header; + Variable meter_variable; + gint meter_value; + gboolean meter_cumulative; +}; + +struct _GroupInfo +{ + const gchar *name; + const gchar *title; + const gchar *description; + gboolean default_active; + gboolean default_expanded; + gboolean has_meter; + Variable meter_limit; + const Variable *meter_led; + const FieldInfo *fields; +}; + +struct _VariableData +{ + gboolean available; + + union + { + gboolean boolean; + gint integer; + guint64 size; /* in bytes */ + struct + { + guint64 antecedent; + guint64 consequent; + } size_ratio; + struct + { + gint antecedent; + gint consequent; + } int_ratio; + gdouble percentage; /* from 0 to 1 */ + gdouble duration; /* in seconds */ + gdouble rate_of_change; /* in source units per second */ + } value; + + gpointer data; + gsize data_size; +}; + +struct _FieldData +{ + gboolean active; + + GtkCheckMenuItem *menu_item; + GtkLabel *value_label; +}; + +struct _GroupData +{ + gint n_fields; + gint n_meter_values; + + gboolean active; + gdouble limit; + + GimpToggleAction *action; + GtkExpander *expander; + GtkLabel *header_values_label; + GtkButton *menu_button; + GtkMenu *menu; + GimpMeter *meter; + GtkTable *table; + + FieldData *fields; +}; + +struct _GimpDashboardPrivate +{ + Gimp *gimp; + + VariableData variables[N_VARIABLES]; + GroupData groups[N_GROUPS]; + + GThread *thread; + GMutex mutex; + GCond cond; + gboolean quit; + gboolean update_now; + + gint update_idle_id; + gint low_swap_space_idle_id; + + GimpDashboardUpdateInteval update_interval; + GimpDashboardHistoryDuration history_duration; + gboolean low_swap_space_warning; + + GOutputStream *log_output; + GError *log_error; + GimpDashboardLogParams log_params; + gint64 log_start_time; + gint log_n_samples; + gint log_n_markers; + VariableData log_variables[N_VARIABLES]; + GimpBacktrace *log_backtrace; + GHashTable *log_addresses; + GimpLogHandler log_log_handler; + + GimpHighlightableButton *log_record_button; + GtkLabel *log_add_marker_label; +}; + + +/* local function prototypes */ + +static void gimp_dashboard_docked_iface_init (GimpDockedInterface *iface); + +static void gimp_dashboard_constructed (GObject *object); +static void gimp_dashboard_dispose (GObject *object); +static void gimp_dashboard_finalize (GObject *object); + +static void gimp_dashboard_map (GtkWidget *widget); +static void gimp_dashboard_unmap (GtkWidget *widget); + +static void gimp_dashboard_set_aux_info (GimpDocked *docked, + GList *aux_info); +static GList * gimp_dashboard_get_aux_info (GimpDocked *docked); + +static gboolean gimp_dashboard_group_expander_button_press (GimpDashboard *dashboard, + GdkEventButton *bevent, + GtkWidget *widget); + +static void gimp_dashboard_group_action_toggled (GimpDashboard *dashboard, + GimpToggleAction *action); +static void gimp_dashboard_field_menu_item_toggled (GimpDashboard *dashboard, + GtkCheckMenuItem *item); + +static gpointer gimp_dashboard_sample (GimpDashboard *dashboard); + +static gboolean gimp_dashboard_update (GimpDashboard *dashboard); +static gboolean gimp_dashboard_low_swap_space (GimpDashboard *dashboard); + +static void gimp_dashboard_sample_function (GimpDashboard *dashboard, + Variable variable); +static void gimp_dashboard_sample_gegl_config (GimpDashboard *dashboard, + Variable variable); +static void gimp_dashboard_sample_gegl_stats (GimpDashboard *dashboard, + Variable variable); +static void gimp_dashboard_sample_variable_changed (GimpDashboard *dashboard, + Variable variable); +static void gimp_dashboard_sample_variable_rate_of_change (GimpDashboard *dashboard, + Variable variable); +static void gimp_dashboard_sample_swap_limit (GimpDashboard *dashboard, + Variable variable); +#ifdef HAVE_CPU_GROUP +static void gimp_dashboard_sample_cpu_usage (GimpDashboard *dashboard, + Variable variable); +static void gimp_dashboard_sample_cpu_active (GimpDashboard *dashboard, + Variable variable); +static void gimp_dashboard_sample_cpu_active_time (GimpDashboard *dashboard, + Variable variable); +#endif /* HAVE_CPU_GROUP */ + +#ifdef HAVE_MEMORY_GROUP +static void gimp_dashboard_sample_memory_used (GimpDashboard *dashboard, + Variable variable); +static void gimp_dashboard_sample_memory_available (GimpDashboard *dashboard, + Variable variable); +static void gimp_dashboard_sample_memory_size (GimpDashboard *dashboard, + Variable variable); +#endif /* HAVE_MEMORY_GROUP */ + +static void gimp_dashboard_sample_object (GimpDashboard *dashboard, + GObject *object, + Variable variable); + +static void gimp_dashboard_group_menu_position (GtkMenu *menu, + gint *x, + gint *y, + gboolean *push_in, + gpointer user_data); + +static void gimp_dashboard_update_groups (GimpDashboard *dashboard); +static void gimp_dashboard_update_group (GimpDashboard *dashboard, + Group group); +static void gimp_dashboard_update_group_values (GimpDashboard *dashboard, + Group group); + +static void gimp_dashboard_group_set_active (GimpDashboard *dashboard, + Group group, + gboolean active); +static void gimp_dashboard_field_set_active (GimpDashboard *dashboard, + Group group, + gint field, + gboolean active); + +static void gimp_dashboard_reset_unlocked (GimpDashboard *dashboard); + +static void gimp_dashboard_reset_variables (GimpDashboard *dashboard); + +static gpointer gimp_dashboard_variable_get_data (GimpDashboard *dashboard, + Variable variable, + gsize size); + +static gboolean gimp_dashboard_variable_to_boolean (GimpDashboard *dashboard, + Variable variable); +static gdouble gimp_dashboard_variable_to_double (GimpDashboard *dashboard, + Variable variable); + +static gchar * gimp_dashboard_field_to_string (GimpDashboard *dashboard, + Group group, + gint field, + gboolean full); + +static gboolean gimp_dashboard_log_printf (GimpDashboard *dashboard, + const gchar *format, + ...) G_GNUC_PRINTF (2, 3); +static gboolean gimp_dashboard_log_print_escaped (GimpDashboard *dashboard, + const gchar *string); +static gint64 gimp_dashboard_log_time (GimpDashboard *dashboard); +static void gimp_dashboard_log_sample (GimpDashboard *dashboard, + gboolean variables_changed, + gboolean include_current_thread); +static void gimp_dashboard_log_add_marker_unlocked (GimpDashboard *dashboard, + const gchar *description); +static void gimp_dashboard_log_update_highlight (GimpDashboard *dashboard); +static void gimp_dashboard_log_update_n_markers (GimpDashboard *dashboard); + +static void gimp_dashboard_log_write_address_map (GimpDashboard *dashboard, + guintptr *addresses, + gint n_addresses, + GimpAsync *async); +static void gimp_dashboard_log_write_global_address_map (GimpAsync *async, + GimpDashboard *dashboard); +static void gimp_dashboard_log_log_func (const gchar *log_domain, + GLogLevelFlags log_levels, + const gchar *message, + GimpDashboard *dashboard); + +static gboolean gimp_dashboard_field_use_meter_underlay (Group group, + gint field); + +static gchar * gimp_dashboard_format_rate_of_change (const gchar *value); +static gchar * gimp_dashboard_format_value (VariableType type, + gdouble value); + +static void gimp_dashboard_label_set_text (GtkLabel *label, + const gchar *text); + + +/* static variables */ + +static const VariableInfo variables[] = +{ + /* cache variables */ + + [VARIABLE_CACHE_OCCUPIED] = + { .name = "cache-occupied", + .title = NC_("dashboard-variable", "Occupied"), + .description = N_("Tile cache occupied size"), + .type = VARIABLE_TYPE_SIZE, + .color = {0.3, 0.6, 0.3, 1.0}, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "tile-cache-total" + }, + + [VARIABLE_CACHE_MAXIMUM] = + { .name = "cache-maximum", + .title = NC_("dashboard-variable", "Maximum"), + .description = N_("Maximal tile cache occupied size"), + .type = VARIABLE_TYPE_SIZE, + .color = {0.3, 0.7, 0.8, 1.0}, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "tile-cache-total-max" + }, + + [VARIABLE_CACHE_LIMIT] = + { .name = "cache-limit", + .title = NC_("dashboard-variable", "Limit"), + .description = N_("Tile cache size limit"), + .type = VARIABLE_TYPE_SIZE, + .sample_func = gimp_dashboard_sample_gegl_config, + .data = "tile-cache-size" + }, + + [VARIABLE_CACHE_COMPRESSION] = + { .name = "cache-compression", + .title = NC_("dashboard-variable", "Compression"), + .description = N_("Tile cache compression ratio"), + .type = VARIABLE_TYPE_SIZE_RATIO, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "tile-cache-total\0" + "tile-cache-total-uncompressed" + }, + + [VARIABLE_CACHE_HIT_MISS] = + { .name = "cache-hit-miss", + .title = NC_("dashboard-variable", "Hit/Miss"), + .description = N_("Tile cache hit/miss ratio"), + .type = VARIABLE_TYPE_INT_RATIO, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "tile-cache-hits\0" + "tile-cache-misses" + }, + + + /* swap variables */ + + [VARIABLE_SWAP_OCCUPIED] = + { .name = "swap-occupied", + .title = NC_("dashboard-variable", "Occupied"), + .description = N_("Swap file occupied size"), + .type = VARIABLE_TYPE_SIZE, + .color = {0.8, 0.2, 0.2, 1.0}, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "swap-total" + }, + + [VARIABLE_SWAP_SIZE] = + { .name = "swap-size", + .title = NC_("dashboard-variable", "Size"), + .description = N_("Swap file size"), + .type = VARIABLE_TYPE_SIZE, + .color = {0.8, 0.6, 0.4, 1.0}, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "swap-file-size" + }, + + [VARIABLE_SWAP_LIMIT] = + { .name = "swap-limit", + .title = NC_("dashboard-variable", "Limit"), + .description = N_("Swap file size limit"), + .type = VARIABLE_TYPE_SIZE, + .sample_func = gimp_dashboard_sample_swap_limit, + }, + + [VARIABLE_SWAP_QUEUED] = + { .name = "swap-queued", + .title = NC_("dashboard-variable", "Queued"), + .description = N_("Size of data queued for writing to the swap"), + .type = VARIABLE_TYPE_SIZE, + .color = {0.8, 0.8, 0.2, 0.5}, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "swap-queued-total" + }, + + [VARIABLE_SWAP_QUEUE_STALLS] = + { .name = "swap-queue-stalls", + .title = NC_("dashboard-variable", "Queue stalls"), + .description = N_("Number of times the writing to the swap has been " + "stalled, due to a full queue"), + .type = VARIABLE_TYPE_INTEGER, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "swap-queue-stalls" + }, + + [VARIABLE_SWAP_QUEUE_FULL] = + { .name = "swap-queue-full", + .title = NC_("dashboard-variable", "Queue full"), + .description = N_("Whether the swap queue is full"), + .type = VARIABLE_TYPE_BOOLEAN, + .sample_func = gimp_dashboard_sample_variable_changed, + .data = GINT_TO_POINTER (VARIABLE_SWAP_QUEUE_STALLS) + }, + + [VARIABLE_SWAP_READ] = + { .name = "swap-read", + /* Translators: this is the past participle form of "read", + * as in "total amount of data read from the swap". + */ + .title = NC_("dashboard-variable", "Read"), + .description = N_("Total amount of data read from the swap"), + .type = VARIABLE_TYPE_SIZE, + .color = {0.2, 0.4, 1.0, 0.4}, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "swap-read-total" + }, + + [VARIABLE_SWAP_READ_THROUGHPUT] = + { .name = "swap-read-throughput", + .title = NC_("dashboard-variable", "Read throughput"), + .description = N_("The rate at which data is read from the swap"), + .type = VARIABLE_TYPE_RATE_OF_CHANGE, + .color = {0.2, 0.4, 1.0, 1.0}, + .sample_func = gimp_dashboard_sample_variable_rate_of_change, + .data = GINT_TO_POINTER (VARIABLE_SWAP_READ) + }, + + [VARIABLE_SWAP_WRITTEN] = + { .name = "swap-written", + /* Translators: this is the past participle form of "write", + * as in "total amount of data written to the swap". + */ + .title = NC_("dashboard-variable", "Written"), + .description = N_("Total amount of data written to the swap"), + .type = VARIABLE_TYPE_SIZE, + .color = {0.8, 0.3, 0.2, 0.4}, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "swap-write-total" + }, + + [VARIABLE_SWAP_WRITE_THROUGHPUT] = + { .name = "swap-write-throughput", + .title = NC_("dashboard-variable", "Write throughput"), + .description = N_("The rate at which data is written to the swap"), + .type = VARIABLE_TYPE_RATE_OF_CHANGE, + .color = {0.8, 0.3, 0.2, 1.0}, + .sample_func = gimp_dashboard_sample_variable_rate_of_change, + .data = GINT_TO_POINTER (VARIABLE_SWAP_WRITTEN) + }, + + [VARIABLE_SWAP_COMPRESSION] = + { .name = "swap-compression", + .title = NC_("dashboard-variable", "Compression"), + .description = N_("Swap compression ratio"), + .type = VARIABLE_TYPE_SIZE_RATIO, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "swap-total\0" + "swap-total-uncompressed" + }, + + +#ifdef HAVE_CPU_GROUP + /* cpu variables */ + + [VARIABLE_CPU_USAGE] = + { .name = "cpu-usage", + .title = NC_("dashboard-variable", "Usage"), + .description = N_("Total CPU usage"), + .type = VARIABLE_TYPE_PERCENTAGE, + .color = {0.8, 0.7, 0.2, 1.0}, + .sample_func = gimp_dashboard_sample_cpu_usage + }, + + [VARIABLE_CPU_ACTIVE] = + { .name = "cpu-active", + .title = NC_("dashboard-variable", "Active"), + .description = N_("Whether the CPU is active"), + .type = VARIABLE_TYPE_BOOLEAN, + .color = {0.9, 0.8, 0.3, 1.0}, + .sample_func = gimp_dashboard_sample_cpu_active + }, + + [VARIABLE_CPU_ACTIVE_TIME] = + { .name = "cpu-active-time", + .title = NC_("dashboard-variable", "Active"), + .description = N_("Total amount of time the CPU has been active"), + .type = VARIABLE_TYPE_DURATION, + .color = {0.8, 0.7, 0.2, 0.4}, + .sample_func = gimp_dashboard_sample_cpu_active_time + }, +#endif /* HAVE_CPU_GROUP */ + + +#ifdef HAVE_MEMORY_GROUP + /* memory variables */ + + [VARIABLE_MEMORY_USED] = + { .name = "memory-used", + .title = NC_("dashboard-variable", "Used"), + .description = N_("Amount of memory used by the process"), + .type = VARIABLE_TYPE_SIZE, + .color = {0.8, 0.5, 0.2, 1.0}, + .sample_func = gimp_dashboard_sample_memory_used + }, + + [VARIABLE_MEMORY_AVAILABLE] = + { .name = "memory-available", + .title = NC_("dashboard-variable", "Available"), + .description = N_("Amount of available physical memory"), + .type = VARIABLE_TYPE_SIZE, + .color = {0.8, 0.5, 0.2, 0.4}, + .sample_func = gimp_dashboard_sample_memory_available + }, + + [VARIABLE_MEMORY_SIZE] = + { .name = "memory-size", + .title = NC_("dashboard-variable", "Size"), + .description = N_("Physical memory size"), + .type = VARIABLE_TYPE_SIZE, + .sample_func = gimp_dashboard_sample_memory_size + }, +#endif /* HAVE_MEMORY_GROUP */ + + + /* misc variables */ + + [VARIABLE_MIPMAPED] = + { .name = "mipmapped", + .title = NC_("dashboard-variable", "Mipmapped"), + .description = N_("Total size of processed mipmapped data"), + .type = VARIABLE_TYPE_SIZE, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "zoom-total" + }, + + [VARIABLE_ASSIGNED_THREADS] = + { .name = "assigned-threads", + .title = NC_("dashboard-variable", "Assigned"), + .description = N_("Number of assigned worker threads"), + .type = VARIABLE_TYPE_INTEGER, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "assigned-threads" + }, + + [VARIABLE_ACTIVE_THREADS] = + { .name = "active-threads", + .title = NC_("dashboard-variable", "Active"), + .description = N_("Number of active worker threads"), + .type = VARIABLE_TYPE_INTEGER, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "active-threads" + }, + + [VARIABLE_ASYNC_RUNNING] = + { .name = "async-running", + .title = NC_("dashboard-variable", "Async"), + .description = N_("Number of ongoing asynchronous operations"), + .type = VARIABLE_TYPE_INTEGER, + .sample_func = gimp_dashboard_sample_function, + .data = gimp_async_get_n_running + }, + + [VARIABLE_TILE_ALLOC_TOTAL] = + { .name = "tile-alloc-total", + .title = NC_("dashboard-variable", "Tile"), + .description = N_("Total size of tile memory"), + .type = VARIABLE_TYPE_SIZE, + .color = {0.3, 0.3, 1.0, 1.0}, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "tile-alloc-total" + }, + + [VARIABLE_SCRATCH_TOTAL] = + { .name = "scratch-total", + .title = NC_("dashboard-variable", "Scratch"), + .description = N_("Total size of scratch memory"), + .type = VARIABLE_TYPE_SIZE, + .sample_func = gimp_dashboard_sample_gegl_stats, + .data = "scratch-total" + }, + + [VARIABLE_TEMP_BUF_TOTAL] = + { .name = "temp-buf-total", + /* Translators: "TempBuf" is a technical term referring to an internal + * GIMP data structure. It's probably OK to leave it untranslated. + */ + .title = NC_("dashboard-variable", "TempBuf"), + .description = N_("Total size of temporary buffers"), + .type = VARIABLE_TYPE_SIZE, + .sample_func = gimp_dashboard_sample_function, + .data = gimp_temp_buf_get_total_memsize + } +}; + +static const GroupInfo groups[] = +{ + /* cache group */ + [GROUP_CACHE] = + { .name = "cache", + .title = NC_("dashboard-group", "Cache"), + .description = N_("In-memory tile cache"), + .default_active = TRUE, + .default_expanded = TRUE, + .has_meter = TRUE, + .meter_limit = VARIABLE_CACHE_LIMIT, + .fields = (const FieldInfo[]) + { + { .variable = VARIABLE_CACHE_OCCUPIED, + .default_active = TRUE, + .show_in_header = TRUE, + .meter_value = 2 + }, + { .variable = VARIABLE_CACHE_MAXIMUM, + .default_active = FALSE, + .meter_value = 1 + }, + { .variable = VARIABLE_CACHE_LIMIT, + .default_active = TRUE + }, + + { VARIABLE_SEPARATOR }, + + { .variable = VARIABLE_CACHE_COMPRESSION, + .default_active = FALSE + }, + { .variable = VARIABLE_CACHE_HIT_MISS, + .default_active = FALSE + }, + + {} + } + }, + + /* swap group */ + [GROUP_SWAP] = + { .name = "swap", + .title = NC_("dashboard-group", "Swap"), + .description = N_("On-disk tile swap"), + .default_active = TRUE, + .default_expanded = TRUE, + .has_meter = TRUE, + .meter_limit = VARIABLE_SWAP_LIMIT, + .meter_led = (const Variable[]) + { + VARIABLE_SWAP_QUEUE_FULL, + VARIABLE_SWAP_READ_THROUGHPUT, + VARIABLE_SWAP_WRITE_THROUGHPUT, + + VARIABLE_NONE + }, + .fields = (const FieldInfo[]) + { + { .variable = VARIABLE_SWAP_OCCUPIED, + .default_active = TRUE, + .show_in_header = TRUE, + .meter_value = 5 + }, + { .variable = VARIABLE_SWAP_SIZE, + .default_active = TRUE, + .meter_value = 4 + }, + { .variable = VARIABLE_SWAP_LIMIT, + .default_active = TRUE + }, + + { VARIABLE_SEPARATOR }, + + { .variable = VARIABLE_SWAP_QUEUED, + .default_active = FALSE, + .meter_variable = VARIABLE_SWAP_QUEUE_FULL, + .meter_value = 3 + }, + + { VARIABLE_SEPARATOR }, + + { .variable = VARIABLE_SWAP_READ, + .default_active = FALSE, + .meter_variable = VARIABLE_SWAP_READ_THROUGHPUT, + .meter_value = 2 + }, + + { .variable = VARIABLE_SWAP_WRITTEN, + .default_active = FALSE, + .meter_variable = VARIABLE_SWAP_WRITE_THROUGHPUT, + .meter_value = 1 + }, + + { VARIABLE_SEPARATOR }, + + { .variable = VARIABLE_SWAP_COMPRESSION, + .default_active = FALSE + }, + + {} + } + }, + +#ifdef HAVE_CPU_GROUP + /* cpu group */ + [GROUP_CPU] = + { .name = "cpu", + .title = NC_("dashboard-group", "CPU"), + .description = N_("CPU usage"), + .default_active = TRUE, + .default_expanded = FALSE, + .has_meter = TRUE, + .meter_led = (const Variable[]) + { + VARIABLE_CPU_ACTIVE, + + VARIABLE_NONE + }, + .fields = (const FieldInfo[]) + { + { .variable = VARIABLE_CPU_USAGE, + .default_active = TRUE, + .show_in_header = TRUE, + .meter_value = 2 + }, + + { VARIABLE_SEPARATOR }, + + { .variable = VARIABLE_CPU_ACTIVE_TIME, + .default_active = FALSE, + .meter_variable = VARIABLE_CPU_ACTIVE, + .meter_value = 1 + }, + + {} + } + }, +#endif /* HAVE_CPU_GROUP */ + +#ifdef HAVE_MEMORY_GROUP + /* memory group */ + [GROUP_MEMORY] = + { .name = "memory", + .title = NC_("dashboard-group", "Memory"), + .description = N_("Memory usage"), + .default_active = TRUE, + .default_expanded = FALSE, + .has_meter = TRUE, + .meter_limit = VARIABLE_MEMORY_SIZE, + .fields = (const FieldInfo[]) + { + { .variable = VARIABLE_CACHE_OCCUPIED, + .title = NC_("dashboard-variable", "Cache"), + .default_active = FALSE, + .meter_value = 4 + }, + { .variable = VARIABLE_TILE_ALLOC_TOTAL, + .default_active = FALSE, + .meter_value = 3 + }, + + { VARIABLE_SEPARATOR }, + + { .variable = VARIABLE_MEMORY_USED, + .default_active = TRUE, + .show_in_header = TRUE, + .meter_value = 2, + .meter_cumulative = TRUE + }, + { .variable = VARIABLE_MEMORY_AVAILABLE, + .default_active = TRUE, + .meter_value = 1, + .meter_cumulative = TRUE + }, + { .variable = VARIABLE_MEMORY_SIZE, + .default_active = TRUE + }, + + {} + } + }, +#endif /* HAVE_MEMORY_GROUP */ + + /* misc group */ + [GROUP_MISC] = + { .name = "misc", + .title = NC_("dashboard-group", "Misc"), + .description = N_("Miscellaneous information"), + .default_active = FALSE, + .default_expanded = FALSE, + .has_meter = FALSE, + .fields = (const FieldInfo[]) + { + { .variable = VARIABLE_MIPMAPED, + .default_active = TRUE + }, + { .variable = VARIABLE_ASSIGNED_THREADS, + .default_active = TRUE + }, + { .variable = VARIABLE_ACTIVE_THREADS, + .default_active = TRUE + }, + { .variable = VARIABLE_ASYNC_RUNNING, + .default_active = TRUE + }, + { .variable = VARIABLE_TILE_ALLOC_TOTAL, + .default_active = TRUE + }, + { .variable = VARIABLE_SCRATCH_TOTAL, + .default_active = TRUE + }, + { .variable = VARIABLE_TEMP_BUF_TOTAL, + .default_active = TRUE + }, + + {} + } + }, +}; + + +G_DEFINE_TYPE_WITH_CODE (GimpDashboard, gimp_dashboard, GIMP_TYPE_EDITOR, + G_ADD_PRIVATE (GimpDashboard) + G_IMPLEMENT_INTERFACE (GIMP_TYPE_DOCKED, + gimp_dashboard_docked_iface_init)) + +#define parent_class gimp_dashboard_parent_class + +static GimpDockedInterface *parent_docked_iface = NULL; + + +/* private functions */ + + +static void +gimp_dashboard_class_init (GimpDashboardClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = gimp_dashboard_constructed; + object_class->dispose = gimp_dashboard_dispose; + object_class->finalize = gimp_dashboard_finalize; + + widget_class->map = gimp_dashboard_map; + widget_class->unmap = gimp_dashboard_unmap; +} + +static void +gimp_dashboard_init (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv; + GtkWidget *box; + GtkWidget *scrolled_window; + GtkWidget *viewport; + GtkWidget *vbox; + GtkWidget *expander; + GtkWidget *hbox; + GtkWidget *button; + GtkWidget *image; + GtkWidget *menu; + GtkWidget *item; + GtkWidget *frame; + GtkWidget *vbox2; + GtkWidget *meter; + GtkWidget *table; + GtkWidget *label; + gint content_spacing; + Group group; + gint field; + + priv = dashboard->priv = gimp_dashboard_get_instance_private (dashboard); + + g_mutex_init (&priv->mutex); + g_cond_init (&priv->cond); + + priv->update_interval = DEFAULT_UPDATE_INTERVAL; + priv->history_duration = DEFAULT_HISTORY_DURATION; + priv->low_swap_space_warning = DEFAULT_LOW_SWAP_SPACE_WARNING; + + gtk_widget_style_get (GTK_WIDGET (dashboard), + "content-spacing", &content_spacing, + NULL); + + /* we put the dashboard inside an event box, so that it gets its own window, + * which reduces the overhead of updating the ui, since it gets updated + * frequently. unfortunately, this means that the dashboard's background + * color may be a bit off for some themes. + */ + box = gtk_event_box_new (); + gtk_box_pack_start (GTK_BOX (dashboard), box, TRUE, TRUE, 0); + gtk_widget_show (box); + + /* scrolled window */ + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + gtk_container_add (GTK_CONTAINER (box), scrolled_window); + gtk_widget_show (scrolled_window); + + /* viewport */ + viewport = gtk_viewport_new ( + gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (scrolled_window)), + gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (scrolled_window))); + gtk_viewport_set_shadow_type (GTK_VIEWPORT (viewport), GTK_SHADOW_NONE); + gtk_container_add (GTK_CONTAINER (scrolled_window), viewport); + gtk_widget_show (viewport); + + /* main vbox */ + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2 * content_spacing); + gtk_container_add (GTK_CONTAINER (viewport), vbox); + gtk_widget_show (vbox); + + /* construct the groups */ + for (group = FIRST_GROUP; group < N_GROUPS; group++) + { + const GroupInfo *group_info = &groups[group]; + GroupData *group_data = &priv->groups[group]; + + group_data->n_fields = 0; + group_data->n_meter_values = 0; + + for (field = 0; group_info->fields[field].variable; field++) + { + const FieldInfo *field_info = &group_info->fields[field]; + + group_data->n_fields++; + group_data->n_meter_values = MAX (group_data->n_meter_values, + field_info->meter_value); + } + + group_data->fields = g_new0 (FieldData, group_data->n_fields); + + /* group expander */ + expander = gtk_expander_new (NULL); + group_data->expander = GTK_EXPANDER (expander); + gtk_expander_set_expanded (GTK_EXPANDER (expander), + group_info->default_expanded); + gtk_expander_set_label_fill (GTK_EXPANDER (expander), TRUE); + gtk_box_pack_start (GTK_BOX (vbox), expander, FALSE, FALSE, 0); + + g_object_set_data (G_OBJECT (expander), + "gimp-dashboard-group", GINT_TO_POINTER (group)); + g_signal_connect_swapped (expander, "button-press-event", + G_CALLBACK (gimp_dashboard_group_expander_button_press), + dashboard); + + /* group expander label box */ + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gimp_help_set_help_data (hbox, + g_dgettext (NULL, group_info->description), + NULL); + gtk_expander_set_label_widget (GTK_EXPANDER (expander), hbox); + gtk_widget_show (hbox); + + /* group expander label */ + label = gtk_label_new (g_dpgettext2 (NULL, "dashboard-group", + group_info->title)); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gimp_label_set_attributes (GTK_LABEL (label), + PANGO_ATTR_WEIGHT, PANGO_WEIGHT_BOLD, + -1); + gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0); + gtk_widget_show (label); + + /* group expander values label */ + label = gtk_label_new (NULL); + group_data->header_values_label = GTK_LABEL (label); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 4); + + g_object_bind_property (expander, "expanded", + label, "visible", + G_BINDING_SYNC_CREATE | + G_BINDING_INVERT_BOOLEAN); + + /* group expander menu button */ + button = gtk_button_new (); + group_data->menu_button = GTK_BUTTON (button); + gimp_help_set_help_data (button, _("Select fields"), + NULL); + gtk_widget_set_can_focus (button, FALSE); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + gtk_box_pack_end (GTK_BOX (hbox), button, FALSE, FALSE, 0); + gtk_widget_show (button); + + image = gtk_image_new_from_icon_name (GIMP_ICON_MENU_LEFT, + GTK_ICON_SIZE_MENU); + gtk_image_set_pixel_size (GTK_IMAGE (image), 12); + gtk_image_set_from_icon_name (GTK_IMAGE (image), GIMP_ICON_MENU_LEFT, + GTK_ICON_SIZE_MENU); + gtk_container_add (GTK_CONTAINER (button), image); + gtk_widget_show (image); + + /* group menu */ + menu = gtk_menu_new (); + group_data->menu = GTK_MENU (menu); + gtk_menu_attach_to_widget (GTK_MENU (menu), button, NULL); + + for (field = 0; field < group_data->n_fields; field++) + { + const FieldInfo *field_info = &group_info->fields[field]; + FieldData *field_data = &group_data->fields[field]; + + if (field_info->variable != VARIABLE_SEPARATOR) + { + const VariableInfo *variable_info = &variables[field_info->variable]; + + item = gtk_check_menu_item_new_with_label ( + g_dpgettext2 (NULL, "dashboard-variable", + field_info->title ? field_info->title : + variable_info->title)); + field_data->menu_item = GTK_CHECK_MENU_ITEM (item); + gimp_help_set_help_data (item, + g_dgettext (NULL, variable_info->description), + NULL); + + g_object_set_data (G_OBJECT (item), + "gimp-dashboard-group", GINT_TO_POINTER (group)); + g_object_set_data (G_OBJECT (item), + "gimp-dashboard-field", GINT_TO_POINTER (field)); + g_signal_connect_swapped (item, "toggled", + G_CALLBACK (gimp_dashboard_field_menu_item_toggled), + dashboard); + + gimp_dashboard_field_set_active (dashboard, group, field, + field_info->default_active); + } + else + { + item = gtk_separator_menu_item_new (); + } + + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + gtk_widget_show (item); + } + + /* group frame */ + frame = gimp_frame_new (NULL); + gtk_container_add (GTK_CONTAINER (expander), frame); + gtk_widget_show (frame); + + /* group vbox */ + vbox2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2 * content_spacing); + gtk_container_add (GTK_CONTAINER (frame), vbox2); + gtk_widget_show (vbox2); + + /* group meter */ + if (group_info->has_meter) + { + meter = gimp_meter_new (group_data->n_meter_values); + group_data->meter = GIMP_METER (meter); + gimp_help_set_help_data (meter, + g_dgettext (NULL, group_info->description), + NULL); + gimp_meter_set_history_resolution (GIMP_METER (meter), + priv->update_interval / 1000.0); + gimp_meter_set_history_duration (GIMP_METER (meter), + priv->history_duration / 1000.0); + gtk_box_pack_start (GTK_BOX (vbox2), meter, FALSE, FALSE, 0); + gtk_widget_show (meter); + + for (field = 0; field < group_data->n_fields; field++) + { + const FieldInfo *field_info = &group_info->fields[field]; + + if (field_info->meter_value) + { + const VariableInfo *variable_info = &variables[field_info->variable]; + + gimp_meter_set_value_color (GIMP_METER (meter), + field_info->meter_value - 1, + &variable_info->color); + + if (gimp_dashboard_field_use_meter_underlay (group, field)) + { + gimp_meter_set_value_show_in_gauge (GIMP_METER (meter), + field_info->meter_value - 1, + FALSE); + gimp_meter_set_value_interpolation (GIMP_METER (meter), + field_info->meter_value - 1, + GIMP_INTERPOLATION_NONE); + } + } + } + } + + /* group table */ + table = gtk_table_new (1, 1, FALSE); + group_data->table = GTK_TABLE (table); + gtk_table_set_row_spacings (GTK_TABLE (table), content_spacing); + gtk_table_set_col_spacings (GTK_TABLE (table), 4); + gtk_box_pack_start (GTK_BOX (vbox2), table, FALSE, FALSE, 0); + gtk_widget_show (table); + + gimp_dashboard_group_set_active (dashboard, group, + group_info->default_active); + gimp_dashboard_update_group (dashboard, group); + } + + /* sampler thread + * + * we use a separate thread for sampling, so that data is sampled even when + * the main thread is busy + */ + priv->thread = g_thread_new ("dashboard", + (GThreadFunc) gimp_dashboard_sample, + dashboard); +} + +static void +gimp_dashboard_docked_iface_init (GimpDockedInterface *iface) +{ + parent_docked_iface = g_type_interface_peek_parent (iface); + + if (! parent_docked_iface) + parent_docked_iface = g_type_default_interface_peek (GIMP_TYPE_DOCKED); + + iface->set_aux_info = gimp_dashboard_set_aux_info; + iface->get_aux_info = gimp_dashboard_get_aux_info; +} + +static void +gimp_dashboard_constructed (GObject *object) +{ + GimpDashboard *dashboard = GIMP_DASHBOARD (object); + GimpDashboardPrivate *priv = dashboard->priv; + GimpUIManager *ui_manager; + GimpActionGroup *action_group; + GimpAction *action; + GtkWidget *button; + GtkWidget *alignment; + GtkWidget *box; + GtkWidget *image; + GtkWidget *label; + Group group; + + G_OBJECT_CLASS (parent_class)->constructed (object); + + ui_manager = gimp_editor_get_ui_manager (GIMP_EDITOR (dashboard)); + action_group = gimp_ui_manager_get_action_group (ui_manager, "dashboard"); + + /* group actions */ + for (group = FIRST_GROUP; group < N_GROUPS; group++) + { + const GroupInfo *group_info = &groups[group]; + GroupData *group_data = &priv->groups[group]; + GimpToggleActionEntry entry = {}; + + entry.name = g_strdup_printf ("dashboard-group-%s", group_info->name); + entry.label = g_dpgettext2 (NULL, "dashboard-group", group_info->title); + entry.tooltip = g_dgettext (NULL, group_info->description); + entry.help_id = GIMP_HELP_DASHBOARD_GROUPS; + entry.is_active = group_data->active; + + gimp_action_group_add_toggle_actions (action_group, "dashboard-groups", + &entry, 1); + + action = gimp_ui_manager_find_action (ui_manager, "dashboard", entry.name); + group_data->action = GIMP_TOGGLE_ACTION (action); + + g_object_set_data (G_OBJECT (action), + "gimp-dashboard-group", GINT_TO_POINTER (group)); + g_signal_connect_swapped (action, "toggled", + G_CALLBACK (gimp_dashboard_group_action_toggled), + dashboard); + + g_free ((gpointer) entry.name); + } + + button = gimp_editor_add_action_button (GIMP_EDITOR (dashboard), "dashboard", + "dashboard-log-record", NULL); + priv->log_record_button = GIMP_HIGHLIGHTABLE_BUTTON (button); + gimp_highlightable_button_set_highlight_color ( + GIMP_HIGHLIGHTABLE_BUTTON (button), + GIMP_HIGHLIGHTABLE_BUTTON_COLOR_AFFIRMATIVE); + + button = gimp_editor_add_action_button (GIMP_EDITOR (dashboard), "dashboard", + "dashboard-log-add-marker", + "dashboard-log-add-empty-marker", + gimp_get_extend_selection_mask (), + NULL); + + action = gimp_action_group_get_action (action_group, + "dashboard-log-add-marker"); + g_object_bind_property (action, "sensitive", + button, "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + image = g_object_ref (gtk_bin_get_child (GTK_BIN (button))); + gtk_container_remove (GTK_CONTAINER (button), image); + + alignment = gtk_alignment_new (0.5, 0.5, 0.0, 0.0); + gtk_container_add (GTK_CONTAINER (button), alignment); + gtk_widget_show (alignment); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4); + gtk_container_add (GTK_CONTAINER (alignment), box); + gtk_widget_show (box); + + gtk_box_pack_start (GTK_BOX (box), image, FALSE, FALSE, 0); + g_object_unref (image); + + label = gtk_label_new (NULL); + priv->log_add_marker_label = GTK_LABEL (label); + gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0); + gtk_widget_show (label); + + button = gimp_editor_add_action_button (GIMP_EDITOR (dashboard), "dashboard", + "dashboard-reset", NULL); + + action = gimp_action_group_get_action (action_group, + "dashboard-reset"); + g_object_bind_property (action, "sensitive", + button, "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + gimp_action_group_update (action_group, dashboard); +} + +static void +gimp_dashboard_dispose (GObject *object) +{ + GimpDashboard *dashboard = GIMP_DASHBOARD (object); + GimpDashboardPrivate *priv = dashboard->priv; + + if (priv->thread) + { + g_mutex_lock (&priv->mutex); + + priv->quit = TRUE; + g_cond_signal (&priv->cond); + + g_mutex_unlock (&priv->mutex); + + g_clear_pointer (&priv->thread, g_thread_join); + } + + if (priv->update_idle_id) + { + g_source_remove (priv->update_idle_id); + priv->update_idle_id = 0; + } + + if (priv->low_swap_space_idle_id) + { + g_source_remove (priv->low_swap_space_idle_id); + priv->low_swap_space_idle_id = 0; + } + + gimp_dashboard_log_stop_recording (dashboard, NULL); + + gimp_dashboard_reset_variables (dashboard); + + G_OBJECT_CLASS (parent_class)->dispose (object); +} + +static void +gimp_dashboard_finalize (GObject *object) +{ + GimpDashboard *dashboard = GIMP_DASHBOARD (object); + GimpDashboardPrivate *priv = dashboard->priv; + gint i; + + for (i = FIRST_GROUP; i < N_GROUPS; i++) + g_free (priv->groups[i].fields); + + g_mutex_clear (&priv->mutex); + g_cond_clear (&priv->cond); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +gimp_dashboard_map (GtkWidget *widget) +{ + GimpDashboard *dashboard = GIMP_DASHBOARD (widget); + + GTK_WIDGET_CLASS (parent_class)->map (widget); + + gimp_dashboard_update (dashboard); +} + +static void +gimp_dashboard_unmap (GtkWidget *widget) +{ + GimpDashboard *dashboard = GIMP_DASHBOARD (widget); + GimpDashboardPrivate *priv = dashboard->priv; + + g_mutex_lock (&priv->mutex); + + if (priv->update_idle_id) + { + g_source_remove (priv->update_idle_id); + priv->update_idle_id = 0; + } + + g_mutex_unlock (&priv->mutex); + + GTK_WIDGET_CLASS (parent_class)->unmap (widget); +} + +#define AUX_INFO_UPDATE_INTERVAL "update-interval" +#define AUX_INFO_HISTORY_DURATION "history-duration" +#define AUX_INFO_LOW_SWAP_SPACE_WARNING "low-swap-space-warning" + +static void +gimp_dashboard_set_aux_info (GimpDocked *docked, + GList *aux_info) +{ + GimpDashboard *dashboard = GIMP_DASHBOARD (docked); + GimpDashboardPrivate *priv = dashboard->priv; + gchar *name; + GList *list; + + parent_docked_iface->set_aux_info (docked, aux_info); + + for (list = aux_info; list; list = g_list_next (list)) + { + GimpSessionInfoAux *aux = list->data; + + if (! strcmp (aux->name, AUX_INFO_UPDATE_INTERVAL)) + { + gint value = atoi (aux->value); + GimpDashboardUpdateInteval update_interval; + + for (update_interval = GIMP_DASHBOARD_UPDATE_INTERVAL_0_25_SEC; + update_interval < value && + update_interval < GIMP_DASHBOARD_UPDATE_INTERVAL_4_SEC; + update_interval *= 2); + + gimp_dashboard_set_update_interval (dashboard, update_interval); + } + else if (! strcmp (aux->name, AUX_INFO_HISTORY_DURATION)) + { + gint value = atoi (aux->value); + GimpDashboardHistoryDuration history_duration; + + for (history_duration = GIMP_DASHBOARD_HISTORY_DURATION_15_SEC; + history_duration < value && + history_duration < GIMP_DASHBOARD_HISTORY_DURATION_240_SEC; + history_duration *= 2); + + gimp_dashboard_set_history_duration (dashboard, history_duration); + } + else if (! strcmp (aux->name, AUX_INFO_LOW_SWAP_SPACE_WARNING)) + { + gimp_dashboard_set_low_swap_space_warning (dashboard, + ! strcmp (aux->value, "yes")); + } + else + { + Group group; + gint field; + + for (group = FIRST_GROUP; group < N_GROUPS; group++) + { + const GroupInfo *group_info = &groups[group]; + GroupData *group_data = &priv->groups[group]; + + name = g_strdup_printf ("%s-active", group_info->name); + + if (! strcmp (aux->name, name)) + { + gboolean active = ! strcmp (aux->value, "yes"); + + gimp_dashboard_group_set_active (dashboard, group, active); + + g_free (name); + goto next_aux_info; + } + + g_free (name); + + name = g_strdup_printf ("%s-expanded", group_info->name); + + if (! strcmp (aux->name, name)) + { + gboolean expanded = ! strcmp (aux->value, "yes"); + + gtk_expander_set_expanded (group_data->expander, expanded); + + g_free (name); + goto next_aux_info; + } + + g_free (name); + + for (field = 0; field < group_data->n_fields; field++) + { + const FieldInfo *field_info = &group_info->fields[field]; + + if (field_info->variable != VARIABLE_SEPARATOR) + { + const VariableInfo *variable_info = &variables[field_info->variable]; + + name = g_strdup_printf ("%s-%s-active", + group_info->name, + variable_info->name); + + if (! strcmp (aux->name, name)) + { + gboolean active = ! strcmp (aux->value, "yes"); + + gimp_dashboard_field_set_active (dashboard, + group, field, + active); + + g_free (name); + goto next_aux_info; + } + + g_free (name); + } + } + } + } +next_aux_info: ; + } + + gimp_dashboard_update_groups (dashboard); +} + +static GList * +gimp_dashboard_get_aux_info (GimpDocked *docked) +{ + GimpDashboard *dashboard = GIMP_DASHBOARD (docked); + GimpDashboardPrivate *priv = dashboard->priv; + GList *aux_info; + GimpSessionInfoAux *aux; + gchar *name; + gchar *value; + Group group; + gint field; + + aux_info = parent_docked_iface->get_aux_info (docked); + + if (priv->update_interval != DEFAULT_UPDATE_INTERVAL) + { + value = g_strdup_printf ("%d", priv->update_interval); + aux = gimp_session_info_aux_new (AUX_INFO_UPDATE_INTERVAL, value); + aux_info = g_list_append (aux_info, aux); + g_free (value); + } + + if (priv->history_duration != DEFAULT_HISTORY_DURATION) + { + value = g_strdup_printf ("%d", priv->history_duration); + aux = gimp_session_info_aux_new (AUX_INFO_HISTORY_DURATION, value); + aux_info = g_list_append (aux_info, aux); + g_free (value); + } + + if (priv->low_swap_space_warning != DEFAULT_LOW_SWAP_SPACE_WARNING) + { + value = priv->low_swap_space_warning ? "yes" : "no"; + aux = gimp_session_info_aux_new (AUX_INFO_LOW_SWAP_SPACE_WARNING, value); + aux_info = g_list_append (aux_info, aux); + } + + for (group = FIRST_GROUP; group < N_GROUPS; group++) + { + const GroupInfo *group_info = &groups[group]; + GroupData *group_data = &priv->groups[group]; + gboolean active = group_data->active; + gboolean expanded = gtk_expander_get_expanded (group_data->expander); + + if (active != group_info->default_active) + { + name = g_strdup_printf ("%s-active", group_info->name); + value = active ? "yes" : "no"; + aux = gimp_session_info_aux_new (name, value); + aux_info = g_list_append (aux_info, aux); + g_free (name); + } + + if (expanded != group_info->default_expanded) + { + name = g_strdup_printf ("%s-expanded", group_info->name); + value = expanded ? "yes" : "no"; + aux = gimp_session_info_aux_new (name, value); + aux_info = g_list_append (aux_info, aux); + g_free (name); + } + + for (field = 0; field < group_data->n_fields; field++) + { + const FieldInfo *field_info = &group_info->fields[field]; + FieldData *field_data = &group_data->fields[field]; + gboolean active = field_data->active; + + if (field_info->variable != VARIABLE_SEPARATOR) + { + const VariableInfo *variable_info = &variables[field_info->variable]; + + if (active != field_info->default_active) + { + name = g_strdup_printf ("%s-%s-active", + group_info->name, + variable_info->name); + value = active ? "yes" : "no"; + aux = gimp_session_info_aux_new (name, value); + aux_info = g_list_append (aux_info, aux); + g_free (name); + } + } + } + } + + return aux_info; +} + +static gboolean +gimp_dashboard_group_expander_button_press (GimpDashboard *dashboard, + GdkEventButton *bevent, + GtkWidget *widget) +{ + GimpDashboardPrivate *priv = dashboard->priv; + Group group; + GroupData *group_data; + GtkAllocation expander_allocation; + GtkAllocation allocation; + + group = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), + "gimp-dashboard-group")); + group_data = &priv->groups[group]; + + gtk_widget_get_allocation (GTK_WIDGET (group_data->expander), + &expander_allocation); + gtk_widget_get_allocation (GTK_WIDGET (group_data->menu_button), + &allocation); + + allocation.x -= expander_allocation.x; + allocation.y -= expander_allocation.y; + + if (bevent->button == 1 && + bevent->x >= allocation.x && + bevent->x < allocation.x + allocation.width && + bevent->y >= allocation.y && + bevent->y < allocation.y + allocation.height) + { + gtk_menu_popup (group_data->menu, + NULL, NULL, + gimp_dashboard_group_menu_position, + group_data->menu_button, + bevent->button, bevent->time); + + return TRUE; + } + + return FALSE; +} + +static void +gimp_dashboard_group_action_toggled (GimpDashboard *dashboard, + GimpToggleAction *action) +{ + GimpDashboardPrivate *priv = dashboard->priv; + Group group; + GroupData *group_data; + + group = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (action), + "gimp-dashboard-group")); + group_data = &priv->groups[group]; + + group_data->active = gimp_toggle_action_get_active (action); + + gimp_dashboard_update_group (dashboard, group); +} + +static void +gimp_dashboard_field_menu_item_toggled (GimpDashboard *dashboard, + GtkCheckMenuItem *item) +{ + GimpDashboardPrivate *priv = dashboard->priv; + Group group; + GroupData *group_data; + gint field; + FieldData *field_data; + + group = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), + "gimp-dashboard-group")); + group_data = &priv->groups[group]; + + field = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), + "gimp-dashboard-field")); + field_data = &group_data->fields[field]; + + field_data->active = gtk_check_menu_item_get_active (item); + + gimp_dashboard_update_group (dashboard, group); +} + +static gpointer +gimp_dashboard_sample (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv = dashboard->priv; + gint64 last_sample_time = 0; + gint64 last_update_time = 0; + gboolean seen_low_swap_space = FALSE; + + g_mutex_lock (&priv->mutex); + + while (! priv->quit) + { + gint64 update_interval; + gint64 sample_interval; + gint64 end_time; + + update_interval = priv->update_interval * G_TIME_SPAN_SECOND / 1000; + + if (priv->log_output) + { + sample_interval = G_TIME_SPAN_SECOND / + priv->log_params.sample_frequency; + } + else + { + sample_interval = update_interval; + } + + end_time = last_sample_time + sample_interval; + + if (! g_cond_wait_until (&priv->cond, &priv->mutex, end_time) || + priv->update_now) + { + gint64 time; + gboolean variables_changed = FALSE; + Variable variable; + Group group; + gint field; + + time = g_get_monotonic_time (); + + /* sample all variables */ + for (variable = FIRST_VARIABLE; variable < N_VARIABLES; variable++) + { + const VariableInfo *variable_info = &variables[variable]; + const VariableData *variable_data = &priv->variables[variable]; + VariableData prev_variable_data = *variable_data; + + variable_info->sample_func (dashboard, variable); + + variables_changed = variables_changed || + memcmp (variable_data, &prev_variable_data, + sizeof (VariableData)); + } + + /* log sample */ + if (priv->log_output) + gimp_dashboard_log_sample (dashboard, variables_changed, FALSE); + + /* update gui */ + if (priv->update_now || + ! priv->log_output || + time - last_update_time >= update_interval) + { + /* add samples to meters */ + for (group = FIRST_GROUP; group < N_GROUPS; group++) + { + const GroupInfo *group_info = &groups[group]; + GroupData *group_data = &priv->groups[group]; + gdouble *sample; + gdouble total = 0.0; + + if (! group_info->has_meter) + continue; + + sample = g_new (gdouble, group_data->n_meter_values); + + for (field = 0; field < group_data->n_fields; field++) + { + const FieldInfo *field_info = &group_info->fields[field]; + + if (field_info->meter_value) + { + gdouble value; + + if (field_info->meter_variable) + variable = field_info->meter_variable; + else + variable = field_info->variable; + + value = gimp_dashboard_variable_to_double (dashboard, + variable); + + if (value && + gimp_dashboard_field_use_meter_underlay (group, + field)) + { + value = G_MAXDOUBLE; + } + + if (field_info->meter_cumulative) + { + total += value; + value = total; + } + + sample[field_info->meter_value - 1] = value; + } + } + + gimp_meter_add_sample (group_data->meter, sample); + + g_free (sample); + } + + if (variables_changed) + { + /* enqueue update source */ + if (! priv->update_idle_id && + gtk_widget_get_mapped (GTK_WIDGET (dashboard))) + { + priv->update_idle_id = g_idle_add_full ( + G_PRIORITY_DEFAULT, + (GSourceFunc) gimp_dashboard_update, + dashboard, NULL); + } + + /* check for low swap space */ + if (priv->low_swap_space_warning && + priv->variables[VARIABLE_SWAP_OCCUPIED].available && + priv->variables[VARIABLE_SWAP_LIMIT].available) + { + guint64 swap_occupied; + guint64 swap_limit; + + swap_occupied = priv->variables[VARIABLE_SWAP_OCCUPIED].value.size; + swap_limit = priv->variables[VARIABLE_SWAP_LIMIT].value.size; + + if (! seen_low_swap_space && + swap_occupied >= LOW_SWAP_SPACE_WARNING_ON * swap_limit) + { + if (! priv->low_swap_space_idle_id) + { + priv->low_swap_space_idle_id = + g_idle_add_full (G_PRIORITY_HIGH, + (GSourceFunc) gimp_dashboard_low_swap_space, + dashboard, NULL); + } + + seen_low_swap_space = TRUE; + } + else if (seen_low_swap_space && + swap_occupied <= LOW_SWAP_SPACE_WARNING_OFF * swap_limit) + { + if (priv->low_swap_space_idle_id) + { + g_source_remove (priv->low_swap_space_idle_id); + + priv->low_swap_space_idle_id = 0; + } + + seen_low_swap_space = FALSE; + } + } + } + + priv->update_now = FALSE; + + last_update_time = time; + } + + last_sample_time = time; + } + } + + g_mutex_unlock (&priv->mutex); + + return NULL; +} + +static gboolean +gimp_dashboard_update (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv = dashboard->priv; + Group group; + + g_mutex_lock (&priv->mutex); + + for (group = FIRST_GROUP; group < N_GROUPS; group++) + gimp_dashboard_update_group_values (dashboard, group); + + priv->update_idle_id = 0; + + g_mutex_unlock (&priv->mutex); + + return G_SOURCE_REMOVE; +} + +static gboolean +gimp_dashboard_low_swap_space (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv = dashboard->priv; + + if (priv->gimp) + { + GdkScreen *screen; + gint monitor; + gint field; + + gtk_expander_set_expanded (priv->groups[GROUP_SWAP].expander, TRUE); + + for (field = 0; field < priv->groups[GROUP_SWAP].n_fields; field++) + { + const FieldInfo *field_info = &groups[GROUP_SWAP].fields[field]; + + if (field_info->variable == VARIABLE_SWAP_OCCUPIED || + field_info->variable == VARIABLE_SWAP_LIMIT) + { + gimp_dashboard_field_set_active (dashboard, + GROUP_SWAP, field, TRUE); + } + } + + gimp_dashboard_update_groups (dashboard); + + monitor = gimp_get_monitor_at_pointer (&screen); + + gimp_window_strategy_show_dockable_dialog ( + GIMP_WINDOW_STRATEGY (gimp_get_window_strategy (priv->gimp)), + priv->gimp, + gimp_dialog_factory_get_singleton (), + screen, monitor, + "gimp-dashboard"); + + g_mutex_lock (&priv->mutex); + + priv->low_swap_space_idle_id = 0; + + g_mutex_unlock (&priv->mutex); + } + + return G_SOURCE_REMOVE; +} + +static void +gimp_dashboard_sample_function (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + const VariableInfo *variable_info = &variables[variable]; + VariableData *variable_data = &priv->variables[variable]; + + #define CALL_FUNC(result_type) \ + (((result_type (*) (void)) variable_info->data) ()) + + switch (variable_info->type) + { + case VARIABLE_TYPE_BOOLEAN: + variable_data->value.boolean = CALL_FUNC (gboolean); + break; + + case VARIABLE_TYPE_INTEGER: + variable_data->value.integer = CALL_FUNC (gint); + + case VARIABLE_TYPE_SIZE: + variable_data->value.size = CALL_FUNC (guint64); + break; + + case VARIABLE_TYPE_PERCENTAGE: + variable_data->value.percentage = CALL_FUNC (gdouble); + break; + + case VARIABLE_TYPE_DURATION: + variable_data->value.duration = CALL_FUNC (gdouble); + break; + + case VARIABLE_TYPE_RATE_OF_CHANGE: + variable_data->value.rate_of_change = CALL_FUNC (gdouble); + break; + + case VARIABLE_TYPE_SIZE_RATIO: + case VARIABLE_TYPE_INT_RATIO: + g_return_if_reached (); + break; + } + + #undef CALL_FUNC + + variable_data->available = TRUE; +} + +static void +gimp_dashboard_sample_gegl_config (GimpDashboard *dashboard, + Variable variable) +{ + gimp_dashboard_sample_object (dashboard, G_OBJECT (gegl_config ()), variable); +} + +static void +gimp_dashboard_sample_gegl_stats (GimpDashboard *dashboard, + Variable variable) +{ + gimp_dashboard_sample_object (dashboard, G_OBJECT (gegl_stats ()), variable); +} + +static void +gimp_dashboard_sample_variable_changed (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + const VariableInfo *variable_info = &variables[variable]; + VariableData *variable_data = &priv->variables[variable]; + Variable var = GPOINTER_TO_INT (variable_info->data); + const VariableData *var_data = &priv->variables[var]; + gpointer prev_value = gimp_dashboard_variable_get_data ( + dashboard, variable, + sizeof (var_data->value)); + + if (var_data->available) + { + variable_data->available = TRUE; + variable_data->value.boolean = memcmp (&var_data->value, prev_value, + sizeof (var_data->value)) != 0; + + if (variable_data->value.boolean) + memcpy (prev_value, &var_data->value, sizeof (var_data->value)); + } + else + { + variable_data->available = FALSE; + } +} + +static void +gimp_dashboard_sample_variable_rate_of_change (GimpDashboard *dashboard, + Variable variable) +{ + typedef struct + { + gint64 last_time; + gboolean last_available; + gdouble last_value; + } Data; + + GimpDashboardPrivate *priv = dashboard->priv; + const VariableInfo *variable_info = &variables[variable]; + VariableData *variable_data = &priv->variables[variable]; + Variable var = GPOINTER_TO_INT (variable_info->data); + const VariableData *var_data = &priv->variables[var]; + Data *data = gimp_dashboard_variable_get_data ( + dashboard, variable, sizeof (Data)); + gint64 time; + + time = g_get_monotonic_time (); + + if (time == data->last_time) + return; + + variable_data->available = FALSE; + + if (var_data->available) + { + gdouble value = gimp_dashboard_variable_to_double (dashboard, var); + + if (data->last_available) + { + variable_data->available = TRUE; + variable_data->value.rate_of_change = (value - data->last_value) * + G_TIME_SPAN_SECOND / + (time - data->last_time); + } + + data->last_value = value; + } + + data->last_time = time; + data->last_available = var_data->available; +} + +static void +gimp_dashboard_sample_swap_limit (GimpDashboard *dashboard, + Variable variable) +{ + typedef struct + { + guint64 free_space; + gboolean has_free_space; + gint64 last_check_time; + } Data; + + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + Data *data = gimp_dashboard_variable_get_data ( + dashboard, variable, sizeof (Data)); + gint64 time; + + /* we don't have a config option for limiting the swap size, so we simply + * return the free space available on the filesystem containing the swap + */ + + time = g_get_monotonic_time (); + + if (time - data->last_check_time >= G_TIME_SPAN_SECOND) + { + gchar *swap_dir; + + g_object_get (gegl_config (), + "swap", &swap_dir, + NULL); + + data->free_space = 0; + data->has_free_space = FALSE; + + if (swap_dir) + { + GFile *file; + GFileInfo *info; + + file = g_file_new_for_path (swap_dir); + + info = g_file_query_filesystem_info (file, + G_FILE_ATTRIBUTE_FILESYSTEM_FREE, + NULL, NULL); + + if (info) + { + data->free_space = + g_file_info_get_attribute_uint64 (info, + G_FILE_ATTRIBUTE_FILESYSTEM_FREE); + data->has_free_space = TRUE; + + g_object_unref (info); + } + + g_object_unref (file); + + g_free (swap_dir); + } + + data->last_check_time = time; + } + + variable_data->available = data->has_free_space; + + if (data->has_free_space) + { + variable_data->value.size = data->free_space; + + if (priv->variables[VARIABLE_SWAP_SIZE].available) + { + /* the swap limit is the sum of free_space and swap_size, since the + * swap itself occupies space in the filesystem + */ + variable_data->value.size += + priv->variables[VARIABLE_SWAP_SIZE].value.size; + } + } +} + +#ifdef HAVE_CPU_GROUP + +#ifdef HAVE_SYS_TIMES_H + +static void +gimp_dashboard_sample_cpu_usage (GimpDashboard *dashboard, + Variable variable) +{ + typedef struct + { + clock_t prev_clock; + clock_t prev_usage; + } Data; + + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + Data *data = gimp_dashboard_variable_get_data ( + dashboard, variable, sizeof (Data)); + clock_t curr_clock; + clock_t curr_usage; + struct tms tms; + + curr_clock = times (&tms); + + if (curr_clock == (clock_t) -1) + { + data->prev_clock = 0; + + variable_data->available = FALSE; + + return; + } + + curr_usage = tms.tms_utime + tms.tms_stime; + + if (data->prev_clock && curr_clock != data->prev_clock) + { + variable_data->available = TRUE; + variable_data->value.percentage = (gdouble) (curr_usage - data->prev_usage) / + (curr_clock - data->prev_clock); + variable_data->value.percentage /= g_get_num_processors (); + } + else + { + variable_data->available = FALSE; + } + + data->prev_clock = curr_clock; + data->prev_usage = curr_usage; +} + +#elif defined (G_OS_WIN32) + +static void +gimp_dashboard_sample_cpu_usage (GimpDashboard *dashboard, + Variable variable) +{ + typedef struct + { + guint64 prev_time; + guint64 prev_usage; + } Data; + + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + Data *data = gimp_dashboard_variable_get_data ( + dashboard, variable, sizeof (Data)); + guint64 curr_time; + guint64 curr_usage; + FILETIME system_time; + FILETIME process_creation_time; + FILETIME process_exit_time; + FILETIME process_kernel_time; + FILETIME process_user_time; + + if (! GetProcessTimes (GetCurrentProcess (), + &process_creation_time, + &process_exit_time, + &process_kernel_time, + &process_user_time)) + { + data->prev_time = 0; + + variable_data->available = FALSE; + + return; + } + + GetSystemTimeAsFileTime (&system_time); + + curr_time = ((guint64) system_time.dwHighDateTime << 32) | + (guint64) system_time.dwLowDateTime; + + curr_usage = ((guint64) process_kernel_time.dwHighDateTime << 32) | + (guint64) process_kernel_time.dwLowDateTime; + curr_usage += ((guint64) process_user_time.dwHighDateTime << 32) | + (guint64) process_user_time.dwLowDateTime; + + if (data->prev_time && curr_time != data->prev_time) + { + variable_data->available = TRUE; + variable_data->value.percentage = (gdouble) (curr_usage - data->prev_usage) / + (curr_time - data->prev_time); + variable_data->value.percentage /= g_get_num_processors (); + } + else + { + variable_data->available = FALSE; + } + + data->prev_time = curr_time; + data->prev_usage = curr_usage; +} + +#endif /* G_OS_WIN32 */ + +static void +gimp_dashboard_sample_cpu_active (GimpDashboard *dashboard, + Variable variable) +{ + typedef struct + { + gboolean active; + } Data; + + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + Data *data = gimp_dashboard_variable_get_data ( + dashboard, variable, sizeof (Data)); + gboolean active = FALSE; + + if (priv->variables[VARIABLE_CPU_USAGE].available) + { + if (! data->active) + { + active = + priv->variables[VARIABLE_CPU_USAGE].value.percentage * + g_get_num_processors () > CPU_ACTIVE_ON; + } + else + { + active = + priv->variables[VARIABLE_CPU_USAGE].value.percentage * + g_get_num_processors () > CPU_ACTIVE_OFF; + } + + variable_data->available = TRUE; + } + else + { + variable_data->available = FALSE; + } + + data->active = active; + variable_data->value.boolean = active; +} + +static void +gimp_dashboard_sample_cpu_active_time (GimpDashboard *dashboard, + Variable variable) +{ + typedef struct + { + gint64 prev_time; + gint64 active_time; + } Data; + + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + Data *data = gimp_dashboard_variable_get_data ( + dashboard, variable, sizeof (Data)); + gint64 curr_time; + + curr_time = g_get_monotonic_time (); + + if (priv->variables[VARIABLE_CPU_ACTIVE].available) + { + gboolean active = priv->variables[VARIABLE_CPU_ACTIVE].value.boolean; + + if (active && data->prev_time) + data->active_time += curr_time - data->prev_time; + } + + data->prev_time = curr_time; + + variable_data->available = TRUE; + variable_data->value.duration = data->active_time / 1000000.0; +} + +#endif /* HAVE_CPU_GROUP */ + +#ifdef HAVE_MEMORY_GROUP +#ifdef PLATFORM_OSX +static void +gimp_dashboard_sample_memory_used (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + + variable_data->available = FALSE; +#ifndef TASK_VM_INFO_REV0_COUNT /* phys_footprint added in REV1 */ + struct mach_task_basic_info info; + mach_msg_type_number_t infoCount = MACH_TASK_BASIC_INFO_COUNT; + + if( task_info(mach_task_self (), MACH_TASK_BASIC_INFO, + (task_info_t)&info, &infoCount ) != KERN_SUCCESS ) + return; /* Can't access? */ + + variable_data->available = TRUE; + variable_data->value.size = info.resident_size; +#else + task_vm_info_data_t info; + mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT; + + if( task_info(mach_task_self (), TASK_VM_INFO, + (task_info_t)&info, &infoCount ) != KERN_SUCCESS ) + return; /* Can't access? */ + variable_data->available = TRUE; + variable_data->value.size = info.phys_footprint; +#endif /* ! TASK_VM_INFO_REV0_COUNT */ +} + +static void +gimp_dashboard_sample_memory_available (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + vm_statistics_data_t info; + mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT; + + variable_data->available = FALSE; + + + if( host_statistics(mach_host_self (), HOST_VM_INFO, + (host_info_t)&info, &infoCount ) != KERN_SUCCESS ) + return; /* Can't access? */ + + variable_data->available = TRUE; + variable_data->value.size = info.free_count * PAGE_SIZE; +} + +#elif defined(G_OS_WIN32) +static void +gimp_dashboard_sample_memory_used (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + PROCESS_MEMORY_COUNTERS_EX pmc = {}; + + variable_data->available = FALSE; + + if (! GetProcessMemoryInfo (GetCurrentProcess (), + (PPROCESS_MEMORY_COUNTERS) &pmc, + sizeof (pmc)) || + pmc.cb != sizeof (pmc)) + { + return; + } + + variable_data->available = TRUE; + variable_data->value.size = pmc.PrivateUsage; +} + +static void +gimp_dashboard_sample_memory_available (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + MEMORYSTATUSEX ms; + + variable_data->available = FALSE; + + ms.dwLength = sizeof (ms); + + if (! GlobalMemoryStatusEx (&ms)) + return; + + variable_data->available = TRUE; + variable_data->value.size = ms.ullAvailPhys; +} + +#elif defined(__OpenBSD__) +#include <sys/resource.h> +#include <sys/types.h> +#include <sys/sysctl.h> + +static void +gimp_dashboard_sample_memory_used (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + struct rusage rusage; + + variable_data->available = FALSE; + + if (getrusage (RUSAGE_SELF, &rusage) == -1) + return; + variable_data->available = TRUE; + variable_data->value.size = (guint64) (rusage.ru_maxrss * 1024); +} + +static void +gimp_dashboard_sample_memory_available (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + int mib[] = { CTL_HW, HW_PHYSMEM64 }; + int64_t result; + size_t sz = sizeof(int64_t); + + variable_data->available = FALSE; + + if (sysctl (mib, 2, &result, &sz, NULL, 0) == -1) + return; + variable_data->available = TRUE; + variable_data->value.size = (guint64) result; +} + +#else /* ! G_OS_WIN32 && ! PLATFORM_OSX */ +static void +gimp_dashboard_sample_memory_used (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + static gboolean initialized = FALSE; + static long page_size; + static gint fd = -1; + gchar buffer[128]; + gint size; + unsigned long long resident; + unsigned long long shared; + + if (! initialized) + { + page_size = sysconf (_SC_PAGE_SIZE); + + if (page_size > 0) + fd = open ("/proc/self/statm", O_RDONLY); + + initialized = TRUE; + } + + variable_data->available = FALSE; + + if (fd < 0) + return; + + if (lseek (fd, 0, SEEK_SET)) + return; + + size = read (fd, buffer, sizeof (buffer) - 1); + + if (size <= 0) + return; + + buffer[size] = '\0'; + + if (sscanf (buffer, "%*u %llu %llu", &resident, &shared) != 2) + return; + + variable_data->available = TRUE; + variable_data->value.size = (guint64) (resident - shared) * page_size; +} + +static void +gimp_dashboard_sample_memory_available (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + static gboolean initialized = FALSE; + static gint64 last_check_time = 0; + static gint fd; + static guint64 available; + static gboolean has_available = FALSE; + gint64 time; + + if (! initialized) + { + fd = open ("/proc/meminfo", O_RDONLY); + + initialized = TRUE; + } + + variable_data->available = FALSE; + + if (fd < 0) + return; + + /* we don't have a config option for limiting the swap size, so we simply + * return the free space available on the filesystem containing the swap + */ + + time = g_get_monotonic_time (); + + if (time - last_check_time >= G_TIME_SPAN_SECOND) + { + gchar buffer[512]; + gint size; + gchar *str; + + last_check_time = time; + + has_available = FALSE; + + if (lseek (fd, 0, SEEK_SET)) + return; + + size = read (fd, buffer, sizeof (buffer) - 1); + + if (size <= 0) + return; + + buffer[size] = '\0'; + + str = strstr (buffer, "MemAvailable:"); + + if (! str) + return; + + available = strtoull (str + 13, &str, 0); + + if (! str) + return; + + for (; *str; str++) + { + if (*str == 'k') + { + available <<= 10; + break; + } + else if (*str == 'M') + { + available <<= 20; + break; + } + } + + if (! *str) + return; + + has_available = TRUE; + } + + if (! has_available) + return; + + variable_data->available = TRUE; + variable_data->value.size = available; +} + +#endif + +static void +gimp_dashboard_sample_memory_size (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + + variable_data->value.size = gimp_get_physical_memory_size (); + variable_data->available = variable_data->value.size > 0; +} + +#endif /* HAVE_MEMORY_GROUP */ + +static void +gimp_dashboard_sample_object (GimpDashboard *dashboard, + GObject *object, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + GObjectClass *klass = G_OBJECT_GET_CLASS (object); + const VariableInfo *variable_info = &variables[variable]; + VariableData *variable_data = &priv->variables[variable]; + + variable_data->available = FALSE; + + switch (variable_info->type) + { + case VARIABLE_TYPE_BOOLEAN: + if (g_object_class_find_property (klass, variable_info->data)) + { + variable_data->available = TRUE; + + g_object_get (object, + variable_info->data, &variable_data->value.boolean, + NULL); + } + break; + + case VARIABLE_TYPE_INTEGER: + if (g_object_class_find_property (klass, variable_info->data)) + { + variable_data->available = TRUE; + + g_object_get (object, + variable_info->data, &variable_data->value.integer, + NULL); + } + break; + + case VARIABLE_TYPE_SIZE: + if (g_object_class_find_property (klass, variable_info->data)) + { + variable_data->available = TRUE; + + g_object_get (object, + variable_info->data, &variable_data->value.size, + NULL); + } + break; + + case VARIABLE_TYPE_SIZE_RATIO: + { + const gchar *antecedent = variable_info->data; + const gchar *consequent = antecedent + strlen (antecedent) + 1; + + if (g_object_class_find_property (klass, antecedent) && + g_object_class_find_property (klass, consequent)) + { + variable_data->available = TRUE; + + g_object_get (object, + antecedent, &variable_data->value.size_ratio.antecedent, + consequent, &variable_data->value.size_ratio.consequent, + NULL); + } + } + break; + + case VARIABLE_TYPE_INT_RATIO: + { + const gchar *antecedent = variable_info->data; + const gchar *consequent = antecedent + strlen (antecedent) + 1; + + if (g_object_class_find_property (klass, antecedent) && + g_object_class_find_property (klass, consequent)) + { + variable_data->available = TRUE; + + g_object_get (object, + antecedent, &variable_data->value.int_ratio.antecedent, + consequent, &variable_data->value.int_ratio.consequent, + NULL); + } + } + break; + + case VARIABLE_TYPE_PERCENTAGE: + if (g_object_class_find_property (klass, variable_info->data)) + { + variable_data->available = TRUE; + + g_object_get (object, + variable_info->data, &variable_data->value.percentage, + NULL); + } + break; + + case VARIABLE_TYPE_DURATION: + if (g_object_class_find_property (klass, variable_info->data)) + { + variable_data->available = TRUE; + + g_object_get (object, + variable_info->data, &variable_data->value.duration, + NULL); + } + break; + + case VARIABLE_TYPE_RATE_OF_CHANGE: + if (g_object_class_find_property (klass, variable_info->data)) + { + variable_data->available = TRUE; + + g_object_get (object, + variable_info->data, &variable_data->value.rate_of_change, + NULL); + } + break; + } +} + +static void +gimp_dashboard_group_menu_position (GtkMenu *menu, + gint *x, + gint *y, + gboolean *push_in, + gpointer user_data) +{ + gimp_button_menu_position (user_data, menu, GTK_POS_LEFT, x, y); +} + +static void +gimp_dashboard_update_groups (GimpDashboard *dashboard) +{ + Group group; + + for (group = FIRST_GROUP; group < N_GROUPS; group++) + gimp_dashboard_update_group (dashboard, group); +} + +static void +gimp_dashboard_update_group (GimpDashboard *dashboard, + Group group) +{ + GimpDashboardPrivate *priv = dashboard->priv; + const GroupInfo *group_info = &groups[group]; + GroupData *group_data = &priv->groups[group]; + gint n_rows; + gboolean add_separator; + gint field; + + gtk_widget_set_visible (GTK_WIDGET (group_data->expander), + group_data->active); + + if (! group_data->active) + return; + + n_rows = 0; + add_separator = FALSE; + + for (field = 0; field < group_data->n_fields; field++) + { + const FieldInfo *field_info = &group_info->fields[field]; + const FieldData *field_data = &group_data->fields[field]; + + if (field_info->variable != VARIABLE_SEPARATOR) + { + if (group_info->has_meter && field_info->meter_value) + { + gimp_meter_set_value_active (group_data->meter, + field_info->meter_value - 1, + field_data->active); + } + + if (field_data->active) + { + if (add_separator) + { + add_separator = FALSE; + n_rows++; + } + + n_rows++; + } + } + else + { + if (n_rows > 0) + add_separator = TRUE; + } + } + + gimp_gtk_container_clear (GTK_CONTAINER (group_data->table)); + gtk_table_resize (group_data->table, MAX (n_rows, 1), 3); + + n_rows = 0; + add_separator = FALSE; + + for (field = 0; field < group_data->n_fields; field++) + { + const FieldInfo *field_info = &group_info->fields[field]; + FieldData *field_data = &group_data->fields[field]; + + if (field_info->variable != VARIABLE_SEPARATOR) + { + const VariableInfo *variable_info = &variables[field_info->variable]; + GtkWidget *separator; + GtkWidget *color_area; + GtkWidget *label; + const gchar *description; + gchar *str; + + if (! field_data->active) + continue; + + description = g_dgettext (NULL, variable_info->description); + + if (add_separator) + { + separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_table_attach (group_data->table, separator, + 0, 3, n_rows, n_rows + 1, + GTK_EXPAND | GTK_FILL, 0, + 0, 0); + gtk_widget_show (separator); + + add_separator = FALSE; + n_rows++; + } + + if (group_info->has_meter && field_info->meter_value) + { + color_area = gimp_color_area_new (&variable_info->color, + GIMP_COLOR_AREA_FLAT, 0); + gimp_help_set_help_data (color_area, description, + NULL); + gtk_widget_set_size_request (color_area, 5, 5); + gtk_table_attach (group_data->table, color_area, + 0, 1, n_rows, n_rows + 1, + 0, 0, + 0, 0); + gtk_widget_show (color_area); + } + + str = g_strdup_printf ("%s:", + g_dpgettext2 (NULL, "dashboard-variable", + field_info->title ? + field_info->title : + variable_info->title)); + + label = gtk_label_new (str); + gimp_help_set_help_data (label, description, + NULL); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_table_attach (group_data->table, label, + 1, 2, n_rows, n_rows + 1, + GTK_FILL, 0, + 0, 0); + gtk_widget_show (label); + + g_free (str); + + label = gtk_label_new (NULL); + field_data->value_label = GTK_LABEL (label); + gimp_help_set_help_data (label, description, + NULL); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_table_attach (group_data->table, label, + 2, 3, n_rows, n_rows + 1, + GTK_EXPAND | GTK_FILL, 0, + 0, 0); + gtk_widget_show (label); + + n_rows++; + } + else + { + if (n_rows > 0) + add_separator = TRUE; + } + } + + g_mutex_lock (&priv->mutex); + + gimp_dashboard_update_group_values (dashboard, group); + + g_mutex_unlock (&priv->mutex); +} + +static void +gimp_dashboard_update_group_values (GimpDashboard *dashboard, + Group group) +{ + GimpDashboardPrivate *priv = dashboard->priv; + const GroupInfo *group_info = &groups[group]; + GroupData *group_data = &priv->groups[group]; + gdouble limit = 0.0; + GString *header_values; + gint field; + + if (! group_data->active) + return; + + if (group_info->has_meter) + { + if (group_info->meter_limit) + { + const VariableData *variable_data = &priv->variables[group_info->meter_limit]; + + if (variable_data->available) + { + limit = gimp_dashboard_variable_to_double (dashboard, + group_info->meter_limit); + } + else + { + for (field = 0; field < group_data->n_fields; field++) + { + const FieldInfo *field_info = &group_info->fields[field]; + const FieldData *field_data = &group_data->fields[field]; + + if (field_info->meter_value && field_data->active) + { + gdouble value; + + value = gimp_dashboard_variable_to_double (dashboard, + field_info->variable); + + limit = MAX (limit, value); + } + } + } + + gimp_meter_set_range (group_data->meter, 0.0, limit); + } + + if (group_info->meter_led) + { + GimpRGB color = {0.0, 0.0, 0.0, 1.0}; + gboolean active = FALSE; + const Variable *var; + + for (var = group_info->meter_led; *var; var++) + { + if (gimp_dashboard_variable_to_boolean (dashboard, *var)) + { + const VariableInfo *variable_info = &variables[*var]; + + color.r = MAX (color.r, variable_info->color.r); + color.g = MAX (color.g, variable_info->color.g); + color.b = MAX (color.b, variable_info->color.b); + + active = TRUE; + } + } + + if (active) + gimp_meter_set_led_color (group_data->meter, &color); + + gimp_meter_set_led_active (group_data->meter, active); + } + } + + group_data->limit = limit; + + header_values = g_string_new (NULL); + + for (field = 0; field < group_data->n_fields; field++) + { + const FieldInfo *field_info = &group_info->fields[field]; + const FieldData *field_data = &group_data->fields[field]; + + if (field_data->active) + { + gchar *text; + + text = gimp_dashboard_field_to_string (dashboard, + group, field, TRUE); + + gimp_dashboard_label_set_text (field_data->value_label, text); + + g_free (text); + + if (field_info->show_in_header) + { + text = gimp_dashboard_field_to_string (dashboard, + group, field, FALSE); + + if (header_values->len > 0) + g_string_append (header_values, ", "); + + g_string_append (header_values, text); + + g_free (text); + } + } + } + + if (header_values->len > 0) + { + g_string_prepend (header_values, "("); + g_string_append (header_values, ")"); + } + + gimp_dashboard_label_set_text (group_data->header_values_label, + header_values->str); + + g_string_free (header_values, TRUE); +} + +static void +gimp_dashboard_group_set_active (GimpDashboard *dashboard, + Group group, + gboolean active) +{ + GimpDashboardPrivate *priv = dashboard->priv; + GroupData *group_data = &priv->groups[group]; + + if (active != group_data->active) + { + group_data->active = active; + + if (group_data->action) + { + g_signal_handlers_block_by_func (group_data->action, + gimp_dashboard_group_action_toggled, + dashboard); + + gimp_toggle_action_set_active (group_data->action, active); + + g_signal_handlers_unblock_by_func (group_data->action, + gimp_dashboard_group_action_toggled, + dashboard); + } + } +} + +static void +gimp_dashboard_field_set_active (GimpDashboard *dashboard, + Group group, + gint field, + gboolean active) +{ + GimpDashboardPrivate *priv = dashboard->priv; + GroupData *group_data = &priv->groups[group]; + FieldData *field_data = &group_data->fields[field]; + + if (active != field_data->active) + { + field_data->active = active; + + g_signal_handlers_block_by_func (field_data->menu_item, + gimp_dashboard_field_menu_item_toggled, + dashboard); + + gtk_check_menu_item_set_active (field_data->menu_item, active); + + g_signal_handlers_unblock_by_func (field_data->menu_item, + gimp_dashboard_field_menu_item_toggled, + dashboard); + } +} + +static void +gimp_dashboard_reset_unlocked (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv; + Group group; + + priv = dashboard->priv; + + gegl_reset_stats (); + + gimp_dashboard_reset_variables (dashboard); + + for (group = FIRST_GROUP; group < N_GROUPS; group++) + { + GroupData *group_data = &priv->groups[group]; + + if (group_data->meter) + gimp_meter_clear_history (group_data->meter); + } +} + +static void +gimp_dashboard_reset_variables (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv = dashboard->priv; + Variable variable; + + for (variable = FIRST_VARIABLE; variable < N_VARIABLES; variable++) + { + const VariableInfo *variable_info = &variables[variable]; + VariableData *variable_data = &priv->variables[variable]; + + if (variable_info->reset_func) + variable_info->reset_func (dashboard, variable); + + g_clear_pointer (&variable_data->data, g_free); + variable_data->data_size = 0; + } +} + +static gpointer +gimp_dashboard_variable_get_data (GimpDashboard *dashboard, + Variable variable, + gsize size) +{ + GimpDashboardPrivate *priv = dashboard->priv; + VariableData *variable_data = &priv->variables[variable]; + + if (variable_data->data_size != size) + { + variable_data->data = g_realloc (variable_data->data, size); + + if (variable_data->data_size < size) + { + memset ((guint8 *) variable_data->data + variable_data->data_size, + 0, size - variable_data->data_size); + } + + variable_data->data_size = size; + } + + return variable_data->data; +} + +static gboolean +gimp_dashboard_variable_to_boolean (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + const VariableInfo *variable_info = &variables[variable]; + const VariableData *variable_data = &priv->variables[variable]; + + if (variable_data->available) + { + switch (variable_info->type) + { + case VARIABLE_TYPE_BOOLEAN: + return variable_data->value.boolean; + + case VARIABLE_TYPE_INTEGER: + return variable_data->value.integer != 0; + + case VARIABLE_TYPE_SIZE: + return variable_data->value.size > 0; + + case VARIABLE_TYPE_SIZE_RATIO: + return variable_data->value.size_ratio.antecedent != 0 && + variable_data->value.size_ratio.consequent != 0; + + case VARIABLE_TYPE_INT_RATIO: + return variable_data->value.int_ratio.antecedent != 0 && + variable_data->value.int_ratio.consequent != 0; + + case VARIABLE_TYPE_PERCENTAGE: + return variable_data->value.percentage != 0.0; + + case VARIABLE_TYPE_DURATION: + return variable_data->value.duration != 0.0; + + case VARIABLE_TYPE_RATE_OF_CHANGE: + return variable_data->value.rate_of_change != 0.0; + } + } + + return FALSE; +} + +static gdouble +gimp_dashboard_variable_to_double (GimpDashboard *dashboard, + Variable variable) +{ + GimpDashboardPrivate *priv = dashboard->priv; + const VariableInfo *variable_info = &variables[variable]; + const VariableData *variable_data = &priv->variables[variable]; + + if (variable_data->available) + { + switch (variable_info->type) + { + case VARIABLE_TYPE_BOOLEAN: + return variable_data->value.boolean ? 1.0 : 0.0; + + case VARIABLE_TYPE_INTEGER: + return variable_data->value.integer; + + case VARIABLE_TYPE_SIZE: + return variable_data->value.size; + + case VARIABLE_TYPE_SIZE_RATIO: + if (variable_data->value.size_ratio.consequent) + { + return (gdouble) variable_data->value.size_ratio.antecedent / + (gdouble) variable_data->value.size_ratio.consequent; + } + break; + + case VARIABLE_TYPE_INT_RATIO: + if (variable_data->value.int_ratio.consequent) + { + return (gdouble) variable_data->value.int_ratio.antecedent / + (gdouble) variable_data->value.int_ratio.consequent; + } + break; + + case VARIABLE_TYPE_PERCENTAGE: + return variable_data->value.percentage; + + case VARIABLE_TYPE_DURATION: + return variable_data->value.duration; + + case VARIABLE_TYPE_RATE_OF_CHANGE: + return variable_data->value.rate_of_change; + } + } + + return 0.0; +} + +static gchar * +gimp_dashboard_field_to_string (GimpDashboard *dashboard, + Group group, + gint field, + gboolean full) +{ + GimpDashboardPrivate *priv = dashboard->priv; + const GroupInfo *group_info = &groups[group]; + const GroupData *group_data = &priv->groups[group]; + const FieldInfo *field_info = &group_info->fields[field]; + const VariableInfo *variable_info = &variables[field_info->variable]; + const VariableData *variable_data = &priv->variables[field_info->variable]; + /* Tranlators: "N/A" is an abbreviation for "not available" */ + const gchar *str = C_("dashboard-value", "N/A"); + gboolean static_str = TRUE; + gboolean show_limit = TRUE; + + if (variable_data->available) + { + switch (variable_info->type) + { + case VARIABLE_TYPE_BOOLEAN: + str = variable_data->value.boolean ? C_("dashboard-value", "Yes") : + C_("dashboard-value", "No"); + break; + + case VARIABLE_TYPE_INTEGER: + str = g_strdup_printf ("%d", variable_data->value.integer); + static_str = FALSE; + break; + + case VARIABLE_TYPE_SIZE: + str = g_format_size_full (variable_data->value.size, + G_FORMAT_SIZE_IEC_UNITS); + static_str = FALSE; + break; + + case VARIABLE_TYPE_SIZE_RATIO: + { + if (variable_data->value.size_ratio.consequent) + { + gdouble value; + + value = 100.0 * variable_data->value.size_ratio.antecedent / + variable_data->value.size_ratio.consequent; + + str = g_strdup_printf ("%d%%", SIGNED_ROUND (value)); + static_str = FALSE; + } + } + break; + + case VARIABLE_TYPE_INT_RATIO: + { + gdouble min; + gdouble max; + gdouble antecedent; + gdouble consequent; + + antecedent = variable_data->value.int_ratio.antecedent; + consequent = variable_data->value.int_ratio.consequent; + + min = MIN (ABS (antecedent), ABS (consequent)); + max = MAX (ABS (antecedent), ABS (consequent)); + + if (min) + { + antecedent /= min; + consequent /= min; + } + else if (max) + { + antecedent /= max; + consequent /= max; + } + + if (max) + { + str = g_strdup_printf ("%g:%g", + RINT (100.0 * antecedent) / 100.0, + RINT (100.0 * consequent) / 100.0); + static_str = FALSE; + } + } + break; + + case VARIABLE_TYPE_PERCENTAGE: + str = g_strdup_printf ("%d%%", + SIGNED_ROUND (100.0 * variable_data->value.percentage)); + static_str = FALSE; + show_limit = FALSE; + break; + + case VARIABLE_TYPE_DURATION: + str = g_strdup_printf ("%02d:%02d:%04.1f", + (gint) floor (variable_data->value.duration / 3600.0), + (gint) floor (fmod (variable_data->value.duration / 60.0, 60.0)), + floor (fmod (variable_data->value.duration, 60.0) * 10.0) / 10.0); + static_str = FALSE; + show_limit = FALSE; + break; + + case VARIABLE_TYPE_RATE_OF_CHANGE: + /* Translators: This string reports the rate of change of a measured + * value. The "%g" is replaced by a certain quantity, and the "/s" + * is an abbreviation for "per second". + */ + str = g_strdup_printf (_("%g/s"), + variable_data->value.rate_of_change); + static_str = FALSE; + break; + } + + if (show_limit && + variable_data->available && + field_info->meter_value && + ! field_info->meter_variable && + group_data->limit) + { + gdouble value; + gchar *tmp; + + value = gimp_dashboard_variable_to_double (dashboard, + field_info->variable); + + if (full) + { + tmp = g_strdup_printf ("%s (%d%%)", + str, + SIGNED_ROUND (100.0 * value / + group_data->limit)); + } + else + { + tmp = g_strdup_printf ("%d%%", + SIGNED_ROUND (100.0 * value / + group_data->limit)); + } + + if (! static_str) + g_free ((gpointer) str); + + str = tmp; + static_str = FALSE; + } + else if (full && + field_info->meter_variable && + variables[field_info->meter_variable].type == + VARIABLE_TYPE_RATE_OF_CHANGE && + priv->variables[field_info->meter_variable].available) + { + gdouble value; + gchar *value_str; + gchar *rate_of_change_str; + gchar *tmp; + + value = gimp_dashboard_variable_to_double (dashboard, + field_info->meter_variable); + + value_str = gimp_dashboard_format_value (variable_info->type, value); + + rate_of_change_str = gimp_dashboard_format_rate_of_change (value_str); + + g_free (value_str); + + tmp = g_strdup_printf ("%s (%s)", str, rate_of_change_str); + + g_free (rate_of_change_str); + + if (! static_str) + g_free ((gpointer) str); + + str = tmp; + static_str = FALSE; + } + } + + if (static_str) + return g_strdup (str); + else + return (gpointer) str; +} + +static gboolean +gimp_dashboard_log_printf (GimpDashboard *dashboard, + const gchar *format, + ...) +{ + GimpDashboardPrivate *priv = dashboard->priv; + va_list args; + gboolean result; + + if (priv->log_error) + return FALSE; + + va_start (args, format); + + result = g_output_stream_vprintf (priv->log_output, + NULL, NULL, + &priv->log_error, + format, args); + + va_end (args); + + return result; +} + +static gboolean +gimp_dashboard_log_print_escaped (GimpDashboard *dashboard, + const gchar *string) +{ + GimpDashboardPrivate *priv = dashboard->priv; + gchar buffer[1024]; + const gchar *s; + gint i; + + if (priv->log_error) + return FALSE; + + i = 0; + + #define FLUSH() \ + G_STMT_START \ + { \ + if (! g_output_stream_write_all (priv->log_output, \ + buffer, i, NULL, \ + NULL, &priv->log_error)) \ + { \ + return FALSE; \ + } \ + \ + i = 0; \ + } \ + G_STMT_END + + #define RESERVE(n) \ + G_STMT_START \ + { \ + if (i + (n) > sizeof (buffer)) \ + FLUSH (); \ + } \ + G_STMT_END + + for (s = string; *s; s++) + { + #define ESCAPE(from, to) \ + case from: \ + RESERVE (sizeof (to) - 1); \ + memcpy (&buffer[i], to, sizeof (to) - 1); \ + i += sizeof (to) - 1; \ + break; + + switch (*s) + { + ESCAPE ('"', """) + ESCAPE ('\'', "'") + ESCAPE ('<', "<") + ESCAPE ('>', ">") + ESCAPE ('&', "&") + + default: + RESERVE (1); + buffer[i++] = *s; + break; + } + + #undef ESCAPE + } + + FLUSH (); + + #undef FLUSH + #undef RESERVE + + return TRUE; +} + +static gint64 +gimp_dashboard_log_time (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv = dashboard->priv; + + return g_get_monotonic_time () - priv->log_start_time; +} + +static void +gimp_dashboard_log_sample (GimpDashboard *dashboard, + gboolean variables_changed, + gboolean include_current_thread) +{ + GimpDashboardPrivate *priv = dashboard->priv; + GimpBacktrace *backtrace = NULL; + GArray *addresses = NULL; + gboolean empty = TRUE; + Variable variable; + + #define NONEMPTY() \ + G_STMT_START \ + { \ + if (empty) \ + { \ + gimp_dashboard_log_printf (dashboard, \ + ">\n"); \ + \ + empty = FALSE; \ + } \ + } \ + G_STMT_END + + gimp_dashboard_log_printf (dashboard, + "\n" + "<sample id=\"%d\" t=\"%lld\"", + priv->log_n_samples, + (long long) gimp_dashboard_log_time (dashboard)); + + if (priv->log_n_samples == 0 || variables_changed) + { + NONEMPTY (); + + gimp_dashboard_log_printf (dashboard, + "<vars>\n"); + + for (variable = FIRST_VARIABLE; variable < N_VARIABLES; variable++) + { + const VariableInfo *variable_info = &variables[variable]; + const VariableData *variable_data = &priv->variables[variable]; + VariableData *log_variable_data = &priv->log_variables[variable]; + + if (variable_info->exclude_from_log) + continue; + + if (priv->log_n_samples > 0 && + ! memcmp (variable_data, log_variable_data, + sizeof (VariableData))) + { + continue; + } + + *log_variable_data = *variable_data; + + if (variable_data->available) + { + #define LOG_VAR(format, ...) \ + gimp_dashboard_log_printf (dashboard, \ + "<%s>" format "</%s>\n", \ + variable_info->name, \ + __VA_ARGS__, \ + variable_info->name) + + #define LOG_VAR_FLOAT(value) \ + G_STMT_START \ + { \ + gchar buffer[G_ASCII_DTOSTR_BUF_SIZE]; \ + \ + LOG_VAR ("%s", g_ascii_dtostr (buffer, sizeof (buffer), \ + value)); \ + } \ + G_STMT_END + + switch (variable_info->type) + { + case VARIABLE_TYPE_BOOLEAN: + LOG_VAR ( + "%d", + variable_data->value.boolean); + break; + + case VARIABLE_TYPE_INTEGER: + LOG_VAR ( + "%d", + variable_data->value.integer); + break; + + case VARIABLE_TYPE_SIZE: + LOG_VAR ( + "%llu", + (unsigned long long) variable_data->value.size); + break; + + case VARIABLE_TYPE_SIZE_RATIO: + LOG_VAR ( + "%llu/%llu", + (unsigned long long) variable_data->value.size_ratio.antecedent, + (unsigned long long) variable_data->value.size_ratio.consequent); + break; + + case VARIABLE_TYPE_INT_RATIO: + LOG_VAR ( + "%d:%d", + variable_data->value.int_ratio.antecedent, + variable_data->value.int_ratio.consequent); + break; + + case VARIABLE_TYPE_PERCENTAGE: + LOG_VAR_FLOAT ( + variable_data->value.percentage); + break; + + case VARIABLE_TYPE_DURATION: + LOG_VAR_FLOAT ( + variable_data->value.duration); + break; + + case VARIABLE_TYPE_RATE_OF_CHANGE: + LOG_VAR_FLOAT ( + variable_data->value.rate_of_change); + break; + } + + #undef LOG_VAR + #undef LOG_VAR_FLOAT + } + else + { + gimp_dashboard_log_printf (dashboard, + "<%s />\n", + variable_info->name); + } + } + + gimp_dashboard_log_printf (dashboard, + "</vars>\n"); + } + + if (priv->log_params.backtrace) + backtrace = gimp_backtrace_new (include_current_thread); + + if (backtrace) + { + gboolean backtrace_empty = TRUE; + gint n_threads; + gint thread; + + #define BACKTRACE_NONEMPTY() \ + G_STMT_START \ + { \ + if (backtrace_empty) \ + { \ + NONEMPTY (); \ + \ + gimp_dashboard_log_printf (dashboard, \ + "<backtrace>\n"); \ + \ + backtrace_empty = FALSE; \ + } \ + } \ + G_STMT_END + + if (priv->log_backtrace) + { + n_threads = gimp_backtrace_get_n_threads (priv->log_backtrace); + + for (thread = 0; thread < n_threads; thread++) + { + guintptr thread_id; + + thread_id = gimp_backtrace_get_thread_id (priv->log_backtrace, + thread); + + if (gimp_backtrace_find_thread_by_id (backtrace, + thread_id, thread) < 0) + { + const gchar *thread_name; + + BACKTRACE_NONEMPTY (); + + thread_name = + gimp_backtrace_get_thread_name (priv->log_backtrace, + thread); + + gimp_dashboard_log_printf (dashboard, + "<thread id=\"%llu\"", + (unsigned long long) thread_id); + + if (thread_name) + { + gimp_dashboard_log_printf (dashboard, + " name=\""); + gimp_dashboard_log_print_escaped (dashboard, thread_name); + gimp_dashboard_log_printf (dashboard, + "\""); + } + + gimp_dashboard_log_printf (dashboard, + " />\n"); + } + } + } + + n_threads = gimp_backtrace_get_n_threads (backtrace); + + for (thread = 0; thread < n_threads; thread++) + { + guintptr thread_id; + const gchar *thread_name; + gint last_running = -1; + gint running; + gint last_n_frames = -1; + gint n_frames; + gint n_head = 0; + gint n_tail = 0; + gint frame; + + thread_id = gimp_backtrace_get_thread_id (backtrace, thread); + thread_name = gimp_backtrace_get_thread_name (backtrace, thread); + + running = gimp_backtrace_is_thread_running (backtrace, thread); + n_frames = gimp_backtrace_get_n_frames (backtrace, thread); + + if (priv->log_backtrace) + { + gint other_thread = gimp_backtrace_find_thread_by_id ( + priv->log_backtrace, thread_id, thread); + + if (other_thread >= 0) + { + gint n; + gint i; + + last_running = gimp_backtrace_is_thread_running ( + priv->log_backtrace, other_thread); + last_n_frames = gimp_backtrace_get_n_frames ( + priv->log_backtrace, other_thread); + + n = MIN (n_frames, last_n_frames); + + for (i = 0; i < n; i++) + { + if (gimp_backtrace_get_frame_address (backtrace, + thread, i) != + gimp_backtrace_get_frame_address (priv->log_backtrace, + other_thread, i)) + { + break; + } + } + + n_head = i; + n -= i; + + for (i = 0; i < n; i++) + { + if (gimp_backtrace_get_frame_address (backtrace, + thread, -i - 1) != + gimp_backtrace_get_frame_address (priv->log_backtrace, + other_thread, -i - 1)) + { + break; + } + } + + n_tail = i; + } + } + + if (running == last_running && + n_frames == last_n_frames && + n_head + n_tail == n_frames) + { + continue; + } + + BACKTRACE_NONEMPTY (); + + gimp_dashboard_log_printf (dashboard, + "<thread id=\"%llu\"", + (unsigned long long) thread_id); + + if (thread_name) + { + gimp_dashboard_log_printf (dashboard, + " name=\""); + gimp_dashboard_log_print_escaped (dashboard, thread_name); + gimp_dashboard_log_printf (dashboard, + "\""); + } + + gimp_dashboard_log_printf (dashboard, + " running=\"%d\"", + running); + + if (n_head > 0) + { + gimp_dashboard_log_printf (dashboard, + " head=\"%d\"", + n_head); + } + + if (n_tail > 0) + { + gimp_dashboard_log_printf (dashboard, + " tail=\"%d\"", + n_tail); + } + + if (n_frames == 0 || n_head + n_tail < n_frames) + { + gimp_dashboard_log_printf (dashboard, + ">\n"); + + for (frame = n_head; frame < n_frames - n_tail; frame++) + { + guintptr address; + + address = gimp_backtrace_get_frame_address (backtrace, + thread, frame); + + gimp_dashboard_log_printf (dashboard, + "<frame address=\"0x%llx\" />\n", + (unsigned long long) address); + + if (g_hash_table_add (priv->log_addresses, + (gpointer) address) && + priv->log_params.progressive) + { + if (! addresses) + { + addresses = g_array_new (FALSE, FALSE, + sizeof (guintptr)); + } + + g_array_append_val (addresses, address); + } + } + + gimp_dashboard_log_printf (dashboard, + "</thread>\n"); + } + else + { + gimp_dashboard_log_printf (dashboard, + " />\n"); + } + } + + if (! backtrace_empty) + { + gimp_dashboard_log_printf (dashboard, + "</backtrace>\n"); + } + + #undef BACKTRACE_NONEMPTY + } + else if (priv->log_backtrace) + { + NONEMPTY (); + + gimp_dashboard_log_printf (dashboard, + "<backtrace />\n"); + } + + gimp_backtrace_free (priv->log_backtrace); + priv->log_backtrace = backtrace; + + if (empty) + { + gimp_dashboard_log_printf (dashboard, + " />\n"); + } + else + { + gimp_dashboard_log_printf (dashboard, + "</sample>\n"); + } + + if (addresses) + { + gimp_dashboard_log_write_address_map (dashboard, + (guintptr *) addresses->data, + addresses->len, + NULL); + + g_array_free (addresses, TRUE); + } + + if (priv->log_params.progressive) + g_output_stream_flush (priv->log_output, NULL, NULL); + + #undef NONEMPTY + + priv->log_n_samples++; +} + +static void +gimp_dashboard_log_add_marker_unlocked (GimpDashboard *dashboard, + const gchar *description) +{ + GimpDashboardPrivate *priv = dashboard->priv; + + priv->log_n_markers++; + + gimp_dashboard_log_printf (dashboard, + "\n" + "<marker id=\"%d\" t=\"%lld\"", + priv->log_n_markers, + (long long) gimp_dashboard_log_time (dashboard)); + + if (description && description[0]) + { + gimp_dashboard_log_printf (dashboard, + ">\n"); + gimp_dashboard_log_print_escaped (dashboard, description); + gimp_dashboard_log_printf (dashboard, + "\n" + "</marker>\n"); + } + else + { + gimp_dashboard_log_printf (dashboard, + " />\n"); + } + + gimp_dashboard_log_update_n_markers (dashboard); +} + +static void +gimp_dashboard_log_update_highlight (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv = dashboard->priv; + + gimp_highlightable_button_set_highlight ( + priv->log_record_button, + gimp_dashboard_log_is_recording (dashboard)); +} + +static void +gimp_dashboard_log_update_n_markers (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv = dashboard->priv; + gchar buffer[32]; + + g_snprintf (buffer, sizeof (buffer), "%d", priv->log_n_markers + 1); + + gtk_label_set_text (priv->log_add_marker_label, buffer); +} + +static gint +gimp_dashboard_log_compare_addresses (gconstpointer a1, + gconstpointer a2) +{ + guintptr address1 = *(const guintptr *) a1; + guintptr address2 = *(const guintptr *) a2; + + if (address1 < address2) + return -1; + else if (address1 > address2) + return +1; + else + return 0; +} + +static void +gimp_dashboard_log_write_address_map (GimpDashboard *dashboard, + guintptr *addresses, + gint n_addresses, + GimpAsync *async) +{ + GimpBacktraceAddressInfo infos[2]; + gint i; + gint n; + + if (n_addresses == 0) + return; + + qsort (addresses, n_addresses, sizeof (guintptr), + gimp_dashboard_log_compare_addresses); + + gimp_dashboard_log_printf (dashboard, + "\n" + "<address-map>\n"); + + n = 0; + + for (i = 0; i < n_addresses; i++) + { + GimpBacktraceAddressInfo *info = &infos[n % 2]; + const GimpBacktraceAddressInfo *prev_info = &infos[(n + 1) % 2]; + + if (async && gimp_async_is_canceled (async)) + break; + + if (gimp_backtrace_get_address_info (addresses[i], info)) + { + gboolean empty = TRUE; + + #define NONEMPTY() \ + G_STMT_START \ + { \ + if (empty) \ + { \ + gimp_dashboard_log_printf (dashboard, \ + ">\n"); \ + \ + empty = FALSE; \ + } \ + } \ + G_STMT_END + + gimp_dashboard_log_printf (dashboard, + "\n" + "<address value=\"0x%llx\"", + (unsigned long long) addresses[i]); + + if (n == 0 || strcmp (info->object_name, prev_info->object_name)) + { + NONEMPTY (); + + if (info->object_name[0]) + { + gimp_dashboard_log_printf (dashboard, + "<object>"); + gimp_dashboard_log_print_escaped (dashboard, + info->object_name); + gimp_dashboard_log_printf (dashboard, + "</object>\n"); + } + else + { + gimp_dashboard_log_printf (dashboard, + "<object />\n"); + } + } + + if (n == 0 || strcmp (info->symbol_name, prev_info->symbol_name)) + { + NONEMPTY (); + + if (info->symbol_name[0]) + { + gimp_dashboard_log_printf (dashboard, + "<symbol>"); + gimp_dashboard_log_print_escaped (dashboard, + info->symbol_name); + gimp_dashboard_log_printf (dashboard, + "</symbol>\n"); + } + else + { + gimp_dashboard_log_printf (dashboard, + "<symbol />\n"); + } + } + + if (n == 0 || info->symbol_address != prev_info->symbol_address) + { + NONEMPTY (); + + if (info->symbol_address) + { + gimp_dashboard_log_printf (dashboard, + "<base>0x%llx</base>\n", + (unsigned long long) + info->symbol_address); + } + else + { + gimp_dashboard_log_printf (dashboard, + "<base />\n"); + } + } + + if (n == 0 || strcmp (info->source_file, prev_info->source_file)) + { + NONEMPTY (); + + if (info->source_file[0]) + { + gimp_dashboard_log_printf (dashboard, + "<source>"); + gimp_dashboard_log_print_escaped (dashboard, + info->source_file); + gimp_dashboard_log_printf (dashboard, + "</source>\n"); + } + else + { + gimp_dashboard_log_printf (dashboard, + "<source />\n"); + } + } + + if (n == 0 || info->source_line != prev_info->source_line) + { + NONEMPTY (); + + if (info->source_line) + { + gimp_dashboard_log_printf (dashboard, + "<line>%d</line>\n", + info->source_line); + } + else + { + gimp_dashboard_log_printf (dashboard, + "<line />\n"); + } + } + + if (empty) + { + gimp_dashboard_log_printf (dashboard, + " />\n"); + } + else + { + gimp_dashboard_log_printf (dashboard, + "</address>\n"); + } + + #undef NONEMPTY + + n++; + } + } + + gimp_dashboard_log_printf (dashboard, + "\n" + "</address-map>\n"); +} + +static void +gimp_dashboard_log_write_global_address_map (GimpAsync *async, + GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv = dashboard->priv; + gint n_addresses; + + n_addresses = g_hash_table_size (priv->log_addresses); + + if (n_addresses > 0) + { + guintptr *addresses; + GList *iter; + gint i; + + addresses = g_new (guintptr, n_addresses); + + for (iter = g_hash_table_get_keys (priv->log_addresses), i = 0; + iter; + iter = g_list_next (iter), i++) + { + addresses[i] = (guintptr) iter->data; + } + + gimp_dashboard_log_write_address_map (dashboard, + addresses, n_addresses, + async); + + g_free (addresses); + } + + gimp_async_finish (async, NULL); +} + +static void +gimp_dashboard_log_log_func (const gchar *log_domain, + GLogLevelFlags log_levels, + const gchar *message, + GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv = dashboard->priv; + const gchar *log_level = NULL; + gchar *description; + + g_mutex_lock (&priv->mutex); + + switch (log_levels & G_LOG_LEVEL_MASK) + { + case G_LOG_LEVEL_ERROR: log_level = "ERROR"; break; + case G_LOG_LEVEL_CRITICAL: log_level = "CRITICAL"; break; + case G_LOG_LEVEL_WARNING: log_level = "WARNING"; break; + case G_LOG_LEVEL_MESSAGE: log_level = "MESSAGE"; break; + case G_LOG_LEVEL_INFO: log_level = "INFO"; break; + case G_LOG_LEVEL_DEBUG: log_level = "DEBUG"; break; + default: log_level = "UNKNOWN"; break; + } + + description = g_strdup_printf ("[%s] %s: %s", log_domain, log_level, message); + + gimp_dashboard_log_add_marker_unlocked (dashboard, description); + + gimp_dashboard_log_sample (dashboard, FALSE, TRUE); + + g_free (description); + + g_mutex_unlock (&priv->mutex); +} + +static gboolean +gimp_dashboard_field_use_meter_underlay (Group group, + gint field) +{ + const GroupInfo *group_info = &groups[group]; + Variable variable = group_info->fields[field].variable; + const VariableInfo *variable_info; + + if (group_info->fields[field].meter_variable) + variable = group_info->fields[field].meter_variable; + + variable_info = &variables [variable]; + + return variable_info->type == VARIABLE_TYPE_BOOLEAN || + (group_info->fields[field].meter_variable && + variable_info->type == VARIABLE_TYPE_RATE_OF_CHANGE); +} + +static gchar * +gimp_dashboard_format_rate_of_change (const gchar *value) +{ + /* Translators: This string reports the rate of change of a measured value. + * The first "%s" is replaced by a certain quantity, usually followed by a + * unit of measurement (e.g., "10 bytes"). and the final "/s" is an + * abbreviation for "per second" (so the full string would read + * "10 bytes/s", that is, "10 bytes per second". + */ + return g_strdup_printf (_("%s/s"), value); +} + +static gchar * +gimp_dashboard_format_value (VariableType type, + gdouble value) +{ + switch (type) + { + case VARIABLE_TYPE_BOOLEAN: + return g_strdup (value ? C_("dashboard-value", "Yes") : + C_("dashboard-value", "No")); + + case VARIABLE_TYPE_INTEGER: + return g_strdup_printf ("%g", value); + + case VARIABLE_TYPE_SIZE: + return g_format_size_full (value, G_FORMAT_SIZE_IEC_UNITS); + + case VARIABLE_TYPE_SIZE_RATIO: + if (isfinite (value)) + return g_strdup_printf ("%d%%", SIGNED_ROUND (100.0 * value)); + break; + + case VARIABLE_TYPE_INT_RATIO: + if (isfinite (value)) + { + gdouble min; + gdouble max; + gdouble antecedent; + gdouble consequent; + + antecedent = value; + consequent = 1.0; + + min = MIN (ABS (antecedent), ABS (consequent)); + max = MAX (ABS (antecedent), ABS (consequent)); + + if (min) + { + antecedent /= min; + consequent /= min; + } + else + { + antecedent /= max; + consequent /= max; + } + + return g_strdup_printf ("%g:%g", + RINT (100.0 * antecedent) / 100.0, + RINT (100.0 * consequent) / 100.0); + } + else if (isinf (value)) + { + return g_strdup ("1:0"); + } + break; + + case VARIABLE_TYPE_PERCENTAGE: + return g_strdup_printf ("%d%%", SIGNED_ROUND (100.0 * value)); + + case VARIABLE_TYPE_DURATION: + return g_strdup_printf ("%02d:%02d:%04.1f", + (gint) floor (value / 3600.0), + (gint) floor (fmod (value / 60.0, 60.0)), + floor (fmod (value, 60.0) * 10.0) / 10.0); + + case VARIABLE_TYPE_RATE_OF_CHANGE: + { + gchar buf[64]; + + g_snprintf (buf, sizeof (buf), "%g", value); + + return gimp_dashboard_format_rate_of_change (buf); + } + } + + return g_strdup (_("N/A")); +} + +static void +gimp_dashboard_label_set_text (GtkLabel *label, + const gchar *text) +{ + /* the strcmp() reduces the overhead of gtk_label_set_text() when the + * text hasn't changed + */ + if (g_strcmp0 (gtk_label_get_text (label), text)) + gtk_label_set_text (label, text); +} + + +/* public functions */ + + +GtkWidget * +gimp_dashboard_new (Gimp *gimp, + GimpMenuFactory *menu_factory) +{ + GimpDashboard *dashboard; + + dashboard = g_object_new (GIMP_TYPE_DASHBOARD, + "menu-factory", menu_factory, + "menu-identifier", "<Dashboard>", + "ui-path", "/dashboard-popup", + NULL); + + dashboard->priv->gimp = gimp; + + return GTK_WIDGET (dashboard); +} + +gboolean +gimp_dashboard_log_start_recording (GimpDashboard *dashboard, + GFile *file, + const GimpDashboardLogParams *params, + GError **error) +{ + GimpDashboardPrivate *priv; + GimpUIManager *ui_manager; + GimpActionGroup *action_group; + gchar *version; + gchar **envp; + gchar **env; + GParamSpec **pspecs; + guint n_pspecs; + gboolean has_backtrace; + Variable variable; + guint i; + + g_return_val_if_fail (GIMP_IS_DASHBOARD (dashboard), FALSE); + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + priv = dashboard->priv; + + g_return_val_if_fail (! gimp_dashboard_log_is_recording (dashboard), FALSE); + + if (! params) + params = gimp_dashboard_log_get_default_params (dashboard); + + priv->log_params = *params; + + if (g_getenv ("GIMP_PERFORMANCE_LOG_SAMPLE_FREQUENCY")) + { + priv->log_params.sample_frequency = + atoi (g_getenv ("GIMP_PERFORMANCE_LOG_SAMPLE_FREQUENCY")); + } + + if (g_getenv ("GIMP_PERFORMANCE_LOG_BACKTRACE")) + { + priv->log_params.backtrace = + atoi (g_getenv ("GIMP_PERFORMANCE_LOG_BACKTRACE")) ? 1 : 0; + } + + if (g_getenv ("GIMP_PERFORMANCE_LOG_MESSAGES")) + { + priv->log_params.messages = + atoi (g_getenv ("GIMP_PERFORMANCE_LOG_MESSAGES")) ? 1 : 0; + } + + if (g_getenv ("GIMP_PERFORMANCE_LOG_PROGRESSIVE")) + { + priv->log_params.progressive = + atoi (g_getenv ("GIMP_PERFORMANCE_LOG_PROGRESSIVE")) ? 1 : 0; + } + + priv->log_params.sample_frequency = CLAMP (priv->log_params.sample_frequency, + LOG_SAMPLE_FREQUENCY_MIN, + LOG_SAMPLE_FREQUENCY_MAX); + + g_mutex_lock (&priv->mutex); + + if (priv->log_params.progressive && + g_file_query_exists (file, NULL) && + ! g_file_delete (file, NULL, error)) + { + g_mutex_unlock (&priv->mutex); + + return FALSE; + } + + priv->log_output = G_OUTPUT_STREAM (g_file_replace (file, + NULL, FALSE, G_FILE_CREATE_NONE, NULL, + error)); + + if (! priv->log_output) + { + g_mutex_unlock (&priv->mutex); + + return FALSE; + } + + priv->log_error = NULL; + priv->log_start_time = g_get_monotonic_time (); + priv->log_n_samples = 0; + priv->log_n_markers = 0; + priv->log_backtrace = NULL; + priv->log_addresses = g_hash_table_new (NULL, NULL); + + if (priv->log_params.backtrace) + has_backtrace = gimp_backtrace_start (); + else + has_backtrace = FALSE; + + gimp_dashboard_log_printf (dashboard, + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<gimp-performance-log version=\"%d\">\n", + LOG_VERSION); + + gimp_dashboard_log_printf (dashboard, + "\n" + "<params>\n" + "<sample-frequency>%d</sample-frequency>\n" + "<backtrace>%d</backtrace>\n" + "<messages>%d</messages>\n" + "<progressive>%d</progressive>\n" + "</params>\n", + priv->log_params.sample_frequency, + has_backtrace, + priv->log_params.messages, + priv->log_params.progressive); + + gimp_dashboard_log_printf (dashboard, + "\n" + "<info>\n"); + + version = gimp_version (TRUE, FALSE); + + gimp_dashboard_log_printf (dashboard, + "\n" + "<gimp-version>\n"); + gimp_dashboard_log_print_escaped (dashboard, version); + gimp_dashboard_log_printf (dashboard, + "</gimp-version>\n"); + + g_free (version); + + gimp_dashboard_log_printf (dashboard, + "\n" + "<env>\n"); + + envp = g_get_environ (); + + for (env = envp; *env; env++) + { + if (g_str_has_prefix (*env, "BABL_") || + g_str_has_prefix (*env, "GEGL_") || + g_str_has_prefix (*env, "GIMP_")) + { + gchar *delim = strchr (*env, '='); + const gchar *s; + + if (! delim) + continue; + + for (s = *env; + s != delim && (g_ascii_isalnum (*s) || *s == '_' || *s == '-'); + s++); + + if (s != delim) + continue; + + *delim = '\0'; + + gimp_dashboard_log_printf (dashboard, + "<%s>", + *env); + gimp_dashboard_log_print_escaped (dashboard, delim + 1); + gimp_dashboard_log_printf (dashboard, + "</%s>\n", + *env); + } + } + + g_strfreev (envp); + + gimp_dashboard_log_printf (dashboard, + "</env>\n"); + + gimp_dashboard_log_printf (dashboard, + "\n" + "<gegl-config>\n"); + + pspecs = g_object_class_list_properties (G_OBJECT_GET_CLASS (gegl_config ()), + &n_pspecs); + + for (i = 0; i < n_pspecs; i++) + { + const GParamSpec *pspec = pspecs[i]; + GValue value = {}; + GValue str_value = {}; + + g_value_init (&value, pspec->value_type); + g_value_init (&str_value, G_TYPE_STRING); + + g_object_get_property (G_OBJECT (gegl_config ()), pspec->name, &value); + + if (g_value_transform (&value, &str_value)) + { + gimp_dashboard_log_printf (dashboard, + "<%s>", + pspec->name); + gimp_dashboard_log_print_escaped (dashboard, + g_value_get_string (&str_value)); + gimp_dashboard_log_printf (dashboard, + "</%s>\n", + pspec->name); + } + + g_value_unset (&str_value); + g_value_unset (&value); + } + + g_free (pspecs); + + gimp_dashboard_log_printf (dashboard, + "</gegl-config>\n"); + + gimp_dashboard_log_printf (dashboard, + "\n" + "</info>\n"); + + gimp_dashboard_log_printf (dashboard, + "\n" + "<var-defs>\n"); + + for (variable = FIRST_VARIABLE; variable < N_VARIABLES; variable++) + { + const VariableInfo *variable_info = &variables[variable]; + const gchar *type = ""; + + if (variable_info->exclude_from_log) + continue; + + switch (variable_info->type) + { + case VARIABLE_TYPE_BOOLEAN: type = "boolean"; break; + case VARIABLE_TYPE_INTEGER: type = "integer"; break; + case VARIABLE_TYPE_SIZE: type = "size"; break; + case VARIABLE_TYPE_SIZE_RATIO: type = "size-ratio"; break; + case VARIABLE_TYPE_INT_RATIO: type = "int-ratio"; break; + case VARIABLE_TYPE_PERCENTAGE: type = "percentage"; break; + case VARIABLE_TYPE_DURATION: type = "duration"; break; + case VARIABLE_TYPE_RATE_OF_CHANGE: type = "rate-of-change"; break; + } + + gimp_dashboard_log_printf (dashboard, + "<var name=\"%s\" type=\"%s\" desc=\"", + variable_info->name, + type); + gimp_dashboard_log_print_escaped (dashboard, + /* intentionally untranslated */ + variable_info->description); + gimp_dashboard_log_printf (dashboard, + "\" />\n"); + } + + gimp_dashboard_log_printf (dashboard, + "</var-defs>\n"); + + gimp_dashboard_log_printf (dashboard, + "\n" + "<samples>\n"); + + if (priv->log_error) + { + GCancellable *cancellable = g_cancellable_new (); + + gimp_backtrace_stop (); + + /* Cancel the overwrite initiated by g_file_replace(). */ + g_cancellable_cancel (cancellable); + g_output_stream_close (priv->log_output, cancellable, NULL); + g_object_unref (cancellable); + + g_clear_object (&priv->log_output); + + g_propagate_error (error, priv->log_error); + priv->log_error = NULL; + + g_mutex_unlock (&priv->mutex); + + return FALSE; + } + + gimp_dashboard_reset_unlocked (dashboard); + + if (priv->log_params.messages) + { + priv->log_log_handler = gimp_log_set_handler ( + TRUE, + G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION, + (GLogFunc) gimp_dashboard_log_log_func, + dashboard); + } + + priv->update_now = TRUE; + g_cond_signal (&priv->cond); + + g_mutex_unlock (&priv->mutex); + + gimp_dashboard_log_update_n_markers (dashboard); + + ui_manager = gimp_editor_get_ui_manager (GIMP_EDITOR (dashboard)); + action_group = gimp_ui_manager_get_action_group (ui_manager, "dashboard"); + + gimp_action_group_update (action_group, dashboard); + + gimp_dashboard_log_update_highlight (dashboard); + + return TRUE; +} + +gboolean +gimp_dashboard_log_stop_recording (GimpDashboard *dashboard, + GError **error) +{ + GimpDashboardPrivate *priv; + GimpUIManager *ui_manager; + GimpActionGroup *action_group; + gboolean result = TRUE; + + g_return_val_if_fail (GIMP_IS_DASHBOARD (dashboard), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + priv = dashboard->priv; + + if (! gimp_dashboard_log_is_recording (dashboard)) + return TRUE; + + g_mutex_lock (&priv->mutex); + + if (priv->log_log_handler) + { + gimp_log_remove_handler (priv->log_log_handler); + + priv->log_log_handler = 0; + } + + gimp_dashboard_log_printf (dashboard, + "\n" + "</samples>\n"); + + + if (! priv->log_params.progressive && + g_hash_table_size (priv->log_addresses) > 0) + { + GimpAsync *async; + + async = gimp_parallel_run_async_independent ( + (GimpRunAsyncFunc) gimp_dashboard_log_write_global_address_map, + dashboard); + + gimp_wait (priv->gimp, GIMP_WAITABLE (async), + _("Resolving symbol information...")); + + g_object_unref (async); + } + + gimp_dashboard_log_printf (dashboard, + "\n" + "</gimp-performance-log>\n"); + + if (priv->log_params.backtrace) + gimp_backtrace_stop (); + + if (! priv->log_error) + { + g_output_stream_close (priv->log_output, NULL, &priv->log_error); + } + else + { + GCancellable *cancellable = g_cancellable_new (); + + /* Cancel the overwrite initiated by g_file_replace(). */ + g_cancellable_cancel (cancellable); + g_output_stream_close (priv->log_output, cancellable, NULL); + g_object_unref (cancellable); + } + + g_clear_object (&priv->log_output); + + if (priv->log_error) + { + g_propagate_error (error, priv->log_error); + priv->log_error = NULL; + + result = FALSE; + } + + g_clear_pointer (&priv->log_backtrace, gimp_backtrace_free); + g_clear_pointer (&priv->log_addresses, g_hash_table_unref); + + g_mutex_unlock (&priv->mutex); + + ui_manager = gimp_editor_get_ui_manager (GIMP_EDITOR (dashboard)); + action_group = gimp_ui_manager_get_action_group (ui_manager, "dashboard"); + + gimp_action_group_update (action_group, dashboard); + + gimp_dashboard_log_update_highlight (dashboard); + + return result; +} + +gboolean +gimp_dashboard_log_is_recording (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv; + + g_return_val_if_fail (GIMP_IS_DASHBOARD (dashboard), FALSE); + + priv = dashboard->priv; + + return priv->log_output != NULL; +} + +const GimpDashboardLogParams * +gimp_dashboard_log_get_default_params (GimpDashboard *dashboard) +{ + static const GimpDashboardLogParams default_params = + { + .sample_frequency = LOG_DEFAULT_SAMPLE_FREQUENCY, + .backtrace = LOG_DEFAULT_BACKTRACE, + .messages = LOG_DEFAULT_MESSAGES, + .progressive = LOG_DEFAULT_PROGRESSIVE + }; + + g_return_val_if_fail (GIMP_IS_DASHBOARD (dashboard), NULL); + + return &default_params; +} + +void +gimp_dashboard_log_add_marker (GimpDashboard *dashboard, + const gchar *description) +{ + GimpDashboardPrivate *priv; + + g_return_if_fail (GIMP_IS_DASHBOARD (dashboard)); + g_return_if_fail (gimp_dashboard_log_is_recording (dashboard)); + + priv = dashboard->priv; + + g_mutex_lock (&priv->mutex); + + gimp_dashboard_log_add_marker_unlocked (dashboard, description); + + g_mutex_unlock (&priv->mutex); +} + +void +gimp_dashboard_reset (GimpDashboard *dashboard) +{ + GimpDashboardPrivate *priv; + + g_return_if_fail (GIMP_IS_DASHBOARD (dashboard)); + + priv = dashboard->priv; + + g_mutex_lock (&priv->mutex); + + gimp_dashboard_reset_unlocked (dashboard); + + priv->update_now = TRUE; + g_cond_signal (&priv->cond); + + g_mutex_unlock (&priv->mutex); +} + +void +gimp_dashboard_set_update_interval (GimpDashboard *dashboard, + GimpDashboardUpdateInteval update_interval) +{ + GimpDashboardPrivate *priv; + + g_return_if_fail (GIMP_IS_DASHBOARD (dashboard)); + + priv = dashboard->priv; + + if (update_interval != priv->update_interval) + { + Group group; + + g_mutex_lock (&priv->mutex); + + priv->update_interval = update_interval; + + for (group = FIRST_GROUP; group < N_GROUPS; group++) + { + GroupData *group_data = &priv->groups[group]; + + if (group_data->meter) + { + gimp_meter_set_history_resolution (group_data->meter, + update_interval / 1000.0); + } + } + + priv->update_now = TRUE; + g_cond_signal (&priv->cond); + + g_mutex_unlock (&priv->mutex); + } +} + +GimpDashboardUpdateInteval +gimp_dashboard_get_update_interval (GimpDashboard *dashboard) +{ + g_return_val_if_fail (GIMP_IS_DASHBOARD (dashboard), DEFAULT_UPDATE_INTERVAL); + + return dashboard->priv->update_interval; +} + +void +gimp_dashboard_set_history_duration (GimpDashboard *dashboard, + GimpDashboardHistoryDuration history_duration) +{ + GimpDashboardPrivate *priv; + + g_return_if_fail (GIMP_IS_DASHBOARD (dashboard)); + + priv = dashboard->priv; + + if (history_duration != priv->history_duration) + { + Group group; + + g_mutex_lock (&priv->mutex); + + priv->history_duration = history_duration; + + for (group = FIRST_GROUP; group < N_GROUPS; group++) + { + GroupData *group_data = &priv->groups[group]; + + if (group_data->meter) + { + gimp_meter_set_history_duration (group_data->meter, + history_duration / 1000.0); + } + } + + priv->update_now = TRUE; + g_cond_signal (&priv->cond); + + g_mutex_unlock (&priv->mutex); + } +} + +GimpDashboardHistoryDuration +gimp_dashboard_get_history_duration (GimpDashboard *dashboard) +{ + g_return_val_if_fail (GIMP_IS_DASHBOARD (dashboard), DEFAULT_HISTORY_DURATION); + + return dashboard->priv->history_duration; +} + +void +gimp_dashboard_set_low_swap_space_warning (GimpDashboard *dashboard, + gboolean low_swap_space_warning) +{ + GimpDashboardPrivate *priv; + + g_return_if_fail (GIMP_IS_DASHBOARD (dashboard)); + + priv = dashboard->priv; + + if (low_swap_space_warning != priv->low_swap_space_warning) + { + g_mutex_lock (&priv->mutex); + + priv->low_swap_space_warning = low_swap_space_warning; + + g_mutex_unlock (&priv->mutex); + } +} + +gboolean +gimp_dashboard_get_low_swap_space_warning (GimpDashboard *dashboard) +{ + g_return_val_if_fail (GIMP_IS_DASHBOARD (dashboard), DEFAULT_LOW_SWAP_SPACE_WARNING); + + return dashboard->priv->low_swap_space_warning; +} + +void +gimp_dashboard_menu_setup (GimpUIManager *manager, + const gchar *ui_path) +{ + guint merge_id; + Group group; + + g_return_if_fail (GIMP_IS_UI_MANAGER (manager)); + g_return_if_fail (ui_path != NULL); + + merge_id = gimp_ui_manager_new_merge_id (manager); + + for (group = FIRST_GROUP; group < N_GROUPS; group++) + { + const GroupInfo *group_info = &groups[group]; + gchar *action_name; + gchar *action_path; + + action_name = g_strdup_printf ("dashboard-group-%s", group_info->name); + action_path = g_strdup_printf ("%s/Groups/Groups", ui_path); + + gimp_ui_manager_add_ui (manager, merge_id, + action_path, action_name, action_name, + GTK_UI_MANAGER_MENUITEM, + FALSE); + + g_free (action_name); + g_free (action_path); + } +} |