mirror of
https://github.com/discourse/discourse.git
synced 2024-12-02 03:53:38 +08:00
135fdd59ed
Time spent in the 'find module with suffix' portion of our `customResolve` function were adding up to around 100ms-150ms when booting the app. This time is spread over 150+ calls, so it's not immediately obvious in flamegraphs. This commit implements a (reversed) [Trie](https://en.wikipedia.org/wiki/Trie) which enables fast suffix-based lookups on a list of strings. In my tests, this requires < 5ms to initialize, and brings the cumulative 'find module with suffix' time down to `< 5ms`. This corresponds to a ~100ms improvement in LCP metrics in my browser. The only behavior change is to remove support for module filenames which are **not** dasherized. I haven't found any core/theme/plugin modules which are not dasherized in their filenames.
88 lines
2.5 KiB
JavaScript
88 lines
2.5 KiB
JavaScript
class TrieNode {
|
|
constructor(name, parent) {
|
|
this.name = name;
|
|
this.parent = parent;
|
|
this.children = new Map();
|
|
this.leafIndex = null;
|
|
}
|
|
}
|
|
|
|
// Given a set of strings, this class can allow efficient lookups
|
|
// based on suffixes.
|
|
//
|
|
// By default, it will create one Trie node per character. If your data
|
|
// has known delimiters (e.g. / in file paths), you can pass a separator
|
|
// to the constructor for better performance.
|
|
//
|
|
// Matching results will be returned in insertion order
|
|
export default class SuffixTrie {
|
|
constructor(separator = "") {
|
|
this._trie = new TrieNode();
|
|
this.separator = separator;
|
|
this._nextIndex = 0;
|
|
}
|
|
|
|
add(value) {
|
|
const nodeNames = value.split(this.separator);
|
|
let currentNode = this._trie;
|
|
|
|
// Iterate over the nodes backwards. The last one should be
|
|
// at the root of the tree
|
|
for (let i = nodeNames.length - 1; i >= 0; i--) {
|
|
let newNode = currentNode.children.get(nodeNames[i]);
|
|
if (!newNode) {
|
|
newNode = new TrieNode(nodeNames[i], currentNode);
|
|
currentNode.children.set(nodeNames[i], newNode);
|
|
}
|
|
currentNode = newNode;
|
|
}
|
|
|
|
currentNode.leafIndex = this._nextIndex++;
|
|
}
|
|
|
|
withSuffix(suffix, resultCount = null) {
|
|
const nodeNames = suffix.split(this.separator);
|
|
|
|
// Traverse the tree to find the root node for this suffix
|
|
let node = this._trie;
|
|
for (let i = nodeNames.length - 1; i >= 0; i--) {
|
|
node = node.children.get(nodeNames[i]);
|
|
if (!node) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Find all the leaves which are descendents of that node
|
|
const leaves = [];
|
|
const descendentNodes = [node];
|
|
while (descendentNodes.length > 0) {
|
|
const thisDescendent = descendentNodes.pop();
|
|
if (thisDescendent.leafIndex !== null) {
|
|
leaves.push(thisDescendent);
|
|
}
|
|
descendentNodes.push(...thisDescendent.children.values());
|
|
}
|
|
|
|
// Sort them in-place according to insertion order
|
|
leaves.sort((a, b) => (a.leafIndex < b.leafIndex ? -1 : 1));
|
|
|
|
// If a subset of results have been requested, truncate
|
|
if (resultCount !== null) {
|
|
leaves.splice(resultCount);
|
|
}
|
|
|
|
// Calculate their full names, and return the joined string
|
|
return leaves.map((leafNode) => {
|
|
const parts = [leafNode.name];
|
|
|
|
let ancestorNode = leafNode;
|
|
while (typeof ancestorNode.parent?.name === "string") {
|
|
parts.push(ancestorNode.parent.name);
|
|
ancestorNode = ancestorNode.parent;
|
|
}
|
|
|
|
return parts.join(this.separator);
|
|
});
|
|
}
|
|
}
|