From 7109b30ff6a274340b71daa3ff6d5d918fd464c3 Mon Sep 17 00:00:00 2001 From: Labreo <74888611+Labreo@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:03:26 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Pre-compute=20lowercase=20blocked?= =?UTF-8?q?=20album=20titles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- content.js | 38 ++++++++++-- tests/shouldBlockAlbum.test.js | 103 +++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 tests/shouldBlockAlbum.test.js diff --git a/content.js b/content.js index 4816eb2..b0a95d3 100644 --- a/content.js +++ b/content.js @@ -12,7 +12,8 @@ class YTMBlockController { // Local memory caches of blocked items to ensure instant lookups this.blockedArtists = []; this.blockedSongs = []; - this.blockedAlbums = []; + this._blockedAlbums = []; + this._precomputedBlockedAlbums = []; // Player observer references this.observer = null; @@ -2398,6 +2399,32 @@ class YTMBlockController { return null; } + /** + * Gets the list of blocked albums. + * @returns {Array} + */ + get blockedAlbums() { + return this._blockedAlbums; + } + + set blockedAlbums(albums) { + this._blockedAlbums = Array.isArray(albums) ? albums : []; + this._precomputedBlockedAlbums = this._blockedAlbums.map(blocked => ({ + raw: blocked, + lower: typeof blocked === 'string' ? blocked.toLowerCase().trim() : String(blocked || '').toLowerCase().trim() + })); + } + + _getPrecomputedBlockedAlbums() { + if (!this._precomputedBlockedAlbums || this._precomputedBlockedAlbums.length !== this._blockedAlbums.length) { + this._precomputedBlockedAlbums = (this._blockedAlbums || []).map(blocked => ({ + raw: blocked, + lower: typeof blocked === 'string' ? blocked.toLowerCase().trim() : String(blocked || '').toLowerCase().trim() + })); + } + return this._precomputedBlockedAlbums; + } + /** * Evaluates if the given album title matches any blocked album rules. * @param {string} albumTitle - The raw album title. @@ -2407,10 +2434,11 @@ class YTMBlockController { if (!albumTitle || this.blockedAlbums.length === 0) return null; const albumLower = albumTitle.toLowerCase().trim(); - for (const blocked of this.blockedAlbums) { - const blockedLower = blocked.toLowerCase().trim(); - if (albumLower === blockedLower || albumLower.includes(blockedLower)) { - return blocked; + const precomputed = this._getPrecomputedBlockedAlbums(); + for (let i = 0; i < precomputed.length; i++) { + const item = precomputed[i]; + if (albumLower === item.lower || albumLower.includes(item.lower)) { + return item.raw; } } return null; diff --git a/tests/shouldBlockAlbum.test.js b/tests/shouldBlockAlbum.test.js new file mode 100644 index 0000000..5eef0fc --- /dev/null +++ b/tests/shouldBlockAlbum.test.js @@ -0,0 +1,103 @@ +const test = require('node:test'); +const assert = require('node:assert'); +const { performance } = require('node:perf_hooks'); +const { YTMBlockController } = require('../content.js'); + +test('YTMBlockController - shouldBlockAlbum correctness', async (t) => { + const controller = new YTMBlockController(); + + await t.test('returns null when albumTitle is empty or blockedAlbums is empty', () => { + controller.blockedAlbums = ['Abbey Road']; + assert.strictEqual(controller.shouldBlockAlbum(''), null); + assert.strictEqual(controller.shouldBlockAlbum(null), null); + assert.strictEqual(controller.shouldBlockAlbum(undefined), null); + + controller.blockedAlbums = []; + assert.strictEqual(controller.shouldBlockAlbum('Abbey Road'), null); + }); + + await t.test('matches exact album title ignoring case and whitespace', () => { + controller.blockedAlbums = [' Abbey Road ', 'Dark Side of the Moon']; + assert.strictEqual(controller.shouldBlockAlbum('abbey road'), ' Abbey Road '); + assert.strictEqual(controller.shouldBlockAlbum('DARK SIDE OF THE MOON'), 'Dark Side of the Moon'); + }); + + await t.test('matches substring album title', () => { + controller.blockedAlbums = ['Abbey Road']; + assert.strictEqual(controller.shouldBlockAlbum('Abbey Road (Remastered 2009)'), 'Abbey Road'); + }); + + await t.test('returns null when album title does not match any blocked album', () => { + controller.blockedAlbums = ['Abbey Road', 'Dark Side of the Moon']; + assert.strictEqual(controller.shouldBlockAlbum('OK Computer'), null); + }); + + await t.test('updates precomputed cache when blockedAlbums property is updated', () => { + controller.blockedAlbums = ['Album 1']; + assert.strictEqual(controller.shouldBlockAlbum('Album 1'), 'Album 1'); + assert.strictEqual(controller.shouldBlockAlbum('Album 2'), null); + + controller.blockedAlbums = ['Album 2']; + assert.strictEqual(controller.shouldBlockAlbum('Album 1'), null); + assert.strictEqual(controller.shouldBlockAlbum('Album 2'), 'Album 2'); + }); +}); + +test('YTMBlockController - shouldBlockAlbum performance benchmark', async (t) => { + const controller = new YTMBlockController(); + + await t.test('benchmark pre-computed lowercase vs on-the-fly lowercasing', () => { + const blockedCount = 50; + const iterations = 10000; + const rawAlbums = []; + + for (let i = 0; i < blockedCount; i++) { + rawAlbums.push(` Album Title Number ${i} (Deluxe Edition) `); + } + + controller.blockedAlbums = rawAlbums; + const testAlbumTitle = 'Album Title Number 49 (Deluxe Edition) [2024 Remaster]'; + + // Unoptimized reference implementation for baseline comparison + function unoptimizedShouldBlockAlbum(albumTitle, blockedAlbums) { + if (!albumTitle || blockedAlbums.length === 0) return null; + const albumLower = albumTitle.toLowerCase().trim(); + for (const blocked of blockedAlbums) { + const blockedLower = blocked.toLowerCase().trim(); + if (albumLower === blockedLower || albumLower.includes(blockedLower)) { + return blocked; + } + } + return null; + } + + // Warmup JIT + for (let i = 0; i < 500; i++) { + unoptimizedShouldBlockAlbum(testAlbumTitle, rawAlbums); + controller.shouldBlockAlbum(testAlbumTitle); + } + + // Measure unoptimized baseline + const startBaseline = performance.now(); + for (let i = 0; i < iterations; i++) { + unoptimizedShouldBlockAlbum(testAlbumTitle, rawAlbums); + } + const durationBaseline = performance.now() - startBaseline; + + // Measure optimized pre-computed implementation + const startOptimized = performance.now(); + for (let i = 0; i < iterations; i++) { + controller.shouldBlockAlbum(testAlbumTitle); + } + const durationOptimized = performance.now() - startOptimized; + + const speedup = ((durationBaseline - durationOptimized) / durationBaseline) * 100; + + console.log(`[Benchmark] Baseline (on-the-fly lowercasing): ${durationBaseline.toFixed(2)}ms`); + console.log(`[Benchmark] Optimized (pre-computed lowercase): ${durationOptimized.toFixed(2)}ms`); + console.log(`[Benchmark] Speedup: ${speedup.toFixed(2)}%`); + + // Ensure optimized is faster than baseline + assert.ok(durationOptimized <= durationBaseline, 'Optimized implementation should be faster or equal to baseline'); + }); +});