Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,39 @@ as far as open_basedir lets it check, one written before the last reboot or
on another host, one older than 26 hours, and one dated more than five
minutes in the future (a clock problem).

## PHP performance

The **PHP performance** card on the Status page explains what the PHP-FPM
and OPcache numbers above it mean for speed. It runs these checks, lists the
ones that found something first, and offers a configuration snippet where a
concrete value can be suggested:

- **PHP-FPM worker limit**: the pool reached `pm.max_children`. The suggested
value plans with 80 % of the memory available now and the average private
memory (RssAnon in `/proc/<pid>/status`) of the pool's workers. Where the
workers cannot be read (restricted mode, a system other than Linux,
open_basedir), `memory_limit` stands in as the size of every new worker,
which is an upper bound, and the card says so. When the memory has no room
for more workers, a second check says that instead.
- **PHP-FPM listen queue**: requests are waiting for a worker now, or waited
at some point since PHP-FPM started.
- **OPcache memory** (used plus wasted), **interned strings** and **script
slots** (`opcache.max_accelerated_files`): more than 90 % taken, or OPcache
restarted because it ran out of memory. OPcache rounds
`max_accelerated_files` up to one of a fixed set of prime numbers; the
check compares against that real limit and suggests one of those sizes.
- **OPcache file checks**: `opcache.validate_timestamps` is on with
`opcache.revalidate_freq` below 2 seconds. A hint, not a problem.
- **PHP memory limit**: `memory_limit` below Nextcloud's recommended 512 MB.
- **Memory limit compared with RAM**: the most PHP-FPM processes seen at once
times `memory_limit` is more than the RAM. A hint, since most requests stay
far below the limit.

PHP-FPM and OPcache state belongs to the PHP that runs the code, so the card
asks the web server for it when the page opens; nothing is collected by cron,
whose PHP would be the command line one. A check whose data is missing is
skipped with the reason, e.g. without PHP-FPM (mod_php) or with OPcache off.

## API

The API provides a lot of information about a running Nextcloud
Expand Down
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
['name' => 'database#settings', 'url' => '/database/settings', 'verb' => 'GET'],
['name' => 'database#updateSettings', 'url' => '/database/settings', 'verb' => 'PUT'],
['name' => 'database#testConnection', 'url' => '/database/test-connection', 'verb' => 'POST'],
['name' => 'php#check', 'url' => '/php/check', 'verb' => 'GET'],
['name' => 'os#status', 'url' => '/os', 'verb' => 'GET'],
['name' => 'os#refresh', 'url' => '/os/refresh', 'verb' => 'POST'],
['name' => 'os#updateSettings', 'url' => '/os/settings', 'verb' => 'PUT'],
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion css/serverinfo-main.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* extracted by css-entry-points-plugin */
@import './main-C-zeP7Uf.chunk.css';
@import './main-BLzJDnbb.chunk.css';
35 changes: 19 additions & 16 deletions js/serverinfo-main.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/serverinfo-main.mjs.map

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions lib/Controller/PhpController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\ServerInfo\Controller;

use OCA\ServerInfo\Database\Advisor;
use OCA\ServerInfo\Database\RuleResult;
use OCA\ServerInfo\Php\PhpProbe;
use OCA\ServerInfo\Php\PhpRuleSet;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\ISession;

/**
* The PHP performance checks on the Status page.
*
* Admin-only by the app framework's default: none of these methods carries
* NoAdminRequired, so the security middleware rejects everyone else.
*
* @psalm-api
*/
class PhpController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private ISession $session,
private PhpProbe $probe,
private PhpRuleSet $ruleSet,
private Advisor $advisor,
) {
parent::__construct($appName, $request);
}

/**
* Run the checks against the PHP serving this request.
*
* PHP-FPM and OPcache state is per SAPI, so this is evaluated live on
* every call rather than taken from a background job. It costs
* fpm_get_status(), opcache_get_status() and a short /proc read per
* worker.
*/
public function check(): JSONResponse {
// Released early so that the Status page's polling does not queue up
// behind this request. Nothing below writes to the session.
$this->session->close();

$snapshot = $this->probe->snapshot();

return new JSONResponse([
'sapi' => PHP_SAPI,
'pool' => $snapshot->details['fpm_pool'][0] ?? null,
'ranAt' => time(),
'results' => array_map(
static fn (RuleResult $result): array => $result->jsonSerialize(),
$this->advisor->run($this->ruleSet, $snapshot),
),
]);
}
}
24 changes: 14 additions & 10 deletions lib/Database/Advisor.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ private function evaluateRule(Rule $rule, Snapshot $snapshot, array $context): R
return new RuleResult(
$rule,
RuleResult::STATUS_SKIPPED,
skipReason: "Required value '$req' not present in this database's reports.",
skipReason: $snapshot->unavailable[$req]
?? "Required value '$req' not present in this database's reports.",
);
}
}
Expand Down Expand Up @@ -183,19 +184,22 @@ private function evaluateRule(Rule $rule, Snapshot $snapshot, array $context): R
$testCtx,
);

// Optional apply descriptor.
// Optional apply descriptor; withheld when the snapshot gives no
// value to recommend.
$apply = null;
if ($rule->apply !== null) {
try {
$recommended = $rule->apply->compute($snapshot);
$apply = [
'variable' => $rule->apply->variable,
'configKey' => $rule->apply->configKey,
'configFile' => $rule->apply->configFile,
'runtimeWritable' => $rule->apply->runtimeWritable,
'recommendedValue' => $recommended,
'note' => $rule->apply->note,
];
if ($recommended !== null) {
$apply = [
'variable' => $rule->apply->variable,
'configKey' => $rule->apply->configKey,
'configFile' => $rule->apply->configFile,
'runtimeWritable' => $rule->apply->runtimeWritable,
'recommendedValue' => $recommended,
'note' => $rule->apply->noteFor($snapshot),
];
}
} catch (\Throwable $e) {
// A broken applier shouldn't sink the whole rule.
$this->logger->warning(
Expand Down
18 changes: 12 additions & 6 deletions lib/Database/ApplyDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,29 @@
final class ApplyDescriptor {
/**
* @param string $variable Name of the GLOBAL / GUC variable.
* @param \Closure(Snapshot):string $recommendedValue Computes the suggested value from the snapshot.
* @param \Closure(Snapshot):?string $recommendedValue Computes the suggested value from the snapshot;
* null when it gives none, which leaves the failing rule without an apply.
* @param bool $runtimeWritable Whether SET GLOBAL / ALTER SYSTEM SET reload-without-restart works.
* @param string $configKey Directive name as it appears in my.cnf / postgresql.conf.
* @param string $configFile 'my.cnf' | 'postgresql.conf'
* @param string|null $note Optional human-readable caveat (e.g. "requires server restart").
* @param string $configKey Directive name as it appears in my.cnf / postgresql.conf / php.ini.
* @param string $configFile 'my.cnf' | 'postgresql.conf' | 'php.ini' | 'php-fpm.conf'
* @param string|(\Closure(Snapshot):?string)|null $note Optional human-readable caveat (e.g. "requires
* server restart"), or a closure deriving it from the snapshot.
*/
public function __construct(
public readonly string $variable,
public readonly \Closure $recommendedValue,
public readonly bool $runtimeWritable,
public readonly string $configKey,
public readonly string $configFile,
public readonly ?string $note = null,
public readonly string|\Closure|null $note = null,
) {
}

public function compute(Snapshot $snapshot): string {
public function compute(Snapshot $snapshot): ?string {
return ($this->recommendedValue)($snapshot);
}

public function noteFor(Snapshot $snapshot): ?string {
return $this->note instanceof \Closure ? ($this->note)($snapshot) : $this->note;
}
}
7 changes: 7 additions & 0 deletions lib/Database/Snapshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* Frozen view of the database's status + variables at a point in time.
*
* Built by {@see DatabaseProbe}, consumed by {@see \OCA\ServerInfo\Database\Advisor}.
* {@see \OCA\ServerInfo\Php\PhpProbe} builds one of flavour `php` for the
* PHP-FPM and OPcache rules, with everything in `derived`.
*
* Three name-spaces are merged into a single context map for the
* expression evaluator (so a rule can write `Slow_queries / Questions`
Expand All @@ -31,6 +33,7 @@ final class Snapshot {
public const FLAVOUR_MARIADB = 'mariadb';
public const FLAVOUR_PGSQL = 'pgsql';
public const FLAVOUR_UNSUPPORTED = 'unsupported';
public const FLAVOUR_PHP = 'php';

/**
* @param array<string, scalar|null> $status
Expand All @@ -42,6 +45,9 @@ final class Snapshot {
* name as that metric (e.g. the table names behind the
* `tables_without_index_use` count). Rules opt in via
* {@see \OCA\ServerInfo\Database\Rule::$detailsKey}.
* @param array<string, string> $unavailable Why a key is missing, keyed by
* that key; the Advisor shows it as the skip reason of
* a rule requiring the key.
*/
public function __construct(
public readonly string $flavour,
Expand All @@ -50,6 +56,7 @@ public function __construct(
public readonly array $variables,
public readonly array $derived,
public readonly array $details = [],
public readonly array $unavailable = [],
) {
}

Expand Down
Loading
Loading