Conversation
generateInternalUrl() checks every canonical url through BaseUri::from()->isRelativePath() before computing the link, so each internal link rendered - the global menu of every page included - pays for a full League\Uri parse, host normalisation and encoding. A url of printable ASCII without a colon that does not start with a slash has neither a scheme nor an authority, and League\Uri accepts all of it, so it is a relative path; that is now answered by one anchored pattern. Everything else, including the input League\Uri refuses with an exception, still goes through BaseUri::from(), so every input ends as before. The pattern ends in \z, not $, which would let a trailing newline through. Assisted-by: claude-code:claude-opus-5-5 Agent-Session: https://claude.ai/code/session_01XzfnUQmxDxanqouHSEHHky Agent-Host: 32116e Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
|
How to repeat each number. Base Corpora. TYPO3CMS-Reference-CoreApi at 1. Call counts. Insert <?php
register_shutdown_function(static function () { file_put_contents(getenv('HARV_OUT'), json_encode($GLOBALS['__urls'] ?? [])); });2. Per call. phpbench, #[Bench\Revs(20)] #[Bench\Iterations(10)] #[Bench\Warmup(2)]
final class IsRelativeUrlBench
{
private array $urls;
public function __construct() { $this->urls = require __DIR__ . '/urls.php'; }
public function benchLeagueUri(): void
{
foreach ($this->urls as $url) { BaseUri::from($url)->isRelativePath(); }
}
public function benchPatternThenLeagueUri(): void
{
foreach ($this->urls as $url) {
if (preg_match('~^(?!/)[\x21-\x39\x3B-\x7E]+\z~', $url) === 1) { continue; }
BaseUri::from($url)->isRelativePath();
}
}
}3. Profile. <?php
$p = new ExcimerProfiler(); $p->setPeriod(0.002); $p->setEventType(EXCIMER_CPU); $p->setMaxDepth(250); $p->start();
register_shutdown_function(static function () use ($p) { $p->stop(); file_put_contents(getenv('PROF_OUT'), $p->getLog()->formatCollapsed()); });A frame's share is the samples of every stack that contains it, over all samples. 4. Full renders. Per round, one render of TYPO3 rendering test: Series A ran four arms per round (also #1398 alone and both together), series B two. Series A measured League 7.5.1. A separate Assisted by claude-code:claude-opus-5-5 — Session |
|
Hey @jaapio - what do you think? |
Merging this stops
AbstractUrlGenerator::generateInternalUrl()from running a fullLeague\Uriparse for every internal link it writes, which accounts for a fifth of the CPU samples of a render of the 1003 documents of TYPO3CMS-Reference-CoreApi. That share is what this change accounts for directly; measured, the render needs 27 % less wall time and 29 % less CPU time (median ratios of 13 paired rounds), the 94 document TYPO3 rendering test 20 % less wall time, the output is identical apart from the rendered timestamp and the peak memory is unchanged.What changes
Before computing a link,
generateInternalUrl()checks that the canonical url is a relative path, throughBaseUri::from($url)->isRelativePath(). The links of the global menu, rendered into every page, reach it throughAssetsExtension::renderLink()andgenerateCanonicalOutputUrl(); they make nearly all of the 1,092,322 calls in one CoreApi render (98.5 % of the samples inisRelativeUrl()sit belowGlobalMenuExtension::renderMenu()). The check now first tests the url against~^(?!/)[\x21-\x39\x3B-\x7E]+\z~— printable ASCII, no colon, not starting with a slash. Such a url has neither a scheme nor an authority, so it is a relative path, andLeague\Uriparses all of it without an exception. Everything else still goes toBaseUri::from()exactly as before, so every input ends as it did: the computed path,InvalidUrlException, orLeague\Uri's ownSyntaxError(it throws formailto:xor a control character, for instance). No cache, no state.Numbers
Each step can be checked on its own; the scripts and raw times are in the comment below.
1. How often the check runs, and how often the new path answers it. Counted by instrumenting
isRelativeUrl()over a full render:docs2. What one check costs. phpbench over the 4,608 distinct CoreApi urls, each counted once, 10 iterations of 20 revolutions:
BaseUri::from()->isRelativePath()90.068 ms (±15.0 %), the pattern with its fallback 0.565 ms (±9.6 %) — 19.5 µs against 0.12 µs per url. Times 1,092,322 calls, that predicts about 21 s less per CoreApi render.3. Where the time goes. A CPU profile (Excimer, 2 ms samples, one render each) of CoreApi:
isRelativeUrl()holds 20.02 % of all samples onmain(20.42 % in an earlier profile of the same base) and 0.06 % on this branch.4. Full renders. PHP 8.5.11 CLI with opcache off, as the CLI defaults to (as did phpbench), one process per render,
mainand this branch alternating within every round, output kept and compared. The machine was shared with other work during the measurement, which moved absolute times by up to 80 % between rounds, so the ratio within a round is the figure that holds. Two series, the first measurede790aa5e, the second20eaab5e; from those to this head only the test and the comment inisRelativeUrl()changed.docsThe CoreApi minimum of 0.49 is a load spike in the
mainrender of that round, and the first round was only 1.2 % faster (0.988). Peak RSS, median of the second series: CoreApi 315,604 KB onmainand 315,474 KB on this branch, TYPO3 rendering test 85,416 KB and 85,074 KB.Output. CoreApi: all 1,189 written files identical. TYPO3 rendering test: 3 of 144 differ, by the rendered timestamp only.
docs: all 32 identical.What does not add up yet. The saving measured in full renders is larger than the direct cost of the parse: on CoreApi 27 % of wall and 29 % of CPU time against the 20 % of samples in the profile (and the 21 s phpbench predicts); on the TYPO3 rendering test about 20 % against a predicted 14 % (46,978 calls × the 19.4 µs saved per call, of 6.475 s). The extra is CPU work, not waiting: the CPU time (user and system) falls by about the same ratio as the wall time — user time alone by 29 %, system time, around a second per render, by 3 % — and the branch profile has 30 % fewer samples in total (22,370 on the branch against 31,941 on
main) although only 20 % sat inisRelativeUrl(). It is not the garbage collector:gc_status()after a render reports the same runs (645 on CoreApi, 54 on the rendering test) and the same collected count on both, and nearly the same collector time (3.65 s onmainand 3.68 s on the branch for CoreApi). I have not found the cause, and the conservative figure for this change is the direct one, 20 %.Proof that every input ends as before
AbstractUrlGeneratorIsRelativeUrlTestcompares the outcome ofgenerateInternalUrl()withLeague\Urias oracle, for 43 named cases (including a 100 KB and a 1 MB url), every byte in two positions, and 5,000 seeded random strings over all printable non-alphanumeric ASCII, space, tab, newline, DEL and non-ASCII bytes.$, which lets a trailing newline through — the first version of this change had that defect, and this test found it.league/uri ^7.5.1. The CI matrix haslowestjobs besideslockedandhighest, and the same inputs plus all harvested urls were also checked directly against 7.5.1 and 7.8.1: of 10,756 inputs, 5,923 take the new path, andLeague\Uricalls every one of them a relative path, in both versions.Relation to #1398
Both change
AbstractUrlGenerator.phpand conflict on the property block, trivially. In the first series #1398 alone took the TYPO3 rendering test from 6.475 s to 5.635 s, this change alone to 5.290 s, both together to 5.305 s: with this change in, #1398 made no measurable difference there (both / this, median ratio 0.994 over 10 rounds, range 0.85 – 1.09). On CoreApi five rounds were too noisy to say either way.Gates
phpunitunit 653, functional 120, integration 266,phpcs,phpstan,deptrac— green on PHP 8.5.11; CI green on8ed03f90.Assisted by claude-code:claude-opus-5-5 — Session