diff --git a/packages/trace-viewer/src/sw/lruCache.ts b/packages/trace-viewer/src/sw/lruCache.ts new file mode 100644 index 0000000000000..573a15ea49a55 --- /dev/null +++ b/packages/trace-viewer/src/sw/lruCache.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class LRUCache { + private _maxSize: number; + private _map: Map; + private _size: number; + + constructor(maxSize: number) { + this._maxSize = maxSize; + this._map = new Map(); + this._size = 0; + } + + getOrCompute(key: K, compute: () => { value: V, size: number }): V { + if (this._map.has(key)) { + const result = this._map.get(key)!; + // reinserting makes this the least recently used entry + this._map.delete(key); + this._map.set(key, result); + return result.value; + } + + const result = compute(); + + while (this._map.size && this._size + result.size > this._maxSize) { + const [firstKey, firstValue] = this._map.entries().next().value; + this._size -= firstValue.size; + this._map.delete(firstKey); + } + + this._map.set(key, result); + this._size += result.size; + return result.value; + } +} diff --git a/packages/trace-viewer/src/sw/snapshotRenderer.ts b/packages/trace-viewer/src/sw/snapshotRenderer.ts index 438683ed84976..a9f89ba27338c 100644 --- a/packages/trace-viewer/src/sw/snapshotRenderer.ts +++ b/packages/trace-viewer/src/sw/snapshotRenderer.ts @@ -17,6 +17,7 @@ import { escapeHTMLAttribute, escapeHTML } from '@isomorphic/stringUtils'; import type { FrameSnapshot, NodeNameAttributesChildNodesSnapshot, NodeSnapshot, RenderedFrameSnapshot, ResourceSnapshot, SubtreeReferenceSnapshot } from '@trace/snapshot'; import type { PageEntry } from '../types/entries'; +import type { LRUCache } from './lruCache'; function findClosest(items: T[], metric: (v: T) => number, target: number) { return items.find((item, index) => { @@ -35,35 +36,8 @@ function isSubtreeReferenceSnapshot(n: NodeSnapshot): n is SubtreeReferenceSnaps return Array.isArray(n) && Array.isArray(n[0]); } -let cacheSize = 0; -const cache = new Map(); -const CACHE_SIZE = 300_000_000; // 300mb - -function lruCache(key: SnapshotRenderer, compute: () => string): string { - if (cache.has(key)) { - const value = cache.get(key)!; - // reinserting makes this the least recently used entry - cache.delete(key); - cache.set(key, value); - return value; - } - - - const result = compute(); - - while (cache.size && cacheSize + result.length > CACHE_SIZE) { - const [firstKey, firstValue] = cache.entries().next().value; - cacheSize -= firstValue.length; - cache.delete(firstKey); - } - - cache.set(key, result); - cacheSize += result.length; - - return result; -} - export class SnapshotRenderer { + private _htmlCache: LRUCache; private _snapshots: FrameSnapshot[]; private _index: number; readonly snapshotName: string | undefined; @@ -72,7 +46,8 @@ export class SnapshotRenderer { private _callId: string; private _screencastFrames: PageEntry['screencastFrames']; - constructor(resources: ResourceSnapshot[], snapshots: FrameSnapshot[], screencastFrames: PageEntry['screencastFrames'], index: number) { + constructor(htmlCache: LRUCache, resources: ResourceSnapshot[], snapshots: FrameSnapshot[], screencastFrames: PageEntry['screencastFrames'], index: number) { + this._htmlCache = htmlCache; this._resources = resources; this._snapshots = snapshots; this._index = index; @@ -171,16 +146,15 @@ export class SnapshotRenderer { }; const snapshot = this._snapshot; - const html = lruCache(this, () => { + const html = this._htmlCache.getOrCompute(this, () => { visit(snapshot.html, this._index, undefined, undefined); - - const html = result.join(''); - // Hide the document in order to prevent flickering. We will unhide once script has processed shadow. const prefix = snapshot.doctype ? `` : ''; - return prefix + [ + const html = prefix + [ + // Hide the document in order to prevent flickering. We will unhide once script has processed shadow. '', `` - ].join('') + html; + ].join('') + result.join(''); + return { value: html, size: html.length }; }); return { html, pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index }; diff --git a/packages/trace-viewer/src/sw/snapshotStorage.ts b/packages/trace-viewer/src/sw/snapshotStorage.ts index 5e10fc97fbcd6..d15bfc4f41674 100644 --- a/packages/trace-viewer/src/sw/snapshotStorage.ts +++ b/packages/trace-viewer/src/sw/snapshotStorage.ts @@ -17,6 +17,7 @@ import type { FrameSnapshot, ResourceSnapshot } from '@trace/snapshot'; import { rewriteURLForCustomProtocol, SnapshotRenderer } from './snapshotRenderer'; import type { PageEntry } from '../types/entries'; +import { LRUCache } from './lruCache'; export class SnapshotStorage { private _resources: ResourceSnapshot[] = []; @@ -24,6 +25,7 @@ export class SnapshotStorage { raw: FrameSnapshot[], renderers: SnapshotRenderer[] }>(); + private _cache = new LRUCache(100_000_000); // 100MB per each trace addResource(resource: ResourceSnapshot): void { resource.request.url = rewriteURLForCustomProtocol(resource.request.url); @@ -44,7 +46,7 @@ export class SnapshotStorage { this._frameSnapshots.set(snapshot.pageId, frameSnapshots); } frameSnapshots.raw.push(snapshot); - const renderer = new SnapshotRenderer(this._resources, frameSnapshots.raw, screencastFrames, frameSnapshots.raw.length - 1); + const renderer = new SnapshotRenderer(this._cache, this._resources, frameSnapshots.raw, screencastFrames, frameSnapshots.raw.length - 1); frameSnapshots.renderers.push(renderer); return renderer; }