Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2025-04-27 - [Pre-calculate derived properties for list rendering]
**Learning:** Instantiating `Date` objects repeatedly inside a render or filter loop for large lists causes massive UI slowdowns. Calculating string concatenations and multiple `.toLowerCase()` calls for search fields per item per keypress exacerbates this issue.
**Action:** Always pre-calculate and store formatted date properties and search strings directly on the data objects during initial load, ensuring that expensive loops operate on pre-computed values, leading to O(1) attribute access per cycle.
152 changes: 152 additions & 0 deletions benchmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
const { performance } = require('perf_hooks');

// Generate mock data
const pdfDatabase = [];
for (let i = 0; i < 5000; i++) {
pdfDatabase.push({
id: `doc_${i}`,
semester: i % 2 === 0 ? '1' : '2',
class: i % 3 === 0 ? 'Math' : 'Science',
category: i % 4 === 0 ? 'favorites' : 'all',
title: `PDF Title ${i}`,
description: `This is a description for PDF ${i}. It has some text.`,
author: `Author ${i}`,
uploadDate: new Date(Date.now() - Math.random() * 10000000000).toISOString()
});
}

const currentSemester = '1';
const currentClass = 'Math';
const currentCategory = 'all';
const searchTerm = 'title 1';
const favorites = ['doc_0', 'doc_4'];

function oldFilter(pdfDatabase) {
return pdfDatabase.filter(pdf => {
const matchesSemester = pdf.semester === currentSemester;
const matchesClass = pdf.class === currentClass;

let matchesCategory = false;
if (currentCategory === 'favorites') {
matchesCategory = favorites.includes(pdf.id);
} else {
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);

return matchesSemester && matchesClass && matchesCategory && matchesSearch;
});
}

function prepareSearchIndex(data) {
const now = Date.now();
const formatter = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
data.forEach(pdf => {
pdf._searchStr = `${pdf.title||''} ${pdf.description||''} ${pdf.category||''} ${pdf.author||''}`.toLowerCase();
const d = new Date(pdf.uploadDate);
if (!isNaN(d)) {
pdf._isNew = (now - d) < 604800000;
pdf._formattedDate = formatter.format(d);
}
});
}

function newFilter(pdfDatabase) {
return pdfDatabase.filter(pdf => {
if (pdf.semester !== currentSemester) return false;
if (pdf.class !== currentClass) return false;

if (currentCategory === 'favorites') {
if (!favorites.includes(pdf.id)) return false;
} else if (currentCategory !== 'all' && pdf.category !== currentCategory) {
return false;
}

if (searchTerm) {
if (pdf._searchStr) {
if (!pdf._searchStr.includes(searchTerm)) return false;
} else {
const fallbackSearchStr = `${pdf.title || ''} ${pdf.description || ''} ${pdf.category || ''} ${pdf.author || ''}`.toLowerCase();
if (!fallbackSearchStr.includes(searchTerm)) return false;
}
}
return true;
});
}

function oldCardLoop(filteredPdfs) {
filteredPdfs.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'
});
});
}

function newCardLoop(filteredPdfs) {
filteredPdfs.forEach(pdf => {
let isNew = pdf._isNew;
if (isNew === undefined) {
const uploadDateObj = new Date(pdf.uploadDate);
if (!isNaN(uploadDateObj.getTime())) {
const timeDiff = new Date() - uploadDateObj;
isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days
} else {
isNew = false;
}
}
let formattedDate = pdf._formattedDate;
if (formattedDate === undefined) {
const uploadDateObj = new Date(pdf.uploadDate);
if (!isNaN(uploadDateObj.getTime())) {
formattedDate = uploadDateObj.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric'
});
} else {
formattedDate = 'Unknown Date';
}
}
});
}


console.log("Warming up...");
oldFilter(pdfDatabase);
prepareSearchIndex(pdfDatabase);
newFilter(pdfDatabase);
oldCardLoop(pdfDatabase);
newCardLoop(pdfDatabase);

const ITERATIONS = 100;

console.log("\nTesting Filter:");
let start = performance.now();
for (let i = 0; i < ITERATIONS; i++) oldFilter(pdfDatabase);
let oldTime = performance.now() - start;

start = performance.now();
for (let i = 0; i < ITERATIONS; i++) newFilter(pdfDatabase);
let newTime = performance.now() - start;

console.log(`Old Filter: ${oldTime.toFixed(2)}ms`);
console.log(`New Filter: ${newTime.toFixed(2)}ms`);
console.log(`Speedup: ${(oldTime/newTime).toFixed(2)}x`);

console.log("\nTesting Card Rendering (Date logic):");
start = performance.now();
for (let i = 0; i < ITERATIONS; i++) oldCardLoop(pdfDatabase);
let oldCardTime = performance.now() - start;

start = performance.now();
for (let i = 0; i < ITERATIONS; i++) newCardLoop(pdfDatabase);
let newCardTime = performance.now() - start;

console.log(`Old Card Loop: ${oldCardTime.toFixed(2)}ms`);
console.log(`New Card Loop: ${newCardTime.toFixed(2)}ms`);
console.log(`Speedup: ${(oldCardTime/newCardTime).toFixed(2)}x`);
95 changes: 72 additions & 23 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,29 @@ async function syncClassSwitcher() {
renderSemesterTabs();
}

/**
* ⚡ Bolt: Performance optimization
* Pre-calculate expensive derived properties (search strings, date formatting)
* during initial load to avoid recalculating them during every filter/render cycle.
*/
function prepareSearchIndex(data) {
const now = Date.now();
const formatter = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;

data.forEach(pdf => {
// Pre-compute lowercased search string
pdf._searchStr = `${pdf.title || ''} ${pdf.description || ''} ${pdf.category || ''} ${pdf.author || ''}`.toLowerCase();

// Pre-compute date-related properties to avoid allocating 'new Date()' in render loops
const d = new Date(pdf.uploadDate);
if (!isNaN(d.getTime())) {
pdf._isNew = (now - d.getTime()) < SEVEN_DAYS_MS;
pdf._formattedDate = formatter.format(d);
}
});
}

async function loadPDFDatabase() {
if (isMaintenanceActive) return;

Expand Down Expand Up @@ -490,6 +513,9 @@ async function loadPDFDatabase() {

if (shouldUseCache) {
pdfDatabase = cachedData;
// ⚡ Bolt: Prepare search index on cached data
prepareSearchIndex(pdfDatabase);

// --- FIX: CALL THIS TO POPULATE UI ---
syncClassSwitcher();
renderSemesterTabs();
Expand All @@ -508,13 +534,20 @@ async function loadPDFDatabase() {
pdfDatabase.push({ id: doc.id, ...doc.data() });
});

// ⚡ Bolt: Save core data to cache before adding derived properties
localStorage.setItem(CACHE_KEY, JSON.stringify({
timestamp: new Date().getTime(),
data: pdfDatabase
}));

// ⚡ Bolt: Prepare search index on fresh data
prepareSearchIndex(pdfDatabase);

// --- FIX: CALL THIS TO POPULATE UI ---
syncClassSwitcher();
// Force rendering category filters during fresh fetch to ensure consistency
renderCategoryFilters();
renderSemesterTabs();
renderPDFs();
hidePreloader();

Expand Down Expand Up @@ -948,26 +981,28 @@ function renderPDFs() {

// Locate renderPDFs() in script.js and update the filter section
const filteredPdfs = pdfDatabase.filter(pdf => {
const matchesSemester = pdf.semester === currentSemester;

// 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;
// ⚡ Bolt: Early returns for O(1) skipping instead of evaluating all conditions
if (pdf.semester !== currentSemester) return false;
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) {
// ⚡ Bolt: Use pre-calculated lowercased search string if available
if (pdf._searchStr) {
if (!pdf._searchStr.includes(searchTerm)) return false;
} else {
// Fallback for unindexed items
const fallbackSearchStr = `${pdf.title || ''} ${pdf.description || ''} ${pdf.category || ''} ${pdf.author || ''}`.toLowerCase();
if (!fallbackSearchStr.includes(searchTerm)) return false;
}
}

// Update return statement to include matchesClass
return matchesSemester && matchesClass && matchesCategory && matchesSearch;
return true;
});

updatePDFCount(filteredPdfs.length);
Expand Down Expand Up @@ -1037,9 +1072,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
// ⚡ Bolt: Fast path for pre-calculated properties
let isNew = pdf._isNew;
let formattedDate = pdf._formattedDate;

// Fallback for unindexed items
if (isNew === undefined || formattedDate === undefined) {
const uploadDateObj = new Date(pdf.uploadDate);
if (!isNaN(uploadDateObj.getTime())) {
if (isNew === undefined) {
const timeDiff = new Date() - uploadDateObj;
isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days
}
if (formattedDate === undefined) {
formattedDate = uploadDateObj.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric'
});
}
} else {
isNew = false;
formattedDate = 'Unknown Date';
}
}

const newBadgeHTML = isNew
? `<span style="background:var(--error-color); color:white; font-size:0.6rem; padding:2px 6px; border-radius:4px; margin-left:8px; vertical-align:middle;">NEW</span>`
Expand All @@ -1053,11 +1107,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) => {
Expand Down
32 changes: 32 additions & 0 deletions test_perf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const fs = require('fs');

// We'll read script.js and see what we're replacing.
const code = fs.readFileSync('script.js', 'utf8');

const regex = /const matchesSemester = pdf.semester === currentSemester;[\s\S]*?return matchesSemester && matchesClass && matchesCategory && matchesSearch;/g;
const match = regex.exec(code);
if (match) {
console.log("Found filter logic:");
console.log(match[0]);
}

const cardRegex = /const uploadDateStr = pdf.uploadDate \|\| Date.now\(\);[\s\S]*?day: 'numeric'\n \}\);/g;
const cardMatch = cardRegex.exec(code);
if (cardMatch) {
console.log("\nFound card date logic:");
console.log(cardMatch[0]);
}

const dbRegex = /if \(shouldUseCache\) \{[\s\S]*?hidePreloader\(\);\n return;\n \}/g;
const dbMatch = dbRegex.exec(code);
if (dbMatch) {
console.log("\nFound db cache logic:");
console.log(dbMatch[0]);
}

const fetchRegex = /localStorage.setItem\(CACHE_KEY, JSON.stringify\(\{[\s\S]*?hidePreloader\(\);/g;
const fetchMatch = fetchRegex.exec(code);
if (fetchMatch) {
console.log("\nFound db fetch logic:");
console.log(fetchMatch[0]);
}