Files
Kristiyan Kostadinov fb65ad157c perf(platform-server): speed up resolution of base (#61392)
The `getBaseHref` method is called several times per request and currently queries through the entire document. We can speed it up by taking advantage of the fact that the `<base>` can only be a direct child of the `<head>` and is usually defined towards the beginning. Below are some benchmarks for a "Hello world" app before and after this change.

### Before:
```
Running 60s test @ http://localhost:4202
100 connections with 10 pipelining factor

┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬─────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%    │ Avg       │ Stdev    │ Max     │
├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼─────────┤
│ Latency │ 568 ms │ 853 ms │ 901 ms │ 904 ms │ 866.58 ms │ 437.6 ms │ 9915 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴─────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev   │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec   │ 490     │ 826     │ 1,006   │ 1,643   │ 1,129.3 │ 234.69  │ 490     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 10.4 MB │ 17.4 MB │ 21.3 MB │ 34.7 MB │ 23.9 MB │ 4.96 MB │ 10.3 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘

Req/Bytes counts sampled once per second.
# of samples: 60

69k requests in 60.04s, 1.43 GB read
90 errors (90 timeouts)
```

### After

```
Running 60s test @ http://localhost:4202
100 connections with 10 pipelining factor

┌─────────┬────────┬────────┬────────┬─────────┬───────────┬───────────┬─────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%     │ Avg       │ Stdev     │ Max     │
├─────────┼────────┼────────┼────────┼─────────┼───────────┼───────────┼─────────┤
│ Latency │ 471 ms │ 831 ms │ 889 ms │ 1668 ms │ 835.91 ms │ 467.89 ms │ 9720 ms │
└─────────┴────────┴────────┴────────┴─────────┴───────────┴───────────┴─────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬──────────┬────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg      │ Stdev  │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼────────┼─────────┤
│ Req/Sec   │ 390     │ 860     │ 1,145   │ 1,572   │ 1,156.77 │ 222.65 │ 390     │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼────────┼─────────┤
│ Bytes/Sec │ 8.24 MB │ 18.2 MB │ 24.2 MB │ 33.2 MB │ 24.4 MB  │ 4.7 MB │ 8.24 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴──────────┴────────┴─────────┘

Req/Bytes counts sampled once per second.
# of samples: 60

71k requests in 60.03s, 1.47 GB read
140 errors (140 timeouts)
```

PR Close #61392
2025-05-16 09:03:38 +00:00

121 lines
3.5 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {ɵsetRootDomAdapter as setRootDomAdapter} from '@angular/common';
import {ɵBrowserDomAdapter as BrowserDomAdapter} from '@angular/platform-browser';
import domino from './bundled-domino';
export function setDomTypes() {
// Make all Domino types available in the global env.
// NB: Any changes here should also be done in `packages/platform-server/init/src/shims.ts`.
Object.assign(globalThis, domino.impl);
(globalThis as any)['KeyboardEvent'] = domino.impl.Event;
}
/**
* Parses a document string to a Document object.
*/
export function parseDocument(html: string, url = '/') {
let window = domino.createWindow(html, url);
let doc = window.document;
return doc;
}
/**
* Serializes a document to string.
*/
export function serializeDocument(doc: Document): string {
return (doc as any).serialize();
}
/**
* DOM Adapter for the server platform based on https://github.com/fgnass/domino.
*/
export class DominoAdapter extends BrowserDomAdapter {
static override makeCurrent() {
setDomTypes();
setRootDomAdapter(new DominoAdapter());
}
override readonly supportsDOMEvents = false;
private static defaultDoc: Document;
override createHtmlDocument(): Document {
return parseDocument('<html><head><title>fakeTitle</title></head><body></body></html>');
}
override getDefaultDocument(): Document {
if (!DominoAdapter.defaultDoc) {
DominoAdapter.defaultDoc = domino.createDocument();
}
return DominoAdapter.defaultDoc;
}
override isElementNode(node: any): boolean {
return node ? node.nodeType === DominoAdapter.defaultDoc.ELEMENT_NODE : false;
}
override isShadowRoot(node: any): boolean {
return node.shadowRoot == node;
}
/** @deprecated No longer being used in Ivy code. To be removed in version 14. */
override getGlobalEventTarget(doc: Document, target: string): EventTarget | null {
if (target === 'window') {
return doc.defaultView;
}
if (target === 'document') {
return doc;
}
if (target === 'body') {
return doc.body;
}
return null;
}
override getBaseHref(doc: Document): string {
const length = doc.head.children.length;
// The `<base>` can only be a direct child of `<head>` so we can save some
// execution time by looking through them directly instead of querying for it.
// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/base
// Note that we can't cache the `href` value itself, because this method gets called with a
// different document every time which means that in theory the value can be different too.
for (let i = 0; i < length; i++) {
const child = doc.head.children[i];
// Tag names are always uppercase for HTML nodes.
if (child.tagName === 'BASE') {
// TODO(alxhub): Need relative path logic from BrowserDomAdapter here?
return child.getAttribute('href') || '';
}
}
return '';
}
override dispatchEvent(el: Node, evt: any) {
el.dispatchEvent(evt);
// Dispatch the event to the window also.
const doc = el.ownerDocument || el;
const win = (doc as any).defaultView;
if (win) {
win.dispatchEvent(evt);
}
}
override getUserAgent(): string {
return 'Fake user agent';
}
override getCookie(name: string): string {
throw new Error('getCookie has not been implemented');
}
}