diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..fa29b20 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-04-24 - Pre-calculate derived search properties outside the render loop +**Learning:** In list rendering algorithms, calculating derived data (like full search strings and formatted dates) during the render or filter loops introduces significant unnecessary CPU overhead. The application was previously re-instantiating `Date` objects, calling `.toLocaleDateString()`, and concatenating multiple strings for every PDF item during every keystroke search and re-render. +**Action:** Always pre-calculate derived search properties (`_searchStr`, `_isNew`, `_formattedDate`) during the initial data load into the database/state. Use `Intl.DateTimeFormat` which is significantly faster than `.toLocaleDateString()`. Store this derived state on the items but ensure it is added *after* the core data is cached to `localStorage` to avoid cache bloat. Utilize early returns (`if (!condition) return false;`) in the filter loop to speed up traversal. diff --git a/patch_prepare.diff b/patch_prepare.diff new file mode 100644 index 0000000..aa40239 --- /dev/null +++ b/patch_prepare.diff @@ -0,0 +1,35 @@ +<<<<<<< SEARCH +async function loadPDFDatabase() { +======= +function prepareSearchIndex(data) { + const dateFormatter = new Intl.DateTimeFormat('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + const now = new Date().getTime(); + const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; + + for (let i = 0; i < data.length; i++) { + const pdf = data[i]; + + // 1. Precalculate search string + pdf._searchStr = ( + (pdf.title || '') + ' ' + + (pdf.description || '') + ' ' + + (pdf.category || '') + ' ' + + (pdf.author || '') + ).toLowerCase(); + + // 2. Precalculate date logic + const uploadDateObj = new Date(pdf.uploadDate); + if (!isNaN(uploadDateObj)) { + pdf._formattedDate = dateFormatter.format(uploadDateObj); + pdf._isNew = (now - uploadDateObj.getTime()) < SEVEN_DAYS; + } else { + pdf._formattedDate = 'Unknown Date'; + pdf._isNew = false; + } + } +} + +async function loadPDFDatabase() { +>>>>>>> REPLACE diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..938f5de --- /dev/null +++ b/plan.md @@ -0,0 +1,6 @@ +1. **Add `prepareSearchIndex` function in `script.js`**: Create a helper function to precalculate `_searchStr`, `_isNew`, and `_formattedDate` for all PDF objects. This will use an `Intl.DateTimeFormat` instance for optimal date formatting performance and validate timestamps using `!isNaN()`. +2. **Call `prepareSearchIndex` after data load**: Update `loadPDFDatabase` to call `prepareSearchIndex(pdfDatabase)` immediately after loading from cache or fetching from Firebase (after `localStorage.setItem` to avoid bloating the cache). +3. **Optimize `renderPDFs` filter**: Update the filter logic in `renderPDFs` to use early returns (`if (!condition) return false;`) and use the pre-calculated `_searchStr` with a truthiness guard instead of rebuilding the search string on every keypress. +4. **Update `createPDFCard`**: Update `createPDFCard` to use the pre-calculated `_isNew` and `_formattedDate` values, keeping inline calculation fallbacks for unindexed data (e.g. `if (isNew === undefined) { ... }`). +5. **Complete pre commit steps**: Ensure proper testing, verification, review, and reflection are done. +6. **Submit PR**: Commit and submit the code with a "⚡ Bolt: [performance improvement]" PR title and detailed description. diff --git a/script.js b/script.js index ef463ba..726ac01 100644 --- a/script.js +++ b/script.js @@ -452,6 +452,36 @@ async function syncClassSwitcher() { renderSemesterTabs(); } +function prepareSearchIndex(data) { + const dateFormatter = new Intl.DateTimeFormat('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + const now = new Date().getTime(); + const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; + + for (let i = 0; i < data.length; i++) { + const pdf = data[i]; + + // 1. Precalculate search string + pdf._searchStr = ( + (pdf.title || '') + ' ' + + (pdf.description || '') + ' ' + + (pdf.category || '') + ' ' + + (pdf.author || '') + ).toLowerCase(); + + // 2. Precalculate date logic + const uploadDateObj = new Date(pdf.uploadDate); + if (!isNaN(uploadDateObj)) { + pdf._formattedDate = dateFormatter.format(uploadDateObj); + pdf._isNew = (now - uploadDateObj.getTime()) < SEVEN_DAYS; + } else { + pdf._formattedDate = 'Unknown Date'; + pdf._isNew = false; + } + } +} + async function loadPDFDatabase() { if (isMaintenanceActive) return; @@ -490,6 +520,7 @@ async function loadPDFDatabase() { if (shouldUseCache) { pdfDatabase = cachedData; + prepareSearchIndex(pdfDatabase); // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -513,6 +544,8 @@ async function loadPDFDatabase() { data: pdfDatabase })); + prepareSearchIndex(pdfDatabase); + // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderPDFs(); @@ -948,26 +981,24 @@ function renderPDFs() { // Locate renderPDFs() in script.js and update the filter section const filteredPdfs = pdfDatabase.filter(pdf => { - const matchesSemester = pdf.semester === currentSemester; + if (pdf.semester !== currentSemester) return false; // NEW: Check if the PDF class matches the UI's current class selection // Note: If old documents don't have this field, they will be hidden. - const matchesClass = pdf.class === currentClass; + if (pdf.class !== currentClass) return false; - let matchesCategory = false; if (currentCategory === 'favorites') { - matchesCategory = favorites.includes(pdf.id); - } else { - matchesCategory = currentCategory === 'all' || pdf.category === currentCategory; + if (!favorites.includes(pdf.id)) return false; + } else if (currentCategory !== 'all' && pdf.category !== currentCategory) { + return false; } - const matchesSearch = pdf.title.toLowerCase().includes(searchTerm) || - pdf.description.toLowerCase().includes(searchTerm) || - pdf.category.toLowerCase().includes(searchTerm) || - pdf.author.toLowerCase().includes(searchTerm); + if (searchTerm) { + if (!pdf._searchStr) return false; // Safety for unindexed items + if (!pdf._searchStr.includes(searchTerm)) return false; + } - // Update return statement to include matchesClass - return matchesSemester && matchesClass && matchesCategory && matchesSearch; + return true; }); updatePDFCount(filteredPdfs.length); @@ -1037,9 +1068,23 @@ 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 for items that weren't indexed (e.g., from old cache) + if (isNew === undefined || !formattedDate) { + 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'; + } + } const newBadgeHTML = isNew ? `NEW` @@ -1053,11 +1098,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_perf.js b/test_perf.js new file mode 100644 index 0000000..cca6e4d --- /dev/null +++ b/test_perf.js @@ -0,0 +1,2 @@ +const fs = require('fs'); +let code = fs.readFileSync('script.js', 'utf8'); diff --git a/test_playwright.py b/test_playwright.py new file mode 100644 index 0000000..f2b346e --- /dev/null +++ b/test_playwright.py @@ -0,0 +1,193 @@ +import asyncio +from playwright.async_api import async_playwright + +async def main(): + async with async_playwright() as p: + browser = await p.chromium.launch() + page = await browser.new_page() + + # We will directly read the script and evaluate it to avoid loading dependencies or the full HTML + with open('script.js', 'r') as f: + script_content = f.read() + + await page.set_content(""" + +
+ + + + + + + """) + + # We define only the parts we need + await page.evaluate(""" + window.pdfDatabase = []; + window.currentClass = 'MSc Chemistry'; + window.currentSemester = 1; + window.currentCategory = 'all'; + window.pdfCount = document.getElementById('pdfCount'); + window.pdfGrid = document.getElementById('pdfGrid'); + window.emptyState = document.getElementById('emptyState'); + window.searchInput = document.getElementById('searchInput'); + window.searchTimeout = null; + window.GAS_URL = ''; + + window.getFavorites = () => []; + window.updatePDFCount = (c) => window.pdfCount.textContent = c; + window.logInteraction = () => {}; + window.escapeHtml = (text) => text ? text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'") : ''; + """) + + # Evaluate script.js logic (only the needed functions) + await page.evaluate(""" + window.prepareSearchIndex = function(data) { + const dateFormatter = new Intl.DateTimeFormat('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + const now = new Date().getTime(); + const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; + + for (let i = 0; i < data.length; i++) { + const pdf = data[i]; + + pdf._searchStr = ( + (pdf.title || '') + ' ' + + (pdf.description || '') + ' ' + + (pdf.category || '') + ' ' + + (pdf.author || '') + ).toLowerCase(); + + const uploadDateObj = new Date(pdf.uploadDate); + if (!isNaN(uploadDateObj)) { + pdf._formattedDate = dateFormatter.format(uploadDateObj); + pdf._isNew = (now - uploadDateObj.getTime()) < SEVEN_DAYS; + } else { + pdf._formattedDate = 'Unknown Date'; + pdf._isNew = false; + } + } + }; + + window.createPDFCard = function(pdf, favoritesList, index = 0, highlightRegex = null) { + const favorites = favoritesList || window.getFavorites(); + const isFav = favorites.includes(pdf.id); + const heartIconClass = isFav ? 'fas' : 'far'; + const btnActiveClass = isFav ? 'active' : ''; + + let isNew = pdf._isNew; + let formattedDate = pdf._formattedDate; + + if (isNew === undefined || !formattedDate) { + const uploadDateObj = new Date(pdf.uploadDate); + if (!isNaN(uploadDateObj)) { + const timeDiff = new Date() - uploadDateObj; + isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); + formattedDate = uploadDateObj.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + } else { + isNew = false; + formattedDate = 'Unknown Date'; + } + } + + const newBadgeHTML = isNew + ? `NEW` + : ''; + + const highlightText = (text) => { + const safeText = window.escapeHtml(text); + if (!highlightRegex) return safeText; + return safeText.replace(highlightRegex, '$1'); + }; + + return ` +