-
-
Notifications
You must be signed in to change notification settings - Fork 553
Description
Is your feature request related to a problem? Please describe.
Issue: There's a TODO comment about a hot inner loop in src/TemplateMap.js lines 328-336:
// TODO(slightlyoff): hot inner loop?
getMapEntryForInputPath(inputPath) {
let absoluteInputPath = TemplatePath.absolutePath(inputPath);
return this.map.find((entry) => {
if (entry.inputPath === inputPath || entry.inputPath === absoluteInputPath) {
return entry;
}
});
}
Performance Issues Identified
- O(n) Linear Search: The method uses Array.find() which has O(n) time complexity
- Called Frequently: This method is called in multiple hot paths:
- cache() method: Line 280 - called for every template in orderedPaths.map()
- populateCollectionsWithContent(): Line 482 - called for every collection item
- initDependencyMap(): Line 185 - called for every dependency entry
- Various test scenarios and template processing workflows
Expensive Path Resolution: TemplatePath.absolutePath() is called on every lookup, even though the result could be cached - Scale Impact: With hundreds or thousands of templates, this becomes a significant bottleneck
Describe the solution you'd like
Proposed Solution
Replace the linear search with a Map-based lookup system:
- Add a lookup Map: Create this.inputPathMap alongside the existing this.map array
- Maintain both structures: Keep the array for iteration, add the Map for O(1) lookups.
- Handle both path formats: Store entries under both relative and absolute path keys.
- Update on modifications: Ensure the lookup Map stays in sync when entries are added
Describe alternatives you've considered
- Constructor Changes
Add the lookup Map initialization:
constructor(eleventyConfig) {
if (!eleventyConfig || eleventyConfig.constructor.name !== "TemplateConfig") {
throw new Error("Missing or invalid eleventyConfig
argument.");
}
this.eleventyConfig = eleventyConfig;
this.map = [];
// NEW: Add lookup Map for O(1) performance
this.inputPathMap = new Map();
this.collectionsData = null;
this.cached = false;
this.verboseOutput = true;
this.collection = new TemplateCollection();
}
- Enhanced add() Method
Modify the add method to populate both structures:
async add(template) {
if (!template) {
return;
}
let data = await template.getData();
let entries = await template.getTemplateMapEntries(data);
for (let map of entries) {
// Add to existing array (preserve existing functionality)
this.map.push(map);
// NEW: Add to lookup Map for fast access
this._addToInputPathMap(map);
}
}
// NEW: Helper method to add entries to the lookup Map
_addToInputPathMap(mapEntry) {
const inputPath = mapEntry.inputPath;
// Store under the original inputPath
this.inputPathMap.set(inputPath, mapEntry);
// Also store under absolute path if different
const absoluteInputPath = TemplatePath.absolutePath(inputPath);
if (absoluteInputPath !== inputPath) {
this.inputPathMap.set(absoluteInputPath, mapEntry);
}
}
- Optimized getMapEntryForInputPath() Method
Replace the linear search with Map lookup:
// OPTIMIZED: O(1) lookup instead of O(n) search
getMapEntryForInputPath(inputPath) {
// Try direct lookup first
let entry = this.inputPathMap.get(inputPath);
if (entry) {
return entry;
}
// Try absolute path lookup if direct lookup failed
let absoluteInputPath = TemplatePath.absolutePath(inputPath);
return this.inputPathMap.get(absoluteInputPath);
}
- Add Benchmarking Support
Add performance measurement to track improvements:
// NEW: Method to benchmark the performance improvement
_benchmarkLookupPerformance() {
if (this.map.length === 0) return;
const iterations = 1000;
const testPath = this.map[Math.floor(this.map.length / 2)].inputPath;
// Benchmark new method
const start = performance.now();
for (let i = 0; i < iterations; i++) {
this.getMapEntryForInputPath(testPath);
}
const mapTime = performance.now() - start;
console.log(`TemplateMap lookup performance: ${iterations} lookups in ${mapTime.toFixed(2)}ms`);
console.log(`Average per lookup: ${(mapTime / iterations).toFixed(4)}ms`);
}
- Add Map Clearing Method
For consistency and potential future use:
// NEW: Clear both data structures
clear() {
this.map = [];
this.inputPathMap.clear();
this.collectionsData = null;
this.cached = false;
}
- Add Validation Method
For debugging and ensuring data integrity:
// NEW: Validate that both data structures are in sync (for debugging)
_validateMapConsistency() {
if (process.env.NODE_ENV === 'development') {
const mapPaths = new Set(this.map.map(entry => entry.inputPath));
const lookupPaths = new Set();
for (let [path, entry] of this.inputPathMap) {
lookupPaths.add(entry.inputPath);
}
const arrayOnly = [...mapPaths].filter(path => !lookupPaths.has(path));
const mapOnly = [...lookupPaths].filter(path => !mapPaths.has(path));
if (arrayOnly.length > 0 || mapOnly.length > 0) {
console.warn('TemplateMap consistency issue:', { arrayOnly, mapOnly });
}
}
}
Complete Updated Methods
Here's how the key methods would look after the optimization:
class TemplateMap {
#dependencyMapInitialized = false;
constructor(eleventyConfig) {
if (!eleventyConfig || eleventyConfig.constructor.name !== "TemplateConfig") {
throw new Error("Missing or invalid `eleventyConfig` argument.");
}
this.eleventyConfig = eleventyConfig;
this.map = [];
this.inputPathMap = new Map(); // NEW: O(1) lookup Map
this.collectionsData = null;
this.cached = false;
this.verboseOutput = true;
this.collection = new TemplateCollection();
}
async add(template) {
if (!template) {
return;
}
let data = await template.getData();
let entries = await template.getTemplateMapEntries(data);
for (let map of entries) {
this.map.push(map);
this._addToInputPathMap(map); // NEW: Add to lookup Map
}
}
_addToInputPathMap(mapEntry) {
const inputPath = mapEntry.inputPath;
this.inputPathMap.set(inputPath, mapEntry);
const absoluteInputPath = TemplatePath.absolutePath(inputPath);
if (absoluteInputPath !== inputPath) {
this.inputPathMap.set(absoluteInputPath, mapEntry);
}
}
hasMapEntryForInputPath(inputPath) {
return Boolean(this.getMapEntryForInputPath(inputPath));
}
// OPTIMIZED: O(1) instead of O(n)
getMapEntryForInputPath(inputPath) {
let entry = this.inputPathMap.get(inputPath);
if (entry) {
return entry;
}
let absoluteInputPath = TemplatePath.absolutePath(inputPath);
return this.inputPathMap.get(absoluteInputPath);
}
// ... rest of the class remains the same
}
Additional context
No response