diff --git a/src/TraversionGraph.ts b/src/TraversionGraph.ts index e4bd3172..81a3b601 100644 --- a/src/TraversionGraph.ts +++ b/src/TraversionGraph.ts @@ -71,6 +71,12 @@ export class TraversionGraph { // Keeps track of path segments that have failed when attempted during the last run private temporaryDeadEnds: ConvertPathNode[][] = []; + // lookup caches rebuilt on every init() call + private nodeIndexByIdentifier = new Map(); + private handlerByName = new Map(); + private formatPriorityByHandler = new Map>(); + private handlerPairs = new Map(); + public addCategoryChangeCost(from: string, to: string, cost: number, handler?: string, updateIfExists: boolean = true) : boolean { if (this.hasCategoryChangeCost(from, to, handler)) { if (updateIfExists) { @@ -138,6 +144,19 @@ export class TraversionGraph { this.nodes.length = 0; this.edges.length = 0; + // rebuild lookup caches + this.nodeIndexByIdentifier.clear(); + this.handlerByName = new Map(handlers.map(h => [h.name, h])); + this.formatPriorityByHandler = new Map(handlers.map(h => [ + h.name, + new Map((h.supportedFormats ?? []).map((f, i) => [f.mime, i])) + ])); + this.handlerPairs = new Map( + this.categoryChangeCosts + .filter(c => c.handler) + .map(c => [`${c.from}->${c.to}`, c.handler!] as [string, string]) + ); + console.log("Initializing traversion graph..."); const startTime = performance.now(); @@ -147,7 +166,7 @@ export class TraversionGraph { let toIndices: Array<{format: FileFormat, index: number}> = []; formats.forEach(format => { const formatIdentifier = format.mime + `(${format.format})`; - let index = this.nodes.findIndex(node => node.identifier === formatIdentifier); + let index = this.nodeIndexByIdentifier.get(formatIdentifier) ?? -1; if (index === -1) { index = this.nodes.length; this.nodes.push({ @@ -155,6 +174,7 @@ export class TraversionGraph { format: format, edges: [] }); + this.nodeIndexByIdentifier.set(formatIdentifier, index); } if (format.from) fromIndices.push({format, index}); if (format.to) toIndices.push({format, index}); @@ -193,19 +213,24 @@ export class TraversionGraph { for (const f of formats) { if (!f.to) continue; const id = f.mime + `(${f.format})`; - const idx = this.nodes.findIndex(n => n.identifier === id); + const idx = this.nodeIndexByIdentifier.get(id) ?? -1; if (idx !== -1) toEntries.push({ format: f, index: idx }); } + // pre-build edge key set to avoid O(edges) scan per (node, output) pair + const existingEdgeKeys = new Set(); + for (const node of this.nodes) { + for (const eIdx of node.edges) { + const e = this.edges[eIdx]; + existingEdgeKeys.add(`${e.from.index}:${e.to.index}:${e.handler}`); + } + } + // Create edges from every known format node to each output format this.nodes.forEach((fromNode, fromIndex) => { for (const to of toEntries) { if (fromIndex === to.index) continue; - // Skip if an edge already exists from this node to this target via this handler - const alreadyExists = fromNode.edges.some(eIdx => - this.edges[eIdx].to.index === to.index && this.edges[eIdx].handler === handler.name - ); - if (alreadyExists) continue; + if (existingEdgeKeys.has(`${fromIndex}:${to.index}:${handler.name}`)) continue; const cost = this.costFunction( { format: fromNode.format, index: fromIndex }, @@ -240,8 +265,6 @@ export class TraversionGraph { ) { let cost = DEPTH_COST; // Base cost for each conversion step - const handlerPairs = new Map(this.categoryChangeCosts.filter(c => c.handler) - .map(c => [`${c.from}->${c.to}`, c.handler] as [string, string])); // Calculate category change cost const fromCategory = from.format.category || from.format.mime.split("/")[0]; const toCategory = to.format.category || to.format.mime.split("/")[0]; @@ -264,7 +287,7 @@ export class TraversionGraph { fromCategories.includes(c.from) && toCategories.includes(c.to) && ( - (!c.handler && handlerPairs.get(`${c.from}->${c.to}`) !== handler.toLowerCase()) + (!c.handler && this.handlerPairs.get(`${c.from}->${c.to}`) !== handler.toLowerCase()) || c.handler === handler.toLowerCase() ) ); @@ -282,8 +305,7 @@ export class TraversionGraph { cost += HANDLER_PRIORITY_COST * handlerIndex; // Add cost based on format priority - const handlerObj = this.handlers.find(h => h.name === handler) - cost += FORMAT_PRIORITY_COST * (handlerObj?.supportedFormats?.findIndex(f => f.mime === to.format.mime) ?? 0); + cost += FORMAT_PRIORITY_COST * (this.formatPriorityByHandler.get(handler)?.get(to.format.mime) ?? 0); // Add cost multiplier for lossy conversions if (!to.format.lossless) cost *= LOSSY_COST_MULTIPLIER; @@ -338,13 +360,13 @@ export class TraversionGraph { 1000, (a: QueueNode, b: QueueNode) => a.cost - b.cost ); - let visited = new Array(); + const visited = new Map(); // node index → insertion order const fromIdentifier = from.format.mime + `(${from.format.format})`; const toIdentifier = to.format.mime + `(${to.format.format})`; - let fromIndex = this.nodes.findIndex(node => node.identifier === fromIdentifier); - let toIndex = this.nodes.findIndex(node => node.identifier === toIdentifier); + const fromIndex = this.nodeIndexByIdentifier.get(fromIdentifier) ?? -1; + const toIndex = this.nodeIndexByIdentifier.get(toIdentifier) ?? -1; if (fromIndex === -1 || toIndex === -1) return []; // If either format is not in the graph, return empty array - queue.add({index: fromIndex, cost: 0, path: [from], visitedBorder: visited.length }); + queue.add({index: fromIndex, cost: 0, path: [from], visitedBorder: visited.size }); console.log(`Starting path search from ${from.format.mime}(${from.handler?.name}) to ${to.format.mime}(${to.handler?.name}) (simple mode: ${simpleMode})`); let iterations = 0; let pathsFound = 0; @@ -352,7 +374,7 @@ export class TraversionGraph { iterations++; // Get the node with the lowest cost let current = queue.poll()!; - const indexInVisited = visited.indexOf(current.index); + const indexInVisited = visited.get(current.index) ?? -1; if (indexInVisited >= 0 && indexInVisited < current.visitedBorder) { this.dispatchEvent("skipped", current.path); continue; @@ -373,13 +395,13 @@ export class TraversionGraph { } continue; } - visited.push(current.index); + if (!visited.has(current.index)) visited.set(current.index, visited.size); this.dispatchEvent("searching", current.path); this.nodes[current.index].edges.forEach(edgeIndex => { let edge = this.edges[edgeIndex]; - const indexInVisited = visited.indexOf(edge.to.index); + const indexInVisited = visited.get(edge.to.index) ?? -1; if (indexInVisited >= 0 && indexInVisited < current.visitedBorder) return; - const handler = this.handlers.find(h => h.name === edge.handler); + const handler = this.handlerByName.get(edge.handler); if (!handler) return; // If the handler for this edge is not found, skip it let path = current.path.concat({handler: handler, format: edge.to.format}); @@ -387,7 +409,7 @@ export class TraversionGraph { index: edge.to.index, cost: current.cost + edge.cost + this.calculateAdaptiveCost(path), path: path, - visitedBorder: visited.length + visitedBorder: visited.size }); }); if (iterations % LOG_FREQUENCY === 0) { diff --git a/src/main.ts b/src/main.ts index bbaf73ee..8745568e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -203,6 +203,8 @@ async function buildOptionList () { ui.inputList.innerHTML = ""; ui.outputList.innerHTML = ""; + const seenInputKeys = new Set(); + const seenOutputKeys = new Set(); for (const handler of handlers) { if (!window.supportedFormatCache.has(handler.name)) { console.warn(`Cache miss for formats of handler "${handler.name}".`); @@ -228,14 +230,9 @@ async function buildOptionList () { // In simple mode, display each input/output format only once let addToInputs = true, addToOutputs = true; if (simpleMode) { - addToInputs = !Array.from(ui.inputList.children).some(c => { - const currFormat = allOptions[parseInt(c.getAttribute("format-index") || "")]?.format; - return currFormat?.mime === format.mime && currFormat?.format === format.format; - }); - addToOutputs = !Array.from(ui.outputList.children).some(c => { - const currFormat = allOptions[parseInt(c.getAttribute("format-index") || "")]?.format; - return currFormat?.mime === format.mime && currFormat?.format === format.format; - }); + const dedupeKey = `${format.mime}|${format.format}`; + addToInputs = !seenInputKeys.has(dedupeKey); + addToOutputs = !seenOutputKeys.has(dedupeKey); if ((!format.from || !addToInputs) && (!format.to || !addToOutputs)) continue; } @@ -271,11 +268,13 @@ async function buildOptionList () { }; if (format.from && addToInputs) { + if (simpleMode) seenInputKeys.add(`${format.mime}|${format.format}`); const clone = newOption.cloneNode(true) as HTMLButtonElement; clone.onclick = clickHandler; ui.inputList.appendChild(clone); } if (format.to && addToOutputs) { + if (simpleMode) seenOutputKeys.add(`${format.mime}|${format.format}`); const clone = newOption.cloneNode(true) as HTMLButtonElement; clone.onclick = clickHandler; ui.outputList.appendChild(clone);