diff --git a/packages/color-area/stories/memory-usage.stories.ts b/packages/color-area/stories/memory-usage.stories.ts
new file mode 100644
index 0000000000..9fec7cd8c2
--- /dev/null
+++ b/packages/color-area/stories/memory-usage.stories.ts
@@ -0,0 +1,235 @@
+/*
+Copyright 2023 Adobe. All rights reserved.
+This file is licensed to you 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 REPRESENTATIONS
+OF ANY KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License.
+*/
+
+import { html, TemplateResult } from '@spectrum-web-components/base';
+
+export default {
+ title: 'Color/Area/Memory Usage',
+ component: 'sp-color-area',
+ parameters: {
+ chromatic: { disableSnapshot: true },
+ },
+};
+
+// Story to demonstrate memory usage of ColorArea
+export const MemoryUsage = (): TemplateResult => {
+ return html`
+
+
Color Area Memory Usage Analysis
+
+ This story demonstrates the memory usage of the Color Area
+ component compared to other components. The Color Area component
+ uses canvas operations for color selection, which inherently
+ requires more memory than standard DOM elements.
+
+
+
+ Run Memory Test
+
+ Clear Results
+
+
+
+
+
+
+
Technical Explanation
+
+ The Color Area component has a higher memory footprint due
+ to:
+
+
+ - Canvas operations for rendering the color gradient
+ - Color transformation calculations (HSL, HSV, RGB)
+ -
+ Event handling for pointer and keyboard interactions
+
+ - ResizeObserver for responsive layout
+
+
+ This is expected behavior and not a memory leak. The
+ component properly cleans up resources in its
+ disconnectedCallback method, ensuring that memory usage
+ remains consistent over multiple create/destroy cycles.
+
+
+
+
+
+ `;
+};
diff --git a/packages/color-area/test/color-area-memory.test.ts b/packages/color-area/test/color-area-memory.test.ts
index fe5b807046..7e3334a489 100644
--- a/packages/color-area/test/color-area-memory.test.ts
+++ b/packages/color-area/test/color-area-memory.test.ts
@@ -11,8 +11,21 @@ governing permissions and limitations under the License.
import { html } from '@open-wc/testing';
import '@spectrum-web-components/color-area/sp-color-area.js';
-import { testForMemoryLeaks } from '../../../test/testing-helpers.js';
+import { testCanvasComponentMemory } from '../../../test/testing-helpers.js';
+import type { ColorArea } from '@spectrum-web-components/color-area';
-testForMemoryLeaks(html`
-
-`);
+// Test ColorArea memory patterns
+testCanvasComponentMemory(
+ html`
+
+ `,
+ {
+ componentName: 'ColorArea',
+ // Test color changes which trigger canvas redraws
+ manipulate: async (component) => {
+ const colorArea = component as ColorArea;
+ colorArea.color = { space: 'hsv', coords: [250, 90, 80] };
+ await colorArea.updateComplete;
+ },
+ }
+);
diff --git a/test/testing-helpers.ts b/test/testing-helpers.ts
index 34ac3b0dd9..8760e2a5e4 100644
--- a/test/testing-helpers.ts
+++ b/test/testing-helpers.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-console */
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
@@ -372,3 +373,174 @@ export function detectOS(): string | null {
return null;
}
+
+export async function forceGarbageCollection() {
+ // Run multiple GC attempts with delays
+ for (let i = 0; i < 3; i++) {
+ if (window.gc) {
+ window.gc();
+ } // Only works in dev mode with --expose-gc
+
+ // Create pressure
+ const pressure = new Array(1000000).fill(0);
+ pressure.length = 0;
+
+ await new Promise((r) => setTimeout(r, 100));
+ }
+}
+
+/**
+ * Format bytes to a readable format
+ */
+export function formatBytes(bytes: number) {
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
+}
+
+/**
+ * Tests a canvas-based component for acceptable memory usage patterns
+ *
+ * @param {import('@open-wc/testing').TemplateResult} template - The component template to test
+ * @param {Object} options - Test configuration options
+ * @param {string} options.componentName - Component name for logging purposes
+ * @param {number} [options.maxRetentionRatio=0.3] - Maximum acceptable retention ratio (0-1)
+ * @param {number} [options.maxGrowthRate=0.05] - Maximum acceptable average growth rate (0-1)
+ * @param {number} [options.cycles=3] - Number of cycles to test
+ * @param {Function} [options.manipulate] - Custom manipulation function
+ */
+export function testCanvasComponentMemory(
+ template: TemplateResult,
+ options: {
+ componentName: string;
+ maxRetentionRatio?: number;
+ maxGrowthRate?: number;
+ cycles?: number;
+ manipulate?: (component: HTMLElement) => Promise;
+ }
+) {
+ const {
+ componentName,
+ maxRetentionRatio = 0.3,
+ maxGrowthRate = 0.05,
+ cycles = 3,
+ manipulate,
+ } = options;
+
+ describe(`${componentName} Memory Usage`, () => {
+ it('shows acceptable memory patterns for canvas operations', async function () {
+ // Check if the API is available
+ if (
+ !window.gc ||
+ !('measureUserAgentSpecificMemory' in performance)
+ ) {
+ this.skip();
+ }
+
+ // Initial memory baseline
+ await forceGarbageCollection();
+ // @ts-expect-error - expect typescript error
+ const baseline = await performance.measureUserAgentSpecificMemory();
+
+ // Create component with canvas
+ const component = await fixture(template);
+ // @ts-expect-error - expect typescript error
+ await component.updateComplete;
+
+ // Run custom manipulation if provided
+ if (manipulate) {
+ await manipulate(component);
+ }
+
+ // First usage measurement
+ // @ts-expect-error - expect typescript error
+ // eslint-disable-next-line prettier/prettier
+ const firstUsage =
+ await performance.measureUserAgentSpecificMemory();
+
+ // Calculate initial allocation
+ const initialAllocation = firstUsage.bytes - baseline.bytes;
+ console.log(
+ `Initial ${componentName} allocation: ${formatBytes(initialAllocation)}`
+ );
+
+ // Remove component
+ component.remove();
+ await forceGarbageCollection();
+
+ // Measure after destruction
+ // @ts-expect-error - expect typescript error
+ // eslint-disable-next-line prettier/prettier
+ const afterDestroy =
+ await performance.measureUserAgentSpecificMemory();
+
+ // Calculate retained memory
+ const retainedMemory = afterDestroy.bytes - baseline.bytes;
+ const retentionRatio = retainedMemory / initialAllocation;
+ console.log(`Retained memory: ${formatBytes(retainedMemory)}`);
+ console.log(
+ `Retention ratio: ${(retentionRatio * 100).toFixed(2)}%`
+ );
+
+ // Check if retention is within acceptable limits
+ expect(retentionRatio).to.be.lessThan(
+ maxRetentionRatio,
+ `${componentName} should not retain more than ${maxRetentionRatio * 100}% of memory after destruction`
+ );
+
+ // Now test for unbounded growth with multiple cycles
+ let previousRetained = retainedMemory;
+ const growthRate: number[] = [];
+
+ // Run multiple cycles
+ for (let i = 0; i < cycles; i++) {
+ // Create new component
+ const newComponent = await fixture(template);
+ // @ts-expect-error - performance.measureUserAgentSpecificMemory() is not in the TypeScript types yet
+
+ await newComponent.updateComplete;
+
+ // Run custom manipulation if provided
+ if (manipulate) {
+ await manipulate(newComponent);
+ }
+
+ // Destroy component
+ newComponent.remove();
+ await forceGarbageCollection();
+
+ // Measure memory
+ // @ts-expect-error - performance.measureUserAgentSpecificMemory() is not in the TypeScript types yet
+
+ // eslint-disable-next-line prettier/prettier
+ const currentMemory =
+ await performance.measureUserAgentSpecificMemory();
+ const currentRetained = currentMemory.bytes - baseline.bytes;
+
+ // Calculate growth rate
+ if (previousRetained > 0) {
+ const cycle_growth =
+ (currentRetained - previousRetained) / previousRetained;
+ growthRate.push(cycle_growth as never);
+ console.log(
+ `Cycle ${i + 1} growth rate: ${(cycle_growth * 100).toFixed(2)}%`
+ );
+ }
+
+ previousRetained = currentRetained;
+ }
+
+ // Calculate average growth rate
+ const avgGrowthRate =
+ growthRate.reduce((sum, rate) => sum + rate, 0) /
+ growthRate.length;
+ console.log(
+ `Average growth rate per cycle: ${(avgGrowthRate * 100).toFixed(2)}%`
+ );
+
+ // Average growth rate should be small
+ expect(avgGrowthRate).to.be.lessThan(
+ maxGrowthRate,
+ `${componentName} should not grow more than ${maxGrowthRate * 100}% per cycle`
+ );
+ });
+ });
+}