diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..e89ddd5 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-18 - [Pre-calculate Search and Date Properties] +**Learning:** High-frequency loop performance in JavaScript (like array filtering and DOM string generation on lists of thousands of items) can be severely bottlenecked by creating intermediate strings (e.g., `toLowerCase()`, string concatenation) and instantiating complex objects (e.g., `new Date()`, `.toLocaleDateString()`) repeatedly. In this specific app's architecture, dynamically checking these inside `renderPDFs` filter loop and `createPDFCard` loop was creating huge GC pressure. +**Action:** When working with list data that needs to be filtered or rendered efficiently, pre-calculate derived properties (like a concatenated `_searchStr` or `_formattedDate`) during the initial data load/parse phase (e.g., inside a `prepareSearchIndex` function) rather than executing these operations inside O(N) loops. Always ensure truthiness guards during usage and inline fallbacks for backwards compatibility on unindexed objects. Use `Intl.DateTimeFormat` for efficient bulk date formatting over `.toLocaleDateString()`. diff --git a/benchmark.js b/benchmark.js new file mode 100644 index 0000000..319eb00 --- /dev/null +++ b/benchmark.js @@ -0,0 +1,70 @@ +const assert = require('assert'); + +// Mock data +const pdfDatabase = []; +for (let i = 0; i < 5000; i++) { + pdfDatabase.push({ + id: `pdf_${i}`, + title: `Mock PDF Title ${i}`, + description: `This is a mock description for pdf ${i}. It contains some keywords.`, + category: i % 2 === 0 ? 'Organic' : 'Inorganic', + author: `Author ${i}`, + uploadDate: new Date(Date.now() - Math.random() * 10000000000).toISOString() + }); +} + +const searchTerm = 'keyword'; + +// --- Before Optimization --- +console.time('Before Render/Filter 1'); +const filtered1 = pdfDatabase.filter(pdf => { + return pdf.title.toLowerCase().includes(searchTerm) || + pdf.description.toLowerCase().includes(searchTerm) || + pdf.category.toLowerCase().includes(searchTerm) || + pdf.author.toLowerCase().includes(searchTerm); +}); +console.timeEnd('Before Render/Filter 1'); + +console.time('Before CreateCard'); +filtered1.forEach(pdf => { + const uploadDateObj = new Date(pdf.uploadDate); + const timeDiff = new Date() - uploadDateObj; + const isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); + + const formattedDate = new Date(pdf.uploadDate).toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); +}); +console.timeEnd('Before CreateCard'); + +// --- After Optimization --- +console.time('Prepare Index'); +const dateFormatter = new Intl.DateTimeFormat('en-US', { + year: 'numeric', month: 'short', day: 'numeric' +}); +const now = new Date(); +pdfDatabase.forEach(pdf => { + pdf._searchStr = `${pdf.title || ''} ${pdf.description || ''} ${pdf.category || ''} ${pdf.author || ''}`.toLowerCase(); + if (pdf.uploadDate) { + const uploadDateObj = new Date(pdf.uploadDate); + if (!isNaN(uploadDateObj)) { + pdf._formattedDate = dateFormatter.format(uploadDateObj); + const timeDiff = now - uploadDateObj; + pdf._isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); + } + } +}); +console.timeEnd('Prepare Index'); + +console.time('After Render/Filter 1'); +const filtered2 = pdfDatabase.filter(pdf => { + return pdf._searchStr ? pdf._searchStr.includes(searchTerm) : false; +}); +console.timeEnd('After Render/Filter 1'); + +console.time('After CreateCard'); +filtered2.forEach(pdf => { + let isNew = pdf._isNew; + let formattedDate = pdf._formattedDate; +}); +console.timeEnd('After CreateCard'); diff --git a/script.js b/script.js index 22f399d..2774a8c 100644 --- a/script.js +++ b/script.js @@ -416,6 +416,26 @@ async function syncClassSwitcher() { renderSemesterTabs(); } +function prepareSearchIndex() { + const dateFormatter = new Intl.DateTimeFormat('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + const now = new Date(); + + pdfDatabase.forEach(pdf => { + pdf._searchStr = `${pdf.title || ''} ${pdf.description || ''} ${pdf.category || ''} ${pdf.author || ''}`.toLowerCase(); + + if (pdf.uploadDate) { + const uploadDateObj = new Date(pdf.uploadDate); + if (!isNaN(uploadDateObj)) { + pdf._formattedDate = dateFormatter.format(uploadDateObj); + const timeDiff = now - uploadDateObj; + pdf._isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); + } + } + }); +} + async function loadPDFDatabase() { if (isMaintenanceActive) return; @@ -454,6 +474,7 @@ async function loadPDFDatabase() { if (shouldUseCache) { pdfDatabase = cachedData; + prepareSearchIndex(); // NEW: Calculate derived runtime properties // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -477,6 +498,8 @@ async function loadPDFDatabase() { data: pdfDatabase })); + prepareSearchIndex(); // NEW: Calculate derived runtime properties after caching + // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderPDFs(); @@ -918,10 +941,10 @@ function renderPDFs() { matchesCategory = currentCategory === 'all' || pdf.category === currentCategory; } - const matchesSearch = pdf.title.toLowerCase().includes(searchTerm) || - pdf.description.toLowerCase().includes(searchTerm) || - pdf.category.toLowerCase().includes(searchTerm) || - pdf.author.toLowerCase().includes(searchTerm); + let matchesSearch = true; + if (searchTerm) { + matchesSearch = pdf._searchStr ? pdf._searchStr.includes(searchTerm) : false; + } // Update return statement to include matchesClass return matchesSemester && matchesClass && matchesCategory && matchesSearch; @@ -994,9 +1017,28 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) { const heartIconClass = isFav ? 'fas' : 'far'; const btnActiveClass = isFav ? 'active' : ''; - const uploadDateObj = new Date(pdf.uploadDate); - const timeDiff = new Date() - uploadDateObj; - const isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days + let isNew = pdf._isNew; + let formattedDate = pdf._formattedDate; + + // Fallback if index wasn't prepared (e.g. unindexed or legacy cached data) + if (isNew === undefined || !formattedDate) { + if (pdf.uploadDate) { + const uploadDateObj = new Date(pdf.uploadDate); + if (!isNaN(uploadDateObj)) { + const timeDiff = new Date() - uploadDateObj; + isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days + formattedDate = uploadDateObj.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + } else { + isNew = false; + formattedDate = "Unknown Date"; + } + } else { + isNew = false; + formattedDate = "Unknown Date"; + } + } const newBadgeHTML = isNew ? `NEW` @@ -1010,11 +1052,6 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) { }; const categoryIcon = categoryIcons[pdf.category] || 'fa-file-pdf'; - // Formatting Date - const formattedDate = new Date(pdf.uploadDate).toLocaleDateString('en-US', { - year: 'numeric', month: 'short', day: 'numeric' - }); - // Uses global escapeHtml() now const highlightText = (text) => { diff --git a/test_prepare.js b/test_prepare.js new file mode 100644 index 0000000..652acff --- /dev/null +++ b/test_prepare.js @@ -0,0 +1,27 @@ +const fs = require('fs'); + +// We will test if our changes broke anything basic +const scriptContent = fs.readFileSync('script.js', 'utf-8'); + +// Quick test to see if we can parse the script +try { + global.localStorage = { getItem: () => null, setItem: () => {} }; + global.document = { + getElementById: () => ({ classList: { add: () => {}, remove: () => {} }, style: {}, innerHTML: '', addEventListener: () => {} }), + querySelectorAll: () => [], + querySelector: () => ({ classList: { add: () => {}, remove: () => {} }, style: {}, setAttribute: () => {} }), + createElement: () => ({ classList: { add: () => {} }, style: {} }), + documentElement: { setAttribute: () => {}, getAttribute: () => {} }, + addEventListener: () => {} + }; + global.window = { location: { search: '' }, addEventListener: () => {}, matchMedia: () => ({ matches: false }), history: { pushState: () => {}, replaceState: () => {} }, scrollY: 0, scrollTo: () => {} }; + global.navigator = { platform: 'test', userAgent: 'test' }; + eval(scriptContent + '\n\nconsole.log("Script evaluated successfully with mocks.");'); +} catch (e) { + if (e.message.includes('firebase is not defined') || e.message.includes('document is not defined')) { + console.log("Expected error in node env:", e.message); + } else { + console.error("Unexpected error:", e); + process.exit(1); + } +}