diff --git a/assets/common.css/main.css b/assets/common.css/main.css index d9bccaf6..c37e59fc 100644 --- a/assets/common.css/main.css +++ b/assets/common.css/main.css @@ -22,3 +22,4 @@ html, body { } @include loading.css; +@include perf.css; diff --git a/assets/common.css/perf.css b/assets/common.css/perf.css new file mode 100644 index 00000000..9756a1c6 --- /dev/null +++ b/assets/common.css/perf.css @@ -0,0 +1,100 @@ +.msz-perfs { + position: fixed; + bottom: 4px; + left: 4px; + display: flex; + gap: 2px; + flex-direction: column-reverse; + align-items: flex-start; + opacity: .5; +} +.msz-perfs-right { + left: initial; + right: 4px; +} +.msz-perfs:hover { + opacity: 1; +} + +.msz-perf { + background-color: #111d; + color: #fff; + font-family: monospace; + font-size: 12px; + line-height: 20px; + padding: 4px 8px; + border-radius: 6px; +} +.msz-perfs:hover .msz-perf { + backdrop-filter: blur(10px); +} + +.msz-perf-number { + color: #fff; +} +.msz-perf-unit { + color: #888; +} + +.msz-perf-header { + display: flex; + flex: 0 0 auto; + gap: 4px; +} +.msz-perf:hover .msz-perf-header { + border-bottom: 1px solid #888; + margin-bottom: 2px; +} + +.msz-perf-type { + flex: 0 0 auto; + font-weight: 700; + min-width: 60px; +} +.msz-perf-type-navigation { + color: #f0f; +} +.msz-perf-type-other { + color: #0ff; +} + +.msz-perf-target { + flex: 1 0 auto; + min-width: 200px; +} +.msz-perf-target-host, +.msz-perf-target-path, +.msz-perf-target-query { + display: inline-block; +} +.msz-perf-target-host, +.msz-perf-target-query { + color: #888; +} + +.msz-perf-total { + flex: 0 0 auto; + min-width: 80px; + text-align: right; +} + +.msz-perf-timings { + display: none; + border-collapse: collapse; + width: 100%; +} +.msz-perf:hover .msz-perf-timings { + display: table; +} + +.msz-perf-timing-name { + font-weight: 700; + min-width: 60px; +} +.msz-perf-timing-comment { + color: #888; +} +.msz-perf-timing-duration { + min-width: 80px; + text-align: right; +} diff --git a/assets/common.js/main.js b/assets/common.js/main.js index e1c805b6..ef40e85e 100644 --- a/assets/common.js/main.js +++ b/assets/common.js/main.js @@ -2,6 +2,7 @@ #include csrf.js #include html.js #include meta.js +#include perf.jsx #include uniqstr.js #include xhr.js diff --git a/assets/common.js/perf.jsx b/assets/common.js/perf.jsx new file mode 100644 index 00000000..50edc3a5 --- /dev/null +++ b/assets/common.js/perf.jsx @@ -0,0 +1,72 @@ +#include html.js + +(() => { + const perfs = <div class="msz-perfs"/>; + perfs.ondblclick = () => { + perfs.classList.toggle('msz-perfs-right'); + }; + + const appendReal = elem => { + $appendChild(perfs, elem); + }; + + let append = elem => { + append = appendReal; + $appendChild(document.body, perfs); + appendReal(elem); + }; + + (new PerformanceObserver(list => { + for(const entry of list.getEntries()) { + if(entry.serverTiming.length < 1) + break; + + const url = new URL(entry.name); + + let total = 0; + let queries = -1; + const timings = <table class="msz-perf-timings"/>; + for(const timing of entry.serverTiming) { + if(timing.name === 'msz-queries') { + queries = Math.ceil(timing.duration); + continue; + } + + total += timing.duration; + $appendChild(timings, <tr class="msz-perf-timing"> + <td class="msz-perf-timing-name">{timing.name}</td> + <td class="msz-perf-timing-comment">{decodeURIComponent(timing.description)}</td> + <td class="msz-perf-timing-duration"> + <span class="msz-perf-number">{timing.duration}</span> + <span class="msz-perf-unit">ms</span> + </td> + </tr>); + } + + append(<div class="msz-perf"> + <div class="msz-perf-header"> + <div class={`msz-perf-type msz-perf-type-${entry instanceof PerformanceNavigationTiming ? 'navigation' : 'other'}`}> + {entry instanceof PerformanceNavigationTiming ? entry.type : ( + entry.initiatorType === 'xmlhttprequest' ? 'xhr' : entry.initiatorType + )} + </div> + <div class="msz-perf-target"> + {url.host !== location.host ? <div class="msz-perf-target-host">{url.host}</div> : null} + <div class="msz-perf-target-path">{url.pathname}</div> + {url.search !== '' ? <div class="msz-perf-target-query">{url.search}</div> : null} + </div> + {queries > 0 ? <div class="msz-perf-total"> + <span class="msz-perf-number">{queries}</span> + {' '} + <span class="msz-perf-unit">{queries === 1 ? 'query' : 'queries'}</span> + </div> : null} + <div class="msz-perf-total"> + <span class="msz-perf-number">{total.toFixed(5)}</span> + <span class="msz-perf-unit">ms</span> + </div> + </div> + {timings} + </div>); + } + })).observe({ entryTypes: ['navigation', 'resource'] }); +})(); diff --git a/composer.lock b/composer.lock index b6a42d3b..2385af28 100644 --- a/composer.lock +++ b/composer.lock @@ -501,11 +501,11 @@ }, { "name": "flashwave/index", - "version": "v0.2504.21040", + "version": "v0.2504.31541", "source": { "type": "git", "url": "https://patchii.net/flash/index.git", - "reference": "42adbe5da8325d66a996e5d69dbb01def80743fc" + "reference": "c2bcb0611bee08a9a21930b92c96cc85304562b6" }, "require": { "ext-mbstring": "*", @@ -554,7 +554,7 @@ ], "description": "Composer package for the common library for my projects.", "homepage": "https://railgun.sh/index", - "time": "2025-04-02T10:41:10+00:00" + "time": "2025-04-03T15:42:01+00:00" }, { "name": "graham-campbell/result-type", diff --git a/misuzu.php b/misuzu.php index 0211a960..afece484 100644 --- a/misuzu.php +++ b/misuzu.php @@ -1,31 +1,41 @@ <?php namespace Misuzu; -define('MSZ_STARTUP', microtime(true)); +define('MSZ_STARTUP', hrtime(true)); define('MSZ_ROOT', __DIR__); require_once __DIR__ . '/vendor/autoload.php'; -\Dotenv\Dotenv::createImmutable(Misuzu::PATH_ROOT)->load(); +$msz = (function() { + $timings = new \Index\Performance\Timings( + new \Index\Performance\Stopwatch(MSZ_STARTUP), + ); -if(!empty($_ENV['SENTRY_DSN'])) - \Sentry\init(['dsn' => $_ENV['SENTRY_DSN']]); + try { + \Dotenv\Dotenv::createImmutable(Misuzu::PATH_ROOT)->load(); -(function(\Whoops\RunInterface $whoops) { - if(Misuzu::cli()) - $whoops->pushHandler(new Whoops\SentryPlainTextHandler); - elseif(Misuzu::debug()) - $whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler); - else - $whoops->pushHandler(new Whoops\SentryPageHandler); + if(!empty($_ENV['SENTRY_DSN'])) + \Sentry\init(['dsn' => $_ENV['SENTRY_DSN']]); - $whoops->register(); -})(new \Whoops\Run); + $whoops = new \Whoops\Run; + if(Misuzu::cli()) + $whoops->pushHandler(new Whoops\SentryPlainTextHandler); + elseif(Misuzu::debug()) + $whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler); + else + $whoops->pushHandler(new Whoops\SentryPageHandler); -$msz = new MisuzuContext( - $_ENV['DATABASE_DSN'] ?? 'null:', - $_ENV['DOMAIN_ROLES'] ?? 'localhost=main,redirect,storage', - $_ENV['STORAGE_PATH_LOCAL'] ?? Misuzu::PATH_STORAGE, - $_ENV['STORAGE_PATH_REMOTE'] ?? '/_storage', - $_ENV['TEMPLATE_CACHE'] ?? '', -); + $whoops->register(); + + return new MisuzuContext( + $timings, + $_ENV['DATABASE_DSN'] ?? 'null:', + $_ENV['DOMAIN_ROLES'] ?? 'localhost=main,redirect,storage', + $_ENV['STORAGE_PATH_LOCAL'] ?? Misuzu::PATH_STORAGE, + $_ENV['STORAGE_PATH_REMOTE'] ?? '/_storage', + $_ENV['TEMPLATE_CACHE'] ?? '', + ); + } finally { + $timings->lap('startup'); + } +})(); diff --git a/public/index.php b/public/index.php index 7145f851..7e6f0041 100644 --- a/public/index.php +++ b/public/index.php @@ -27,6 +27,7 @@ if(is_file($msz->dbCtx->getMigrateLockPath())) { } $request = \Index\Http\HttpRequest::fromRequest(); +$msz->timings->lap('request'); $msz->registerRequestRoutes($request); if($msz->routingCtx->domainRoles->hasRole($request->getHeaderLine('Host'), 'main')) { diff --git a/src/Auth/AuthInfo.php b/src/Auth/AuthInfo.php index e6b51e38..63cf5bcd 100644 --- a/src/Auth/AuthInfo.php +++ b/src/Auth/AuthInfo.php @@ -169,10 +169,6 @@ class AuthInfo implements LastModifiedExtensionInterface { return $this->usersCtx->tryGetActiveBan($this->userInfo); } - public function getDisplayTimings(): bool { - return Misuzu::debug() || $this->getPerms('global')->check(Perm::G_TIMINGS_VIEW); - } - #[\Override] public function getFunctions() { return [ @@ -181,7 +177,6 @@ class AuthInfo implements LastModifiedExtensionInterface { new TwigFunction('auth_impersonating', $this->getImpersonating(...)), new TwigFunction('auth_get_real_user_info', $this->getRealUserInfo(...)), new TwigFunction('auth_get_ban_info', $this->getBanInfo(...)), - new TwigFunction('auth_display_timings', $this->getDisplayTimings(...)), ]; } } diff --git a/src/DatabaseContext.php b/src/DatabaseContext.php index 4cfe8050..75420785 100644 --- a/src/DatabaseContext.php +++ b/src/DatabaseContext.php @@ -6,12 +6,9 @@ use Index\Db\{DbBackends,DbConnection}; use Index\Db\Migration\{DbMigrationManager,DbMigrationRepo,FsDbMigrationRepo}; use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; use Index\Http\Routing\Filters\PrefixFilter; -use Index\Templating\Extension\TplExtensionCommon; -use Twig\TwigFunction; -use Twig\Extension\LastModifiedExtensionInterface; -class DatabaseContext implements RouteHandler, LastModifiedExtensionInterface { - use RouteHandlerCommon, TplExtensionCommon; +class DatabaseContext implements RouteHandler { + use RouteHandlerCommon; public private(set) DbConnection $conn; @@ -49,11 +46,4 @@ class DatabaseContext implements RouteHandler, LastModifiedExtensionInterface { if(is_file($this->getMigrateLockPath())) return 503; } - - #[\Override] - public function getFunctions() { - return [ - new TwigFunction('sql_query_count', $this->getQueryCount(...)), - ]; - } } diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php index 8d24e4ed..c01538cf 100644 --- a/src/MisuzuContext.php +++ b/src/MisuzuContext.php @@ -7,6 +7,7 @@ use Index\Config\Db\DbConfig; use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{Router,RouteHandler,RouteHandlerCommon}; use Index\Http\Routing\Filters\PrefixFilter; +use Index\Performance\Timings; use Index\Snowflake\{BinarySnowflake,RandomSnowflake,SnowflakeGenerator}; use Index\Urls\{ArrayUrlRegistry,UrlRegistry,UrlSource}; @@ -52,6 +53,7 @@ class MisuzuContext implements RouteHandler { public private(set) Users\UsersContext $usersCtx; public function __construct( + public private(set) Timings $timings, #[\SensitiveParameter] string $dsn, string $domainRoles, string $storageLocalPath, @@ -61,6 +63,7 @@ class MisuzuContext implements RouteHandler { $this->deps = new Dependencies; $this->deps->register($this); $this->deps->register($this->deps); + $this->deps->register($timings); $this->deps->register($this->dbCtx = new DatabaseContext($dsn)); $this->deps->register($this->dbCtx->conn); diff --git a/src/Routing/RoutingContext.php b/src/Routing/RoutingContext.php index b7849969..51993abe 100644 --- a/src/Routing/RoutingContext.php +++ b/src/Routing/RoutingContext.php @@ -6,8 +6,11 @@ use Index\Config\Config; use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{Router,RouteHandler as IndexRouteHandler}; use Index\Http\Routing\Filters\FilterInfo; +use Index\Performance\Timings; use Index\Templating\Extension\TplExtensionCommon; use Index\Urls\{ArrayUrlRegistry,UrlRegistry,UrlSource}; +use Misuzu\{DatabaseContext,Misuzu,Perm}; +use Misuzu\Auth\AuthInfo; use Twig\TwigFunction; use Twig\Extension\LastModifiedExtensionInterface; @@ -20,6 +23,9 @@ class RoutingContext implements LastModifiedExtensionInterface { public private(set) DomainRoles $domainRoles; public function __construct( + private Timings $timings, + private AuthInfo $authInfo, + private DatabaseContext $dbCtx, Dependencies $deps, string $domainRoles, ) { @@ -47,7 +53,21 @@ class RoutingContext implements LastModifiedExtensionInterface { } public function dispatch(?HttpRequest $request = null): void { - $this->router->dispatch($request); + if($request === null) { + $request = HttpRequest::fromRequest(); + $this->timings->lap('request'); + } + + $response = $this->router->handle($request); + $this->timings->lap('response'); + + if(Misuzu::debug() || $this->authInfo->getPerms('global')->check(Perm::G_TIMINGS_VIEW)) + $response = $response->withHeader( + 'Server-Timing', + sprintf('%s, msz-queries;dur=%d', $this->timings, $this->dbCtx->getQueryCount()) + ); + + Router::output($response); } #[\Override] diff --git a/src/Template.php b/src/Template.php index 655ad104..dbf772b5 100644 --- a/src/Template.php +++ b/src/Template.php @@ -2,17 +2,20 @@ namespace Misuzu; use InvalidArgumentException; +use Index\Performance\Timings; use Index\Templating\TplEnvironment; final class Template { private const FILE_EXT = '.twig'; + private static Timings $timings; private static TplEnvironment $env; /** @var array<string, mixed> */ private static array $vars = []; public static function init(MisuzuContext $env): void { + self::$timings = $env->timings; self::$env = $env->tplCtx->env; } @@ -22,13 +25,15 @@ final class Template { /** @param array<string, mixed> $vars */ public static function renderRaw(string $file, array $vars = []): string { - if(!defined('MSZ_TPL_RENDER')) - define('MSZ_TPL_RENDER', microtime(true)); + self::$timings->lap('load'); + try { + if(!str_ends_with($file, self::FILE_EXT)) + $file = str_replace('.', DIRECTORY_SEPARATOR, $file) . self::FILE_EXT; - if(!str_ends_with($file, self::FILE_EXT)) - $file = str_replace('.', DIRECTORY_SEPARATOR, $file) . self::FILE_EXT; - - return self::$env->render($file, array_merge(self::$vars, $vars)); + return self::$env->render($file, array_merge(self::$vars, $vars)); + } finally { + self::$timings->lap('render'); + } } /** @param array<string, mixed> $vars */ diff --git a/src/Templating/TemplatingExtension.php b/src/Templating/TemplatingExtension.php index 45abaa2b..0b005b66 100644 --- a/src/Templating/TemplatingExtension.php +++ b/src/Templating/TemplatingExtension.php @@ -44,7 +44,6 @@ final class TemplatingExtension implements LastModifiedExtensionInterface { new TwigFunction('git_commit_hash', GitInfo::hash(...)), new TwigFunction('git_tag', GitInfo::tag(...)), new TwigFunction('git_branch', GitInfo::branch(...)), - new TwigFunction('startup_time', fn(float $time = MSZ_STARTUP) => microtime(true) - $time), new TwigFunction('msz_header_menu', $this->getHeaderMenu(...)), new TwigFunction('msz_user_menu', $this->getUserMenu(...)), new TwigFunction('msz_manage_menu', $this->getManageMenu(...)), diff --git a/templates/_layout/footer.twig b/templates/_layout/footer.twig index 74281deb..be32a26a 100644 --- a/templates/_layout/footer.twig +++ b/templates/_layout/footer.twig @@ -12,13 +12,6 @@ <a href="https://patchii.net/flashii/misuzu/src/tag/{{ git_tag }}" target="_blank" rel="noopener" class="footer__link">{{ git_tag }}</a> {% endif %} # <a href="https://patchii.net/flashii/misuzu/commit/{{ git_commit_hash(true) }}" target="_blank" rel="noopener" class="footer__link">{{ git_commit_hash() }}</a> - {% if auth_display_timings() %} - / Index {{ ndx_version() }} - / {{ sql_query_count()|number_format }} queries - / {{ (startup_time() - startup_time(constant('MSZ_TPL_RENDER')))|number_format(5) }} load - / {{ startup_time(constant('MSZ_TPL_RENDER'))|number_format(5) }} template - / {{ startup_time()|number_format(5) }} total - {% endif %} </div> </div> </footer> diff --git a/templates/home/landing.twig b/templates/home/landing.twig index 6142cfb6..8f520dea 100644 --- a/templates/home/landing.twig +++ b/templates/home/landing.twig @@ -89,11 +89,6 @@ {% endif %} # <a href="https://github.com/flashwave/misuzu/commit/{{ git_commit_hash(true) }}" target="_blank" rel="noreferrer noopener">{{ git_commit_hash() }}</a> </div> -{% if auth_display_timings() %} - <div class="landingv2-footer-copyright-line"> - {{ sql_query_count()|number_format }} queries / {{ (startup_time() - startup_time(constant('MSZ_TPL_RENDER')))|number_format(5) }} load / {{ startup_time(constant('MSZ_TPL_RENDER'))|number_format(5) }} template / {{ startup_time()|number_format(5) }} total - </div> -{% endif %} </div> </div> </footer> diff --git a/tools/cron b/tools/cron index 8f616059..cc47383b 100755 --- a/tools/cron +++ b/tools/cron @@ -407,5 +407,5 @@ try { } } finally { unlink($cronPath); - echo 'Took ' . number_format(microtime(true) - MSZ_STARTUP, 5) . 'ms.' . PHP_EOL; + printf('Took %.5fms.%s', ((float)hrtime(true) - MSZ_STARTUP) / 1000000, PHP_EOL); } diff --git a/tools/render-tpl b/tools/render-tpl index 20e04aa4..ccfcc935 100755 --- a/tools/render-tpl +++ b/tools/render-tpl @@ -86,8 +86,4 @@ $ctx = $msz->tplCtx->loadTemplate(implode(' ', array_slice($argv, $pathIndex))); foreach($tplArgs as $name => $value) $ctx->setVar($name, $value); -// things expect this to be defined which is also a bit yucky -if(!defined('MSZ_TPL_RENDER')) - define('MSZ_TPL_RENDER', microtime(true)); - echo $ctx->render();