add memory-constrained cache and collect html on array instead of callstack
This commit is contained in:
parent
08a4045525
commit
57a50d0250
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue