Skip to content

Commit

Permalink
Optimise DOM mutation handling in DOMMonitor (ghostery#4071)
Browse files Browse the repository at this point in the history
* feat: optimise mutation handling

refs https://efa.mvv-muenchen.de/index.html?name_origin=91000680&name_destination=streetID%[…]20240704&itdTime=1001&language=de&itdTripDateTimeDepArr=dep
refs ghostery/broken-page-reports#732

---------

Co-authored-by: chrmod <chrmod@ghostery.com>
Co-authored-by: Krzysztof Modras <1228153+chrmod@users.noreply.github.com>
Co-authored-by: Philipp Claßen <philipp.classen@posteo.de>
Co-authored-by: Rémi <remusao@users.noreply.github.com>
  • Loading branch information
5 people committed Jul 9, 2024
1 parent 5aecd4c commit 37461de
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 7 deletions.
78 changes: 73 additions & 5 deletions packages/adblocker-content/adblocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,45 @@ export interface IMessageFromBackground {
}[];
}

function debounce(
fn: () => void,
{
waitFor,
maxWait,
}: {
waitFor: number;
maxWait: number;
},
) {
let delayedTimer: NodeJS.Timeout | undefined;
let maxWaitTimer: NodeJS.Timeout | undefined;

const clear = () => {
clearTimeout(delayedTimer);
clearTimeout(maxWaitTimer);

delayedTimer = undefined;
maxWaitTimer = undefined;
};

const run = () => {
clear();
fn();
};

return [
() => {
if (maxWait > 0 && maxWaitTimer === undefined) {
maxWaitTimer = setTimeout(run, maxWait);
}

clearTimeout(delayedTimer);
delayedTimer = setTimeout(run, waitFor);
},
clear,
];
}

function isElement(node: Node): node is Element {
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#node_type_constants
return node.nodeType === 1; // Node.ELEMENT_NODE;
Expand Down Expand Up @@ -73,6 +112,7 @@ export function extractFeaturesFromDOM(roots: Element[]): {
const classes: Set<string> = new Set();
const hrefs: Set<string> = new Set();
const ids: Set<string> = new Set();
const seenElements: Set<Element> = new Set();

for (const root of roots) {
for (const element of [
Expand All @@ -81,6 +121,13 @@ export function extractFeaturesFromDOM(roots: Element[]): {
'[id]:not(html):not(body),[class]:not(html):not(body),[href]:not(html):not(body)',
),
]) {
// If one of root belongs to another root which is parent node of the one, querySelectorAll can return duplicates.
if (seenElements.has(element)) {
continue;
}
seenElements.add(element);

// Any conditions to filter this element out should be placed under this line:
if (ignoredTags.has(element.nodeName.toLowerCase())) {
continue;
}
Expand All @@ -93,10 +140,8 @@ export function extractFeaturesFromDOM(roots: Element[]): {

// Update classes
const classList = element.classList;
if (classList) {
for (const cls of classList) {
classes.add(cls);
}
for (const classEntry of classList) {
classes.add(classEntry);
}

// Update href
Expand Down Expand Up @@ -146,8 +191,31 @@ export class DOMMonitor {
window: Pick<Window, 'document'> & { MutationObserver?: typeof MutationObserver },
): void {
if (this.observer === null && window.MutationObserver !== undefined) {
const nodes: Set<Element> = new Set();

const handleUpdatedNodesCallback = () => {
this.handleUpdatedNodes(Array.from(nodes));
nodes.clear();
};
const [debouncedHandleUpdatedNodes, cancelHandleUpdatedNodes] = debounce(
handleUpdatedNodesCallback,
{
waitFor: 25,
maxWait: 1000,
},
);

this.observer = new window.MutationObserver((mutations: MutationRecord[]) => {
this.handleUpdatedNodes(getElementsFromMutations(mutations));
getElementsFromMutations(mutations).forEach(nodes.add, nodes);

// Set a threshold to prevent websites continuously
// causing DOM mutations making the set being filled up infinitely.
if (nodes.size > 512) {
cancelHandleUpdatedNodes();
handleUpdatedNodesCallback();
} else {
debouncedHandleUpdatedNodes();
}
});

this.observer.observe(window.document.documentElement, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe('#injectCosmetics', () => {
div.appendChild(a);

document.body.appendChild(div);
await tick();
await tick(30);

sinon.assert.calledThrice(getCosmeticsFilters);
sinon.assert.calledWith(getCosmeticsFilters.thirdCall, {
Expand All @@ -129,7 +129,7 @@ describe('#injectCosmetics', () => {
a.href = 'https://baz.com/';
a.classList.add('class1');

await tick();
await tick(30);
dom.window.close();

expect(getCosmeticsFilters.callCount).to.eql(4);
Expand Down

0 comments on commit 37461de

Please sign in to comment.