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 @@
## 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()`.
70 changes: 70 additions & 0 deletions benchmark.js
Original file line number Diff line number Diff line change
@@ -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');
61 changes: 49 additions & 12 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
? `<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 @@ -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) => {
Expand Down
27 changes: 27 additions & 0 deletions test_prepare.js
Original file line number Diff line number Diff line change
@@ -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);
}
}