add memory-constrained cache and collect html on array instead of callstack

This commit is contained in:
Simon Knott 2024-09-02 14:21:03 +02:00
parent 08a4045525
commit 57a50d0250
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC

View file

@ -25,6 +25,28 @@ function isSubtreeReferenceSnapshot(n: NodeSnapshot): n is SubtreeReferenceSnaps
return Array.isArray(n) && Array.isArray(n[0]); return Array.isArray(n) && Array.isArray(n[0]);
} }
let cacheSize = 0;
const cache = new Map<SnapshotRenderer, string>();
const CACHE_SIZE = 300000000; // 300mb
function cacheAndReturn(key: SnapshotRenderer, compute: () => string): string {
if (cache.has(key))
return cache.get(key)!;
const result = compute();
while (cacheSize + result.length > CACHE_SIZE) {
const first = cache.keys().next().value;
cacheSize -= cache.get(first)!.length;
cache.delete(first);
}
cache.set(key, result);
cacheSize += result.length;
return result;
}
export class SnapshotRenderer { export class SnapshotRenderer {
private _snapshots: FrameSnapshot[]; private _snapshots: FrameSnapshot[];
private _index: number; private _index: number;
@ -32,7 +54,6 @@ export class SnapshotRenderer {
private _resources: ResourceSnapshot[]; private _resources: ResourceSnapshot[];
private _snapshot: FrameSnapshot; private _snapshot: FrameSnapshot;
private _callId: string; private _callId: string;
private _renderResults = new WeakMap<FrameSnapshot, string>();
constructor(resources: ResourceSnapshot[], snapshots: FrameSnapshot[], index: number) { constructor(resources: ResourceSnapshot[], snapshots: FrameSnapshot[], index: number) {
this._resources = resources; this._resources = resources;
@ -52,14 +73,17 @@ export class SnapshotRenderer {
} }
render(): RenderedFrameSnapshot { render(): RenderedFrameSnapshot {
const visit = (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined, parentAttrs: [string, string][] | undefined): string => { const result: string[] = [];
const visit = (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined, parentAttrs: [string, string][] | undefined) => {
// Text node. // Text node.
if (typeof n === 'string') { if (typeof n === 'string') {
// Best-effort Electron support: rewrite custom protocol in url() links in stylesheets. // Best-effort Electron support: rewrite custom protocol in url() links in stylesheets.
// Old snapshotter was sending lower-case. // Old snapshotter was sending lower-case.
if (parentTag === 'STYLE' || parentTag === 'style') if (parentTag === 'STYLE' || parentTag === 'style')
return rewriteURLsInStyleSheetForCustomProtocol(n); result.push(rewriteURLsInStyleSheetForCustomProtocol(n));
return escapeHTML(n); else
result.push(escapeHTML(n));
return;
} }
if (isSubtreeReferenceSnapshot(n)) { if (isSubtreeReferenceSnapshot(n)) {
@ -78,8 +102,7 @@ export class SnapshotRenderer {
// JS is enabled. So rename it to <x-noscript>. // JS is enabled. So rename it to <x-noscript>.
const nodeName = name === 'NOSCRIPT' ? 'X-NOSCRIPT' : name; const nodeName = name === 'NOSCRIPT' ? 'X-NOSCRIPT' : name;
const attrs = Object.entries(nodeAttrs || {}); const attrs = Object.entries(nodeAttrs || {});
const builder: string[] = []; result.push('<', nodeName);
builder.push('<', nodeName);
const kCurrentSrcAttribute = '__playwright_current_src__'; const kCurrentSrcAttribute = '__playwright_current_src__';
const isFrame = nodeName === 'IFRAME' || nodeName === 'FRAME'; const isFrame = nodeName === 'IFRAME' || nodeName === 'FRAME';
const isAnchor = nodeName === 'A'; const isAnchor = nodeName === 'A';
@ -107,34 +130,32 @@ export class SnapshotRenderer {
attrValue = 'link://' + value; attrValue = 'link://' + value;
else if (attr.toLowerCase() === 'href' || attr.toLowerCase() === 'src' || attr === kCurrentSrcAttribute) else if (attr.toLowerCase() === 'href' || attr.toLowerCase() === 'src' || attr === kCurrentSrcAttribute)
attrValue = rewriteURLForCustomProtocol(value); attrValue = rewriteURLForCustomProtocol(value);
builder.push(' ', attrName, '="', escapeHTMLAttribute(attrValue), '"'); result.push(' ', attrName, '="', escapeHTMLAttribute(attrValue), '"');
} }
builder.push('>'); result.push('>');
for (const child of children) for (const child of children)
builder.push(visit(child, snapshotIndex, nodeName, attrs)); visit(child, snapshotIndex, nodeName, attrs);
if (!autoClosing.has(nodeName)) if (!autoClosing.has(nodeName))
builder.push('</', nodeName, '>'); result.push('</', nodeName, '>');
return builder.join(''); return;
} else { } else {
// Why are we here? Let's not throw, just in case. // Why are we here? Let's not throw, just in case.
return ''; return;
} }
}; };
const snapshot = this._snapshot; const snapshot = this._snapshot;
if (!this._renderResults.has(snapshot)) const html = cacheAndReturn(this, () => {
this._renderResults.set(snapshot, visit(snapshot.html, this._index, undefined, undefined)); visit(snapshot.html, this._index, undefined, undefined);
let html = this._renderResults.get(snapshot);
if (!html)
return { html: '', pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index };
const html = result.join('');
// Hide the document in order to prevent flickering. We will unhide once script has processed shadow. // Hide the document in order to prevent flickering. We will unhide once script has processed shadow.
const prefix = snapshot.doctype ? `<!DOCTYPE ${snapshot.doctype}>` : ''; const prefix = snapshot.doctype ? `<!DOCTYPE ${snapshot.doctype}>` : '';
html = prefix + [ return prefix + [
'<style>*,*::before,*::after { visibility: hidden }</style>', '<style>*,*::before,*::after { visibility: hidden }</style>',
`<script>${snapshotScript(this._callId, this.snapshotName)}</script>` `<script>${snapshotScript(this._callId, this.snapshotName)}</script>`
].join('') + html; ].join('') + html;
});
return { html, pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index }; return { html, pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index };
} }