From 6f5e4c5c2ed74d518ad75bf9d4832820afbd3209 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Wed, 18 Feb 2026 21:26:52 +0100 Subject: [PATCH 01/17] Add QueueWorkerStats class A simple class to fetch and hold queueworker stats. Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Lib/QueueWorkerStats.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Zotlabs/Lib/QueueWorkerStats.php diff --git a/Zotlabs/Lib/QueueWorkerStats.php b/Zotlabs/Lib/QueueWorkerStats.php new file mode 100644 index 000000000..f8c673509 --- /dev/null +++ b/Zotlabs/Lib/QueueWorkerStats.php @@ -0,0 +1,28 @@ + + * + * SPDX-License-Identifier: MIT + */ + +namespace Zotlabs\Lib; + +class QueueWorkerStats +{ + public readonly int $size; + public readonly int $active; + + public function __construct() { + $query = <<<'SQL' + select count(*) as total from workerq + union (select count(*) as qworkers from workerq where workerq_reservationid is not null) + SQL; + + $result = dbq('select count(*) as total from workerq'); + $this->size = !empty($result) ? $result[0]['total'] : -1; + + $result = dbq('select count(*) as qworkers from workerq where workerq_reservationid is not null'); + $this->active = !empty($result) ? $result[0]['qworkers'] : -1; + } +} From cc1713b69aa6eee119706b7ba1e1073f9b184e66 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Wed, 18 Feb 2026 21:28:31 +0100 Subject: [PATCH 02/17] Add Queue::get_undelivered() helper function Returns the number of undelivered messages in the outq. Instead of having direct database queries at various places in the code, I thought it would be better to keep them in a more logical place. Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Lib/Queue.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Zotlabs/Lib/Queue.php b/Zotlabs/Lib/Queue.php index 942a633ef..cde98b456 100644 --- a/Zotlabs/Lib/Queue.php +++ b/Zotlabs/Lib/Queue.php @@ -7,6 +7,11 @@ use Zotlabs\Zot6\Zot6Handler; class Queue { + static function get_undelivered(): int { + $r = dbq("select count(*) as total from outq where outq_delivered = 0"); + return isset($r['total']) ? $r['total'] : 0; + } + static function update($id, $add_priority = 0) { logger('queue: requeue item ' . $id,LOGGER_DEBUG); From a2ee5705f47d3c4794180b0af4e433bee8a31ce5 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Wed, 18 Feb 2026 21:32:47 +0100 Subject: [PATCH 03/17] Add System Status panel to HQ for admins Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Widget/Channel_activities.php | 28 +++++++++++++++++++++++++++ view/tpl/system_status_widget.tpl | 15 ++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 view/tpl/system_status_widget.tpl diff --git a/Zotlabs/Widget/Channel_activities.php b/Zotlabs/Widget/Channel_activities.php index c2184301e..35a28b627 100644 --- a/Zotlabs/Widget/Channel_activities.php +++ b/Zotlabs/Widget/Channel_activities.php @@ -9,6 +9,8 @@ namespace Zotlabs\Widget; use App; use Zotlabs\Lib\Apps; +use Zotlabs\Lib\Queue; +use Zotlabs\Lib\QueueWorkerStats; class Channel_activities { @@ -25,6 +27,9 @@ class Channel_activities { self::$uid = local_channel(); self::$channel = App::get_channel(); + if (is_site_admin()) { + self::get_system_status(); + } self::get_photos_activity(); self::get_files_activity(); self::get_webpages_activity(); @@ -247,5 +252,28 @@ class Channel_activities { } + private static function get_system_status(): void { + $items = []; + + if (function_exists('sys_getloadavg')) { + $items['System load'] = implode(' / ', sys_getloadavg()); + } + + $items['Output queue'] = Queue::get_undelivered(); + + $qwstats = new QueueWorkerStats(); + $items['Queue workers'] = $qwstats->active; + $items['Worker queue size'] = $qwstats->size; + + self::$activities['status'] = [ + 'label' => t('System status'), + 'icon' => 'gpu-card', + 'url' => z_root() . '/perf', + 'date' => datetime_convert(), + 'items' => $items, + 'tpl' => 'system_status_widget.tpl' + ]; + } + } diff --git a/view/tpl/system_status_widget.tpl b/view/tpl/system_status_widget.tpl new file mode 100644 index 000000000..057ec3866 --- /dev/null +++ b/view/tpl/system_status_widget.tpl @@ -0,0 +1,15 @@ + +
+
+ + {{foreach $items as $title => $item}} + + + + + {{/foreach}} +
{{$title}}:{{$item}}
+
+
From 738797467d326fc82df1695eb04341867550cdab Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 23 Feb 2026 16:52:48 +0100 Subject: [PATCH 04/17] Turn dba_driver params into attributes To allow for inspecting the params used to connect to the database, we now save the params in the dba_driver object instance as readonly attributes. An exception is made to the $pass parameter which is set to protected access, to make it slightly harder to accidentally leak it. Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- include/dba/dba_driver.php | 31 +++++++++++++++++-------------- include/dba/dba_pdo.php | 24 ++++++++++++++---------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/include/dba/dba_driver.php b/include/dba/dba_driver.php index 631513819..c121b6bb5 100644 --- a/include/dba/dba_driver.php +++ b/include/dba/dba_driver.php @@ -101,19 +101,13 @@ abstract class dba_driver { public $error = false; /** - * @brief Connect to the database. + * Connects to the database. * * This abstract function needs to be implemented in the real driver. * - * @param string $server DB server name - * @param string $scheme DB scheme - * @param string $port DB port - * @param string $user DB username - * @param string $pass DB password - * @param string $db database name * @return bool */ - abstract function connect($server, $scheme, $port, $user, $pass, $db, $db_charset); + abstract function connect(): bool; /** * @brief Perform a DB query with the SQL statement $sql. @@ -147,18 +141,27 @@ abstract class dba_driver { */ abstract function getdriver(); - function __construct($server, $scheme, $port, $user,$pass,$db,$db_charset,$install = false) { - if(($install) && (! $this->install($server, $scheme, $port, $user, $pass, $db, $db_charset))) { + function __construct( + readonly string $server, + readonly string $scheme, + readonly string $port, + readonly string $user, + protected string $pass, + readonly string $dbname, + readonly string $db_charset, + $install = false) + { + if ($install && ! $this->install()) { return; } - $this->connect($server, $scheme, $port, $user, $pass, $db, $db_charset); + $this->connect(); } function get_null_date() { return \DBA::$null_date; } - function get_install_script() { + public static function get_install_script() { $platform_name = \Zotlabs\Lib\System::get_platform_name(); if(file_exists('install/' . $platform_name . '/' . \DBA::$install_script)) return 'install/' . $platform_name . '/' . \DBA::$install_script; @@ -174,8 +177,8 @@ abstract class dba_driver { return \DBA::$utc_now; } - function install($server,$scheme,$port,$user,$pass,$db) { - if (!(strlen($server) && strlen($user))){ + function install() { + if (!strlen($this->server) && strlen($this->user)) { $this->connected = false; $this->db = null; return false; diff --git a/include/dba/dba_pdo.php b/include/dba/dba_pdo.php index 4e3d61c3d..0528d0446 100644 --- a/include/dba/dba_pdo.php +++ b/include/dba/dba_pdo.php @@ -14,31 +14,35 @@ class dba_pdo extends dba_driver { /** * {@inheritDoc} + * * @see dba_driver::connect() */ - function connect($server, $scheme, $port, $user, $pass, $db, $db_charset) { + function connect(): bool { - $this->driver_dbtype = $scheme; + $this->driver_dbtype = $this->scheme; - if(strpbrk($server,':;')) { - $dsn = $this->driver_dbtype . ':unix_socket=' . trim($server, ':;'); + if(strpbrk($this->server,':;')) { + $dsn = $this->driver_dbtype . ':unix_socket=' . trim($this->server, ':;'); } else { - $dsn = $this->driver_dbtype . ':host=' . $server . (intval($port) ? ';port=' . $port : ''); + $dsn = $this->driver_dbtype + . ':host=' + . $this->server + . (intval($this->port) ? ';port=' . $this->port : ''); } - $dsn .= ';dbname=' . $db; + $dsn .= ';dbname=' . $this->dbname; if ($this->driver_dbtype === 'mysql') { - $dsn .= ';charset=' . $db_charset; + $dsn .= ';charset=' . $this->db_charset; } else { - $dsn .= ";options='--client_encoding=" . $db_charset . "'"; + $dsn .= ";options='--client_encoding=" . $this->db_charset . "'"; } try { - $this->db = new PDO($dsn,$user,$pass); - $this->db->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION); + $this->db = new PDO($dsn, $this->user, $this->pass); + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->server_version = $this->db->getAttribute(PDO::ATTR_SERVER_VERSION); } catch(PDOException $e) { From b0b5523f2ba61b5cad55a56f1fd0b94c74a9ab2f Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 23 Feb 2026 16:58:59 +0100 Subject: [PATCH 05/17] Add perfstat module Moves collecting performance statistics for the system status activity widget to a separate module. This will make it easier to get the data from JavaScript or external monitoring tools. Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Module/Perfstats.php | 59 +++++++++++++++++++++++++++ Zotlabs/Widget/Channel_activities.php | 45 ++++++++++++-------- 2 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 Zotlabs/Module/Perfstats.php diff --git a/Zotlabs/Module/Perfstats.php b/Zotlabs/Module/Perfstats.php new file mode 100644 index 000000000..1de1088cc --- /dev/null +++ b/Zotlabs/Module/Perfstats.php @@ -0,0 +1,59 @@ + + * + * SPDX-License-Identifier: MIT + */ + +namespace Zotlabs\Module; + +use DBA; +use Zotlabs\Lib\Queue; +use Zotlabs\Lib\QueueWorkerStats; +use Zotlabs\Web\Controller; + +class Perfstats extends Controller +{ + public function init(): void { + $data = $this->getStats(); + json_return_and_die($data); + } + + private function getStats(): array { + $stats = []; + + if (function_exists('sys_getloadavg')) { + $stats['System load'] = implode(' / ', sys_getloadavg()); + } + + + // Get number of queries. + // select sum(xact_commit + xact_rollback) from pg_stat_database where datname='db'; + + $stats['Queries'] = $this->getNumQueries(); + $stats['Output queue'] = Queue::get_undelivered(); + + $qwstats = new QueueWorkerStats(); + $stats['Queue workers'] = $qwstats->active; + $stats['Worker queue size'] = $qwstats->size; + + return $stats; + } + + private function getNumQueries(): int { + static $sqlGetQps = <<<'SQL' + select sum(xact_commit + xact_rollback) as sum + from pg_stat_database + where datname='%s' + SQL; + + $result = q($sqlGetQps, DBA::$dba->dbname); + if (!empty($result)) { + return $result[0]['sum'] ?? -1; + } + + return 0; + } +} diff --git a/Zotlabs/Widget/Channel_activities.php b/Zotlabs/Widget/Channel_activities.php index 35a28b627..10555dfdc 100644 --- a/Zotlabs/Widget/Channel_activities.php +++ b/Zotlabs/Widget/Channel_activities.php @@ -253,26 +253,35 @@ class Channel_activities { } private static function get_system_status(): void { - $items = []; + $response = z_fetch_url( + z_root() . '/perfstats', + false, // binary + 0, // redirects + [ 'headers' => [ 'accept: application/json' ] ] + ); - if (function_exists('sys_getloadavg')) { - $items['System load'] = implode(' / ', sys_getloadavg()); + if ($response['success'] === true) { + $items = json_decode($response['body'], true); + //$items['debug'] = print_r($response['body'], true); + + self::$activities['status'] = [ + 'label' => t('System status'), + 'icon' => 'gpu-card', + 'url' => z_root() . '/perf', + 'date' => datetime_convert(), + 'items' => $items, + 'tpl' => 'system_status_widget.tpl' + ]; + } else { + self::$activities['status'] = [ + 'label' => t('System status'), + 'icon' => 'gpu-card', + 'url' => z_root() . '/perf', + 'date' => datetime_convert(), + 'items' => ['error' => print_r($response, true)], + 'tpl' => 'system_status_widget.tpl' + ]; } - - $items['Output queue'] = Queue::get_undelivered(); - - $qwstats = new QueueWorkerStats(); - $items['Queue workers'] = $qwstats->active; - $items['Worker queue size'] = $qwstats->size; - - self::$activities['status'] = [ - 'label' => t('System status'), - 'icon' => 'gpu-card', - 'url' => z_root() . '/perf', - 'date' => datetime_convert(), - 'items' => $items, - 'tpl' => 'system_status_widget.tpl' - ]; } } From 283b606c0919e03209872b2c1febd8b9cc74b401 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 23 Feb 2026 18:14:04 +0100 Subject: [PATCH 06/17] Update system status activity widget periodically Adds a bit of javascript that requests the performance stats every 5 seconds. Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Module/Perfstats.php | 14 +++++--------- view/tpl/system_status_widget.tpl | 27 +++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Zotlabs/Module/Perfstats.php b/Zotlabs/Module/Perfstats.php index 1de1088cc..f5e6b50e5 100644 --- a/Zotlabs/Module/Perfstats.php +++ b/Zotlabs/Module/Perfstats.php @@ -25,19 +25,15 @@ class Perfstats extends Controller $stats = []; if (function_exists('sys_getloadavg')) { - $stats['System load'] = implode(' / ', sys_getloadavg()); + $stats['loadavg'] = implode(' / ', sys_getloadavg()); } - - // Get number of queries. - // select sum(xact_commit + xact_rollback) from pg_stat_database where datname='db'; - - $stats['Queries'] = $this->getNumQueries(); - $stats['Output queue'] = Queue::get_undelivered(); + $stats['dbqueries'] = $this->getNumQueries(); + $stats['outqueue'] = Queue::get_undelivered(); $qwstats = new QueueWorkerStats(); - $stats['Queue workers'] = $qwstats->active; - $stats['Worker queue size'] = $qwstats->size; + $stats['queueworkers'] = $qwstats->active; + $stats['workqsz'] = $qwstats->size; return $stats; } diff --git a/view/tpl/system_status_widget.tpl b/view/tpl/system_status_widget.tpl index 057ec3866..2182c1974 100644 --- a/view/tpl/system_status_widget.tpl +++ b/view/tpl/system_status_widget.tpl @@ -1,15 +1,34 @@
- {{foreach $items as $title => $item}} + {{foreach $items as $id => $item}} - - + + {{/foreach}}
{{$title}}:{{$item}}{{$id|escape}}:{{$item|escape}}
+ From 492533729d226ee97c1fd7f3d4c154b34c4bc983 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 23 Feb 2026 18:25:40 +0100 Subject: [PATCH 07/17] Human labels for system status widget Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Widget/Channel_activities.php | 12 ++++++++++-- view/tpl/system_status_widget.tpl | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Zotlabs/Widget/Channel_activities.php b/Zotlabs/Widget/Channel_activities.php index 10555dfdc..704cbe2a6 100644 --- a/Zotlabs/Widget/Channel_activities.php +++ b/Zotlabs/Widget/Channel_activities.php @@ -54,7 +54,8 @@ class Channel_activities { '$url' => $a['url'], '$icon' => $a['icon'], '$label' => $a['label'], - '$items' => $a['items'] + '$items' => $a['items'], + '$labels' => $a['labels'] ?? [], ] ); } @@ -270,7 +271,14 @@ class Channel_activities { 'url' => z_root() . '/perf', 'date' => datetime_convert(), 'items' => $items, - 'tpl' => 'system_status_widget.tpl' + 'tpl' => 'system_status_widget.tpl', + 'labels' => [ + 'loadavg' => t('Load average'), + 'dbqueries' => t('DB queries'), + 'outqueue' => t('Output queue'), + 'queueworkers' => t('Queue workers'), + 'workqsz' => t('Work queue size'), + ], ]; } else { self::$activities['status'] = [ diff --git a/view/tpl/system_status_widget.tpl b/view/tpl/system_status_widget.tpl index 2182c1974..0c07b3441 100644 --- a/view/tpl/system_status_widget.tpl +++ b/view/tpl/system_status_widget.tpl @@ -6,7 +6,7 @@ {{foreach $items as $id => $item}} - + {{/foreach}} From 82bd91d9d7e8586a5db8294a32bbb9db0dacbbd4 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 23 Feb 2026 20:06:12 +0100 Subject: [PATCH 08/17] Log error if system status can't load Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Widget/Channel_activities.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Zotlabs/Widget/Channel_activities.php b/Zotlabs/Widget/Channel_activities.php index 704cbe2a6..db430ba8b 100644 --- a/Zotlabs/Widget/Channel_activities.php +++ b/Zotlabs/Widget/Channel_activities.php @@ -281,14 +281,7 @@ class Channel_activities { ], ]; } else { - self::$activities['status'] = [ - 'label' => t('System status'), - 'icon' => 'gpu-card', - 'url' => z_root() . '/perf', - 'date' => datetime_convert(), - 'items' => ['error' => print_r($response, true)], - 'tpl' => 'system_status_widget.tpl' - ]; + logger("fetching perfstats failed: {$response['return_code']}", LOGGER_NORMAL, LOG_ERR); } } From 04d44c9965ec162b68f8d7cfa9e8f01e99edd610 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 23 Feb 2026 20:07:18 +0100 Subject: [PATCH 09/17] Remove link from system status widget title At least for now, there's no sensible link target, so it's better to not link anywhere. Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Widget/Channel_activities.php | 3 +-- view/tpl/system_status_widget.tpl | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Zotlabs/Widget/Channel_activities.php b/Zotlabs/Widget/Channel_activities.php index db430ba8b..03d5123c8 100644 --- a/Zotlabs/Widget/Channel_activities.php +++ b/Zotlabs/Widget/Channel_activities.php @@ -51,7 +51,7 @@ class Channel_activities { $activity_html .= replace_macros( get_markup_template($a['tpl']), [ - '$url' => $a['url'], + '$url' => $a['url'] ?? null, '$icon' => $a['icon'], '$label' => $a['label'], '$items' => $a['items'], @@ -268,7 +268,6 @@ class Channel_activities { self::$activities['status'] = [ 'label' => t('System status'), 'icon' => 'gpu-card', - 'url' => z_root() . '/perf', 'date' => datetime_convert(), 'items' => $items, 'tpl' => 'system_status_widget.tpl', diff --git a/view/tpl/system_status_widget.tpl b/view/tpl/system_status_widget.tpl index 0c07b3441..4da43bd24 100644 --- a/view/tpl/system_status_widget.tpl +++ b/view/tpl/system_status_widget.tpl @@ -1,7 +1,7 @@
- {{$label|escape}} + {{$label|escape}}
-
+
{{$id|escape}}:{{$labels.$id|escape}}: {{$item|escape}}
{{foreach $items as $id => $item}} From 3d3580b23fa0a4a4b0dec7192cee95f4ab190007 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 23 Feb 2026 21:05:27 +0100 Subject: [PATCH 10/17] Validate requests to perfstats Make some stricter rules for accessing the perfstats module. We only want to respond to GET request for json data made by a site admin. This means we also need to pass along the credentials in the request. Didn't immediately figure out how to do that using `z_fetch_url`, so we just fall back to using the JavaScript to populate the widget. This makes it idle for about 5 sec before it gets the first data sample. I think that's probably OK. Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Module/Perfstats.php | 33 ++++++++++++++++++ Zotlabs/Widget/Channel_activities.php | 48 +++++++++++---------------- view/tpl/system_status_widget.tpl | 3 +- 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/Zotlabs/Module/Perfstats.php b/Zotlabs/Module/Perfstats.php index f5e6b50e5..8709038db 100644 --- a/Zotlabs/Module/Perfstats.php +++ b/Zotlabs/Module/Perfstats.php @@ -14,9 +14,37 @@ use Zotlabs\Lib\Queue; use Zotlabs\Lib\QueueWorkerStats; use Zotlabs\Web\Controller; +/** + * Controller for the `/perfstats` module. + * + * Collects various performance stats for the site, and reponds with the stats + * as a json array. + */ class Perfstats extends Controller { public function init(): void { + // + // We only accept GET requests + // + if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + http_status_exit(400, 'Unsupported method'); + } + + // + // We only accept json requests + // + if (getBestSupportedMimeType(['application/json']) === null) { + http_status_exit(400, 'No supported format'); + } + + // + // Only admins should be given access + // + if (!is_site_admin()) { + http_status(401, 'Access denied'); + json_return_and_die(['error' => 'access denied']); + } + $data = $this->getStats(); json_return_and_die($data); } @@ -52,4 +80,9 @@ class Perfstats extends Controller return 0; } + + private function requireJson(): void { + if (getBestSupportedMimeTypes(['application/json']) === null) { + } + } } diff --git a/Zotlabs/Widget/Channel_activities.php b/Zotlabs/Widget/Channel_activities.php index 03d5123c8..af94d6235 100644 --- a/Zotlabs/Widget/Channel_activities.php +++ b/Zotlabs/Widget/Channel_activities.php @@ -254,34 +254,26 @@ class Channel_activities { } private static function get_system_status(): void { - $response = z_fetch_url( - z_root() . '/perfstats', - false, // binary - 0, // redirects - [ 'headers' => [ 'accept: application/json' ] ] - ); - - if ($response['success'] === true) { - $items = json_decode($response['body'], true); - //$items['debug'] = print_r($response['body'], true); - - self::$activities['status'] = [ - 'label' => t('System status'), - 'icon' => 'gpu-card', - 'date' => datetime_convert(), - 'items' => $items, - 'tpl' => 'system_status_widget.tpl', - 'labels' => [ - 'loadavg' => t('Load average'), - 'dbqueries' => t('DB queries'), - 'outqueue' => t('Output queue'), - 'queueworkers' => t('Queue workers'), - 'workqsz' => t('Work queue size'), - ], - ]; - } else { - logger("fetching perfstats failed: {$response['return_code']}", LOGGER_NORMAL, LOG_ERR); - } + self::$activities['status'] = [ + 'label' => t('System status'), + 'icon' => 'gpu-card', + 'date' => datetime_convert(), + 'items' => [ + 'loadavg' => '0 / 0 / 0', + 'dbqueries' => 0, + 'outqueue' => 0, + 'queueworkers' => 0, + 'workqsz' => 0, + ], + 'tpl' => 'system_status_widget.tpl', + 'labels' => [ + 'loadavg' => t('Load average'), + 'dbqueries' => t('DB queries'), + 'outqueue' => t('Output queue'), + 'queueworkers' => t('Queue workers'), + 'workqsz' => t('Work queue size'), + ], + ]; } } diff --git a/view/tpl/system_status_widget.tpl b/view/tpl/system_status_widget.tpl index 4da43bd24..b36455e9a 100644 --- a/view/tpl/system_status_widget.tpl +++ b/view/tpl/system_status_widget.tpl @@ -18,7 +18,8 @@ setInterval(() => { fetch('/perfstats', { headers: { "Accept": "application/json", - } + }, + credentials: "include", }) .then((response) => response.json()) .then((json) => { From db5e92b72d390d004f7345eab16e2c9a0451fb60 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 23 Feb 2026 21:34:04 +0100 Subject: [PATCH 11/17] Remove unused/incomplete function Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Module/Perfstats.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Zotlabs/Module/Perfstats.php b/Zotlabs/Module/Perfstats.php index 8709038db..f5d6b9c8a 100644 --- a/Zotlabs/Module/Perfstats.php +++ b/Zotlabs/Module/Perfstats.php @@ -80,9 +80,4 @@ class Perfstats extends Controller return 0; } - - private function requireJson(): void { - if (getBestSupportedMimeTypes(['application/json']) === null) { - } - } } From ccd6d1a38c4c75b0541926a8ed851a7014cf6f91 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Thu, 26 Feb 2026 16:19:17 +0100 Subject: [PATCH 12/17] Move db stats to separate classes for each db type Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Lib/DbStats.php | 43 +++++++++++++++++++++++++++++++++ Zotlabs/Lib/MySQLDbStats.php | 37 ++++++++++++++++++++++++++++ Zotlabs/Lib/PostgresDbStats.php | 32 ++++++++++++++++++++++++ Zotlabs/Module/Perfstats.php | 15 +++--------- tests/unit/Lib/DbStatsTest.php | 41 +++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 12 deletions(-) create mode 100644 Zotlabs/Lib/DbStats.php create mode 100644 Zotlabs/Lib/MySQLDbStats.php create mode 100644 Zotlabs/Lib/PostgresDbStats.php create mode 100644 tests/unit/Lib/DbStatsTest.php diff --git a/Zotlabs/Lib/DbStats.php b/Zotlabs/Lib/DbStats.php new file mode 100644 index 000000000..83186966b --- /dev/null +++ b/Zotlabs/Lib/DbStats.php @@ -0,0 +1,43 @@ + + * + * SPDX-License-Identifier: MIT + */ + +namespace Zotlabs\Lib; + +use DBA; + +/** + * Abstract class to obtain statistics from the database. + * + * This class should not be instantiated on it's own, but you can get + * a concrete class for the configured database type of this site by + * calling the `DbStats::getStats()` function. + */ +abstract class DbStats { + + /** + * Get an object for getting statistics from the database. + * + * @return DbStats The concrete class for obtaining the statistics from + * this instances database. + */ + public static function getStats(): DbStats { + return DBA::$dba->is_postgres() + ? new PostgresDbStats() + : new MySQLDbStats(); + } + + /** + * Return the number of queries recorded by the database. + * + * @return int Number of queries. + */ + public abstract function getQueries(): int; + + // Prevent instantiation of this class + private function __construct() {} +} diff --git a/Zotlabs/Lib/MySQLDbStats.php b/Zotlabs/Lib/MySQLDbStats.php new file mode 100644 index 000000000..c7376e37f --- /dev/null +++ b/Zotlabs/Lib/MySQLDbStats.php @@ -0,0 +1,37 @@ + + * + * SPDX-License-Identifier: MIT + */ + +namespace Zotlabs\Lib; + +use DBA; +use PDO; + +/** + * Concrete implementation for getting stats from MySQL and MariaDB databases. + */ +class MySQLDbStats extends DbStats { + + public function getQueries(): int { + // + // We can't use the regular Hubzilla db helper function here, as + // it will only return information from a `SELECT` statement. + // + // Use the basic PDO access instead. + // + $query = DBA::$dba->db->prepare('SHOW STATUS LIKE "Queries"'); + $query->execute(); + + $result = $query->fetch(PDO::FETCH_ASSOC); + logger(print_r($result, true)); + if (!empty($result)) { + return $result['Value'] ?? -1; + } + + return 0; + } +} diff --git a/Zotlabs/Lib/PostgresDbStats.php b/Zotlabs/Lib/PostgresDbStats.php new file mode 100644 index 000000000..97dc4a6ec --- /dev/null +++ b/Zotlabs/Lib/PostgresDbStats.php @@ -0,0 +1,32 @@ + + * + * SPDX-License-Identifier: MIT + */ + +namespace Zotlabs\Lib; + +use DBA; + +/** + * Concrete implementation for getting stats from PostgreSQL databases. + */ +class PostgresDbStats extends DbStats { + + public function getQueries(): int { + $sqlGetQps = <<<'SQL' + select (xact_commit + xact_rollback) as queries + from pg_stat_database + where datname='%s' + SQL; + + $result = q($sqlGetQps, DBA::$dba->dbname); + if (!empty($result)) { + return $result[0]['queries'] ?? -1; + } + + return 0; + } +} diff --git a/Zotlabs/Module/Perfstats.php b/Zotlabs/Module/Perfstats.php index f5d6b9c8a..afc9adea4 100644 --- a/Zotlabs/Module/Perfstats.php +++ b/Zotlabs/Module/Perfstats.php @@ -10,6 +10,7 @@ namespace Zotlabs\Module; use DBA; +use Zotlabs\Lib\DbStats; use Zotlabs\Lib\Queue; use Zotlabs\Lib\QueueWorkerStats; use Zotlabs\Web\Controller; @@ -67,17 +68,7 @@ class Perfstats extends Controller } private function getNumQueries(): int { - static $sqlGetQps = <<<'SQL' - select sum(xact_commit + xact_rollback) as sum - from pg_stat_database - where datname='%s' - SQL; - - $result = q($sqlGetQps, DBA::$dba->dbname); - if (!empty($result)) { - return $result[0]['sum'] ?? -1; - } - - return 0; + $stats = DbStats::getStats(); + return $stats->getQueries(); } } diff --git a/tests/unit/Lib/DbStatsTest.php b/tests/unit/Lib/DbStatsTest.php new file mode 100644 index 000000000..beb522a46 --- /dev/null +++ b/tests/unit/Lib/DbStatsTest.php @@ -0,0 +1,41 @@ + + * + * SPDX-License-Identifier: MIT + */ + +namespace Zotlabs\Tests\Unit\Lib; + +use DBA; +use Zotlabs\Lib\DbStats; +use Zotlabs\Tests\Unit\UnitTestCase; + +class DbStatsTest extends UnitTestCase +{ + public function testGetQueries(): void { + $stats = DbStats::getStats(); + $this->assertNotNull($stats); + $this->assertInstanceOf(DbStats::class, $stats); + + $numQueries = $stats->getQueries(); + $this->assertNotEquals(0, $numQueries); + + if (!DBA::$dba->is_postgres()) { + // + // Postgres will only update the stats once the transaction + // is committed or rolled back. As we wrap the tests in a + // transaction to begin with, the stats won't change here, + // so we skip this test on Postgres. + // + dbq("select * from account"); + dbq("select * from channel"); + dbq("select * from xchan"); + + $numMoreQueries = $stats->getQueries(); + $this->assertGreaterThan($numQueries, $numMoreQueries); + } + } +} From 78e30a4d3275f1ca77abb1b27a5f0bf7ee93b8b0 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Thu, 26 Feb 2026 16:28:59 +0100 Subject: [PATCH 13/17] Simplify stats for getting size of output queue Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Lib/Queue.php | 15 ++++++++++++--- Zotlabs/Module/Perfstats.php | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Zotlabs/Lib/Queue.php b/Zotlabs/Lib/Queue.php index cde98b456..208239264 100644 --- a/Zotlabs/Lib/Queue.php +++ b/Zotlabs/Lib/Queue.php @@ -7,9 +7,18 @@ use Zotlabs\Zot6\Zot6Handler; class Queue { - static function get_undelivered(): int { - $r = dbq("select count(*) as total from outq where outq_delivered = 0"); - return isset($r['total']) ? $r['total'] : 0; + /** + * Get number of entries in the out queue. + * + * When delivery is successful, the item is removed from the out queue, so + * the number of items in the queue reflects the number of pending delivery + * attempts. + * + * @return int Number of items in the out queue. + */ + static function count(): int { + $r = dbq('select count(*) as total from outq'); + return $r['total'] ?? 0; } static function update($id, $add_priority = 0) { diff --git a/Zotlabs/Module/Perfstats.php b/Zotlabs/Module/Perfstats.php index afc9adea4..cea08bde3 100644 --- a/Zotlabs/Module/Perfstats.php +++ b/Zotlabs/Module/Perfstats.php @@ -58,7 +58,7 @@ class Perfstats extends Controller } $stats['dbqueries'] = $this->getNumQueries(); - $stats['outqueue'] = Queue::get_undelivered(); + $stats['outqueue'] = Queue::count(); $qwstats = new QueueWorkerStats(); $stats['queueworkers'] = $qwstats->active; From 91944da69edabf48d1a6430eb2baffc00f2734a6 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Sun, 1 Mar 2026 11:16:05 +0100 Subject: [PATCH 14/17] Report queries/sec in system status widget The total number of queries is not that interesting for performance measurements, so let's do queries pr/second instead. Adds a timestamp column to the dataset, which could be useful for other purposes as well (like making a graph over time etc.) Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Module/Perfstats.php | 5 +++ Zotlabs/Widget/Channel_activities.php | 3 +- view/tpl/system_status_widget.tpl | 51 ++++++++++++++++++--------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/Zotlabs/Module/Perfstats.php b/Zotlabs/Module/Perfstats.php index cea08bde3..3ccd1bbc0 100644 --- a/Zotlabs/Module/Perfstats.php +++ b/Zotlabs/Module/Perfstats.php @@ -64,6 +64,11 @@ class Perfstats extends Controller $stats['queueworkers'] = $qwstats->active; $stats['workqsz'] = $qwstats->size; + // Return a timestamp, so that it is possible to infer + // changes of the stats over time. A resolution of + // seconds should be good enough for our purposes. + $stats['ts'] = time(); + return $stats; } diff --git a/Zotlabs/Widget/Channel_activities.php b/Zotlabs/Widget/Channel_activities.php index af94d6235..fde46d1dd 100644 --- a/Zotlabs/Widget/Channel_activities.php +++ b/Zotlabs/Widget/Channel_activities.php @@ -264,11 +264,12 @@ class Channel_activities { 'outqueue' => 0, 'queueworkers' => 0, 'workqsz' => 0, + 'ts' => time(), ], 'tpl' => 'system_status_widget.tpl', 'labels' => [ 'loadavg' => t('Load average'), - 'dbqueries' => t('DB queries'), + 'dbqueries' => t('DB queries/sec'), 'outqueue' => t('Output queue'), 'queueworkers' => t('Queue workers'), 'workqsz' => t('Work queue size'), diff --git a/view/tpl/system_status_widget.tpl b/view/tpl/system_status_widget.tpl index b36455e9a..0a129a34b 100644 --- a/view/tpl/system_status_widget.tpl +++ b/view/tpl/system_status_widget.tpl @@ -5,31 +5,48 @@
{{foreach $items as $id => $item}} + {{if $id != 'ts'}} + {{/if}} {{/foreach}}
{{$labels.$id|escape}}: {{$item|escape}}
From 52a2a0d89a2ddad52b5f6ce599e8d240c5722704 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Sun, 1 Mar 2026 11:34:28 +0100 Subject: [PATCH 15/17] Pass loadavg as array from Perfstats There's no reason we should format the data into a string in the Perfstats module. Let the recipient do what they want with it instead. As an example, we reduce the precision of the loadavg stats in the system status widget. 3 digits precision should be more than enough for this type of status display. Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- Zotlabs/Module/Perfstats.php | 2 +- view/tpl/system_status_widget.tpl | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Zotlabs/Module/Perfstats.php b/Zotlabs/Module/Perfstats.php index 3ccd1bbc0..a38173859 100644 --- a/Zotlabs/Module/Perfstats.php +++ b/Zotlabs/Module/Perfstats.php @@ -54,7 +54,7 @@ class Perfstats extends Controller $stats = []; if (function_exists('sys_getloadavg')) { - $stats['loadavg'] = implode(' / ', sys_getloadavg()); + $stats['loadavg'] = sys_getloadavg(); } $stats['dbqueries'] = $this->getNumQueries(); diff --git a/view/tpl/system_status_widget.tpl b/view/tpl/system_status_widget.tpl index 0a129a34b..26a4003dd 100644 --- a/view/tpl/system_status_widget.tpl +++ b/view/tpl/system_status_widget.tpl @@ -31,7 +31,11 @@ for (const item in json) { element = document.getElementById(`perfstat-${item}-value`); if (element) { - if (item === "dbqueries") { + if (item === "loadavg") { + element.innerText = json['loadavg'] + .map((v) => v.toPrecision(3)) + .join(" / "); + } else if (item === "dbqueries") { console.log(`dbqueries = ${json['dbqueries']}, ts = ${json['ts']}`); if (status_update_ts !== 0) { let dt = json['ts'] - status_update_ts; From ba24958b3775384135d30aba0a7d957919c1e57b Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Sun, 1 Mar 2026 11:38:07 +0100 Subject: [PATCH 16/17] Remove some debug logging in system status widget Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- view/tpl/system_status_widget.tpl | 1 - 1 file changed, 1 deletion(-) diff --git a/view/tpl/system_status_widget.tpl b/view/tpl/system_status_widget.tpl index 26a4003dd..4381b9f08 100644 --- a/view/tpl/system_status_widget.tpl +++ b/view/tpl/system_status_widget.tpl @@ -36,7 +36,6 @@ .map((v) => v.toPrecision(3)) .join(" / "); } else if (item === "dbqueries") { - console.log(`dbqueries = ${json['dbqueries']}, ts = ${json['ts']}`); if (status_update_ts !== 0) { let dt = json['ts'] - status_update_ts; let dq = json['dbqueries'] - status_update_last_q; From 1897cd0b1b1cffd88bc274a5be4b5d78b2bf6f17 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Sun, 1 Mar 2026 11:47:39 +0100 Subject: [PATCH 17/17] Make system status widget a bit more snappy Fetch the initial data when the widget loads, and then update every five sec after that. Also better indication of uninitialized values. Project......: Performance Profiling Sponsored-by.: NLnet NGI0 Commons Fund --- view/tpl/system_status_widget.tpl | 73 +++++++++++++++++-------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/view/tpl/system_status_widget.tpl b/view/tpl/system_status_widget.tpl index 4381b9f08..c618e56ac 100644 --- a/view/tpl/system_status_widget.tpl +++ b/view/tpl/system_status_widget.tpl @@ -8,7 +8,7 @@ {{if $id != 'ts'}} {{$labels.$id|escape}}: - {{$item|escape}} + … {{/if}} {{/foreach}} @@ -16,40 +16,49 @@