Replaced timings in the footer with a visual for the Server-Timing header.

This commit is contained in:
flash 2025-04-03 19:40:10 +00:00
parent ceb6bece09
commit 83068a4183
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
17 changed files with 246 additions and 65 deletions

View file

@ -22,3 +22,4 @@ html, body {
}
@include loading.css;
@include perf.css;

100
assets/common.css/perf.css Normal file
View file

@ -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;
}

View file

@ -2,6 +2,7 @@
#include csrf.js
#include html.js
#include meta.js
#include perf.jsx
#include uniqstr.js
#include xhr.js

72
assets/common.js/perf.jsx Normal file
View file

@ -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'] });
})();

6
composer.lock generated
View file

@ -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",

View file

@ -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');
}
})();

View file

@ -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')) {

View file

@ -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(...)),
];
}
}

View file

@ -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(...)),
];
}
}

View file

@ -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);

View file

@ -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]

View file

@ -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 */

View file

@ -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(...)),

View file

@ -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>

View file

@ -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>

View file

@ -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);
}

View file

@ -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();