Skip to content

Conversation

@daniel-graham-amplitude
Copy link
Collaborator

@daniel-graham-amplitude daniel-graham-amplitude commented Jul 4, 2025

Summary

  • Adds 2 test pages that do performance tests (stress tests) on autocapture. They're both checkboxes that are deeply nested inside of a tree where each parent has many siblings and many attributes.
  • Optimizes performance by adding caching.
    • getHierarchy and getNearestLabel get executed multiple times (click handler, change handler) on the same element, within the same event loop, returning the same result
    • This adds a cache so that if they get called within the same event loop, it memoizes the result. If they happen on the same event loop the contents are expected to be the same because the DOM hasn't had a chance to re-render yet
  • Replaces array creation with array iterations
    • We use Array.from and Array.prototype.filter on both the children and attributes properties of Element. These methods create new arrays, where the creation of the arrays is O(n) both size and space
    • This refactors it so that instead of creating new arrays, it just runs a for-loop over the original array, and performs the same calculations (note that the tests are unchanged and still passing, indicating the behaviour is unchanged)
    • This changes it from running 3 O(n) runtime operations + 2 O(n) space operations down to just 1 O(n) runtime operation.
  • Next steps
    • Consolidate getHierarchy and getNearestLabel into one function so that we traverse an element's ancestry once instead of twice
    • Subject to agreement from team, put limits on how many DOM traversals we do (limit ancestor count, limit capture on elements that have high sibling counts and high attribute counts)

Checklist

  • Does your PR title have the correct title format?
  • Does your PR have a breaking change?: No

Note

Memoizes getHierarchy and nearest-label lookups per event loop, streamlines hierarchy property computation to avoid array creations, and adds deep DOM performance test pages.

  • Autocapture Performance:
    • Memoize getHierarchy(element) results within the same event loop using MessageChannel-backed cache.
    • Add per-event-loop cache for getNearestLabel(element) used in getEventProperties.
    • Replace Array.from/filter usage with indexed iteration for siblings and attributes to avoid extra allocations.
    • Optimize index calculations (index, indexOfType) and previous-sibling tag retrieval.
  • Tests:
    • Mock getGlobalScope and add test ensuring cached hierarchy reuse.
  • Test Server:
    • Add test-server/autocapture/performance-test.html and performance-test-extreme.html to benchmark deep DOM hierarchies with metrics UI.

Written by Cursor Bugbot for commit 7b21b28. Configure here.

@daniel-graham-amplitude daniel-graham-amplitude marked this pull request as draft July 4, 2025 16:48
@promptless
Copy link

promptless bot commented Jul 4, 2025

✅ No documentation updates required.

@daniel-graham-amplitude daniel-graham-amplitude changed the title fix: reduce redundant array creation in getHierarchy fix: add caching + reduce redundant array creation in getHierarchy Jul 16, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR optimizes the performance of the getHierarchy and getNearestLabel functions by implementing caching mechanisms and replacing inefficient array operations with direct iteration. The changes are motivated by performance tests showing that these functions are called multiple times on the same element within the same event loop.

  • Adds caching for getHierarchy and getNearestLabel results that invalidates after the current event loop
  • Replaces Array.from and Array.prototype.filter operations with direct for-loop iterations to reduce O(n) array creation overhead
  • Includes comprehensive performance test pages to validate the optimizations

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
test-server/autocapture/performance-test.html Performance test page with deeply nested DOM structure (15 levels, 12 siblings per level)
test-server/autocapture/performance-test-extreme.html Extreme performance test page with 100th descendant target and 100 siblings per row
packages/plugin-autocapture-browser/src/hierarchy.ts Adds caching mechanism and optimizes sibling iteration in getElementProperties and getHierarchy
packages/plugin-autocapture-browser/src/helpers.ts Adds caching mechanism for getNearestLabel function

@daniel-graham-amplitude daniel-graham-amplitude marked this pull request as ready for review July 16, 2025 18:48
@promptless
Copy link

promptless bot commented Jul 16, 2025

✅ No documentation updates required.

@Mercy811
Copy link
Contributor

bugbot run

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no bugs!


Comment @cursor review or bugbot run to trigger another review on this PR

@daniel-graham-amplitude daniel-graham-amplitude marked this pull request as draft October 24, 2025 17:50
Copy link
Contributor

@Mercy811 Mercy811 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Dan for fixing it! LGTM!

Comment on lines +32 to +61
// beforeAll(() => {
// // mock getGlobalScope
// jest.spyOn(AnalyticsCore, 'getGlobalScope').mockImplementation(() => {
// const globalThis = global as any;
// console.log("RETURNING GLOBAL SCOPE");
// return {
// MessageChannel: function () {
// return {
// port1: {
// onmessage: () => {},
// },
// port2: {
// postMessage: () => {},
// },
// };
// },
// ...globalThis,
// };
// });
// // (global as any).MessageChannel = function () {
// // return {
// // port1: {
// // onmessage: () => {},
// // },
// // port2: {
// // postMessage: () => {},
// // },
// // };
// // };
// });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just delete it?

Suggested change
// beforeAll(() => {
// // mock getGlobalScope
// jest.spyOn(AnalyticsCore, 'getGlobalScope').mockImplementation(() => {
// const globalThis = global as any;
// console.log("RETURNING GLOBAL SCOPE");
// return {
// MessageChannel: function () {
// return {
// port1: {
// onmessage: () => {},
// },
// port2: {
// postMessage: () => {},
// },
// };
// },
// ...globalThis,
// };
// });
// // (global as any).MessageChannel = function () {
// // return {
// // port1: {
// // onmessage: () => {},
// // },
// // port2: {
// // postMessage: () => {},
// // },
// // };
// // };
// });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants