diff --git a/benchmark/cleanList.bench.js b/benchmark/cleanList.bench.js new file mode 100644 index 0000000..ee42b21 --- /dev/null +++ b/benchmark/cleanList.bench.js @@ -0,0 +1,119 @@ +const { performance } = require('perf_hooks'); +const { normalizeEntry } = require('../storage.js'); + +// Current implementation of cleanList (forEach) +function currentCleanList(list) { + let migrated = false; + const seen = new Set(); + const cleaned = []; + list.forEach(item => { + const norm = normalizeEntry(item); + if (norm && !seen.has(norm)) { + seen.add(norm); + cleaned.push(norm); + } + }); + if (cleaned.length !== list.length) { + migrated = true; + } + return cleaned; +} + +// Option A: Standard for loop +function cleanListForLoop(list) { + let migrated = false; + const seen = new Set(); + const cleaned = []; + const len = list.length; + for (let i = 0; i < len; i++) { + const norm = normalizeEntry(list[i]); + if (norm && !seen.has(norm)) { + seen.add(norm); + cleaned.push(norm); + } + } + if (cleaned.length !== len) { + migrated = true; + } + return cleaned; +} + +// Option B: Array.from + Set +function cleanListArrayFrom(list) { + let migrated = false; + const seen = new Set(); + const cleaned = []; + for (let i = 0; i < list.length; i++) { + const norm = normalizeEntry(list[i]); + if (norm && !seen.has(norm)) { + seen.add(norm); + cleaned.push(norm); + } + } + if (cleaned.length !== list.length) { + migrated = true; + } + return cleaned; +} + +// Option C: Set initialized directly from filtered normalized items +function cleanListSetFrom(list) { + let migrated = false; + const cleaned = Array.from(new Set( + list + .map(normalizeEntry) + .filter(Boolean) + )); + if (cleaned.length !== list.length) { + migrated = true; + } + return cleaned; +} + +function generateDataset(size) { + const dataset = []; + const sampleArtists = [ + ' Taylor Swift ', 'taylor swift', 'DRAKE', 'drake', ' \u200B Kanye \u200D WEST \uFEFF ', + 'Coldplay', 'COLDPLAY', 'Radiohead - Karma Police', 'The Beatles', ' The Beatles ', + '', ' ', null, undefined, 123 + ]; + + for (let i = 0; i < size; i++) { + const sample = sampleArtists[i % sampleArtists.length]; + dataset.push(sample); + } + return dataset; +} + +function benchmark(fn, list, iterations = 1000) { + for (let i = 0; i < 50; i++) fn(list); + + const start = performance.now(); + for (let i = 0; i < iterations; i++) { + fn(list); + } + const end = performance.now(); + return end - start; +} + +function runBenchmarks() { + const sizes = [10, 100, 1000, 10000]; + console.log('=== Performance Benchmarks Comparison ===\n'); + + sizes.forEach(size => { + const dataset = generateDataset(size); + const iterations = size >= 10000 ? 200 : (size >= 1000 ? 2000 : 20000); + + const timeCurrent = benchmark(currentCleanList, dataset, iterations); + const timeForLoop = benchmark(cleanListForLoop, dataset, iterations); + const timeSetFrom = benchmark(cleanListSetFrom, dataset, iterations); + + console.log(`Dataset size: ${size} items (${iterations} iterations)`); + console.log(` Current (forEach): ${timeCurrent.toFixed(3)} ms`); + console.log(` For loop (cleanListForLoop): ${timeForLoop.toFixed(3)} ms (${((timeCurrent - timeForLoop) / timeCurrent * 100).toFixed(2)}% improvement)`); + console.log(` Array.from(new Set) (SetFrom): ${timeSetFrom.toFixed(3)} ms (${((timeCurrent - timeSetFrom) / timeCurrent * 100).toFixed(2)}% improvement)`); + console.log(''); + }); +} + +runBenchmarks(); diff --git a/storage.js b/storage.js index b76a74a..5908607 100644 --- a/storage.js +++ b/storage.js @@ -59,14 +59,15 @@ function getBlockData() { const cleanList = (list) => { const seen = new Set(); const cleaned = []; - list.forEach(item => { - const norm = normalizeEntry(item); + const len = list.length; + for (let i = 0; i < len; i++) { + const norm = normalizeEntry(list[i]); if (norm && !seen.has(norm)) { seen.add(norm); cleaned.push(norm); } - }); - if (cleaned.length !== list.length) { + } + if (cleaned.length !== len) { migrated = true; } return cleaned; diff --git a/test/cleanList.test.js b/test/cleanList.test.js new file mode 100644 index 0000000..63bb0ed --- /dev/null +++ b/test/cleanList.test.js @@ -0,0 +1,88 @@ +const { describe, it, beforeEach } = require('node:test'); +const assert = require('node:assert'); + +// Setup mock chrome API before loading storage +let storageData = {}; +let storageSaved = null; +let lastError = null; + +global.chrome = { + runtime: { + get lastError() { return lastError; } + }, + storage: { + sync: { + get: (keys, callback) => { + const res = {}; + keys.forEach(k => { + if (k in storageData) res[k] = storageData[k]; + }); + callback(res); + }, + set: (data, callback) => { + storageSaved = data; + Object.assign(storageData, data); + if (callback) callback(); + } + } + } +}; + +const { getBlockData, addBlockedItem, removeBlockedItem } = require('../storage.js'); + +describe('getBlockData and list cleaning / deduplication', () => { + beforeEach(() => { + storageData = {}; + storageSaved = null; + lastError = null; + }); + + it('should clean, normalize, and de-duplicate lists correctly', async () => { + storageData = { + blockedArtists: [' Taylor Swift ', 'taylor swift', 'DRAKE', 'drake', ''], + blockedSongs: ['song 1', 'SONG 1', ' song 2 ', null, 123], + blockedAlbums: ['Album A', 'album a', 'Album B'] + }; + + const data = await getBlockData(); + + assert.deepStrictEqual(data.blockedArtists, ['taylor swift', 'drake']); + assert.deepStrictEqual(data.blockedSongs, ['song 1', 'song 2']); + assert.deepStrictEqual(data.blockedAlbums, ['album a', 'album b']); + + // Should have saved migrated data back to storage because cleanup changed length + assert.ok(storageSaved); + assert.deepStrictEqual(storageSaved.blockedArtists, ['taylor swift', 'drake']); + }); + + it('should not mark migrated if lists are already clean and normalized', async () => { + storageData = { + blockedArtists: ['drake', 'taylor swift'], + blockedSongs: ['song 1', 'song 2'], + blockedAlbums: ['album a'] + }; + + const data = await getBlockData(); + + assert.deepStrictEqual(data.blockedArtists, ['drake', 'taylor swift']); + assert.deepStrictEqual(data.blockedSongs, ['song 1', 'song 2']); + assert.deepStrictEqual(data.blockedAlbums, ['album a']); + + // No set call if not migrated + assert.strictEqual(storageSaved, null); + }); + + it('should set migrated if undefined lists are returned', async () => { + storageData = {}; + + const data = await getBlockData(); + + assert.deepStrictEqual(data, { + blockedArtists: [], + blockedSongs: [], + blockedAlbums: [] + }); + + assert.ok(storageSaved); + }); +});