* Speed up snapshotting of many new dom nodes By avoiding reflow we shave about 15-25% off our snapshotting time * Improve newlyAddedElement docs * Optimize needMaskingText by using el.closest and less recursion * Serve all rrweb dist files * Split serializeNode into smaller functions Makes it easier to profile * Slow down cpu enhance tracing on fast machines * Increase timeout * Perf: only loop through ancestors when they have something to compare to * Perf: `hasNode` is cheaper than `getMeta` * Perf: If parents where already checked, no need to do it again * Perf: reverse for loops are faster Because they only do the .lenght check once. In this case I don't think we'll see much performance gains if any * Clean up code * Perf: check ancestors once with isBlocked * guessing this might fixes canvas test * Update packages/rrweb/src/record/observers/canvas/webgl.ts Co-authored-by: yz-yu <yanzhen@smartx.com> * Fix #904 (#906) Properly remove crossorigin attribute * Bump minimist from 1.2.5 to 1.2.6 (#902) Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: yz-yu <yanzhen@smartx.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
148 lines
3.6 KiB
TypeScript
148 lines
3.6 KiB
TypeScript
import {
|
|
idNodeMap,
|
|
MaskInputFn,
|
|
MaskInputOptions,
|
|
nodeMetaMap,
|
|
IMirror,
|
|
serializedNodeWithId,
|
|
} from './types';
|
|
|
|
export function isElement(n: Node): n is Element {
|
|
return n.nodeType === n.ELEMENT_NODE;
|
|
}
|
|
|
|
export function isShadowRoot(n: Node): n is ShadowRoot {
|
|
const host: Element | null = (n as ShadowRoot)?.host;
|
|
return Boolean(host?.shadowRoot === n);
|
|
}
|
|
|
|
export class Mirror implements IMirror<Node> {
|
|
private idNodeMap: idNodeMap = new Map();
|
|
private nodeMetaMap: nodeMetaMap = new WeakMap();
|
|
|
|
getId(n: Node | undefined | null): number {
|
|
if (!n) return -1;
|
|
|
|
const id = this.getMeta(n)?.id;
|
|
|
|
// if n is not a serialized Node, use -1 as its id.
|
|
return id ?? -1;
|
|
}
|
|
|
|
getNode(id: number): Node | null {
|
|
return this.idNodeMap.get(id) || null;
|
|
}
|
|
|
|
getIds(): number[] {
|
|
return Array.from(this.idNodeMap.keys());
|
|
}
|
|
|
|
getMeta(n: Node): serializedNodeWithId | null {
|
|
return this.nodeMetaMap.get(n) || null;
|
|
}
|
|
|
|
// removes the node from idNodeMap
|
|
// doesn't remove the node from nodeMetaMap
|
|
removeNodeFromMap(n: Node) {
|
|
const id = this.getId(n);
|
|
this.idNodeMap.delete(id);
|
|
|
|
if (n.childNodes) {
|
|
n.childNodes.forEach((childNode) =>
|
|
this.removeNodeFromMap((childNode as unknown) as Node),
|
|
);
|
|
}
|
|
}
|
|
has(id: number): boolean {
|
|
return this.idNodeMap.has(id);
|
|
}
|
|
|
|
hasNode(node: Node): boolean {
|
|
return this.nodeMetaMap.has(node);
|
|
}
|
|
|
|
add(n: Node, meta: serializedNodeWithId) {
|
|
const id = meta.id;
|
|
this.idNodeMap.set(id, n);
|
|
this.nodeMetaMap.set(n, meta);
|
|
}
|
|
|
|
replace(id: number, n: Node) {
|
|
this.idNodeMap.set(id, n);
|
|
}
|
|
|
|
reset() {
|
|
this.idNodeMap = new Map();
|
|
this.nodeMetaMap = new WeakMap();
|
|
}
|
|
}
|
|
|
|
export function createMirror(): Mirror {
|
|
return new Mirror();
|
|
}
|
|
|
|
export function maskInputValue({
|
|
maskInputOptions,
|
|
tagName,
|
|
type,
|
|
value,
|
|
maskInputFn,
|
|
}: {
|
|
maskInputOptions: MaskInputOptions;
|
|
tagName: string;
|
|
type: string | number | boolean | null;
|
|
value: string | null;
|
|
maskInputFn?: MaskInputFn;
|
|
}): string {
|
|
let text = value || '';
|
|
if (
|
|
maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] ||
|
|
maskInputOptions[type as keyof MaskInputOptions]
|
|
) {
|
|
if (maskInputFn) {
|
|
text = maskInputFn(text);
|
|
} else {
|
|
text = '*'.repeat(text.length);
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__';
|
|
type PatchedGetImageData = {
|
|
[ORIGINAL_ATTRIBUTE_NAME]: CanvasImageData['getImageData'];
|
|
} & CanvasImageData['getImageData'];
|
|
|
|
export function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean {
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return true;
|
|
|
|
const chunkSize = 50;
|
|
|
|
// get chunks of the canvas and check if it is blank
|
|
for (let x = 0; x < canvas.width; x += chunkSize) {
|
|
for (let y = 0; y < canvas.height; y += chunkSize) {
|
|
const getImageData = ctx.getImageData as PatchedGetImageData;
|
|
const originalGetImageData =
|
|
ORIGINAL_ATTRIBUTE_NAME in getImageData
|
|
? getImageData[ORIGINAL_ATTRIBUTE_NAME]
|
|
: getImageData;
|
|
// by getting the canvas in chunks we avoid an expensive
|
|
// `getImageData` call that retrieves everything
|
|
// even if we can already tell from the first chunk(s) that
|
|
// the canvas isn't blank
|
|
const pixelBuffer = new Uint32Array(
|
|
originalGetImageData.call(
|
|
ctx,
|
|
x,
|
|
y,
|
|
Math.min(chunkSize, canvas.width - x),
|
|
Math.min(chunkSize, canvas.height - y),
|
|
).data.buffer,
|
|
);
|
|
if (pixelBuffer.some((pixel) => pixel !== 0)) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|