More optimisations

These ones stemming from a wiki with 80K tiddlers and 5K tags, and a total wiki size of 150MB.

> The biggest win is the P50 (median) refresh dropping from 124ms to 46ms — a 63% improvement
This commit is contained in:
Jeremy Ruston
2026-03-14 09:05:36 +00:00
parent b20b578183
commit b378f3f462
9 changed files with 202 additions and 32 deletions
+50
View File
@@ -1154,6 +1154,30 @@ $tw.Wiki = function(options) {
pluginTiddlers = [], // Array of tiddlers containing registered plugins, ordered by priority
pluginInfo = Object.create(null), // Hashmap of parsed plugin content
shadowTiddlers = Object.create(null), // Hashmap by title of {source:, tiddler:}
systemTiddlerTitles = null, // Array of system tiddler titles (starting with "$:/")
nonSystemTiddlerTitles = null, // Array of non-system tiddler titles
partitionTiddlerTitles = function() {
if(systemTiddlerTitles === null) {
systemTiddlerTitles = [];
nonSystemTiddlerTitles = [];
var titles = getTiddlerTitles();
for(var i = 0, length = titles.length; i < length; i++) {
if(titles[i].indexOf("$:/") === 0) {
systemTiddlerTitles.push(titles[i]);
} else {
nonSystemTiddlerTitles.push(titles[i]);
}
}
}
},
getSystemTiddlerTitles = function() {
partitionTiddlerTitles();
return systemTiddlerTitles;
},
getNonSystemTiddlerTitles = function() {
partitionTiddlerTitles();
return nonSystemTiddlerTitles;
},
shadowTiddlerTitles = null,
getShadowTiddlerTitles = function() {
if(!shadowTiddlerTitles) {
@@ -1202,6 +1226,14 @@ $tw.Wiki = function(options) {
tiddlers[title] = tiddler;
// Check we've got the title
tiddlerTitles = $tw.utils.insertSortedArray(tiddlerTitles || [],title);
// Maintain system/non-system partitions
if(systemTiddlerTitles !== null) {
if(title.indexOf("$:/") === 0) {
$tw.utils.insertSortedArray(systemTiddlerTitles,title);
} else {
$tw.utils.insertSortedArray(nonSystemTiddlerTitles,title);
}
}
// Record the new tiddler state
updateDescriptor["new"] = {
tiddler: tiddler,
@@ -1242,6 +1274,14 @@ $tw.Wiki = function(options) {
tiddlerTitles.splice(index,1);
}
}
// Delete from system/non-system partitions
if(systemTiddlerTitles !== null) {
var partitionArray = title.indexOf("$:/") === 0 ? systemTiddlerTitles : nonSystemTiddlerTitles;
var partitionIndex = partitionArray.indexOf(title);
if(partitionIndex !== -1) {
partitionArray.splice(partitionIndex,1);
}
}
// Record the new tiddler state
updateDescriptor["new"] = {
tiddler: this.getTiddler(title),
@@ -1280,6 +1320,16 @@ $tw.Wiki = function(options) {
return getTiddlerTitles().slice(0);
};
// Get an array of all system tiddler titles (returns cached array; do not mutate)
this.allSystemTitles = function() {
return getSystemTiddlerTitles();
};
// Get an array of all non-system tiddler titles (returns cached array; do not mutate)
this.allNonSystemTitles = function() {
return getNonSystemTiddlerTitles();
};
// Iterate through all tiddler titles
this.each = function(callback) {
var titles = getTiddlerTitles(),
+1 -1
View File
@@ -23,7 +23,7 @@ $tw.boot = $tw.boot || Object.create(null);
// Config
$tw.config = $tw.config || Object.create(null);
$tw.config.maxEditFileSize = 100 * 1024 * 1024; // 100MB
$tw.config.maxEditFileSize = 200 * 1024 * 1024; // 200MB
// Detect platforms
if(!("browser" in $tw)) {
+34 -17
View File
@@ -261,6 +261,7 @@ exports.compileFilter = function(filterString) {
var filterOperators = this.getFilterOperators();
// Assemble array of functions, one for each operation
var operationFunctions = [];
var operationSubFunctions = []; // Unwrapped sub-functions for fast path
// Step through the operations
var self = this;
$tw.utils.each(filterParseTree,function(operation) {
@@ -289,20 +290,24 @@ exports.compileFilter = function(filterString) {
operand.value = self.getTextReference(operand.text,"",currTiddlerTitle);
operand.multiValue = [operand.value];
} else if(operand.variable) {
var varTree = $tw.utils.parseFilterVariable(operand.text);
operand.value = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source})[0] || "";
if(!operand._varTree) {
operand._varTree = $tw.utils.parseFilterVariable(operand.text);
}
operand.value = widgetClass.evaluateVariable(widget,operand._varTree.name,{params: operand._varTree.params, source: source})[0] || "";
operand.multiValue = [operand.value];
} else if(operand.multiValuedVariable) {
var varTree = $tw.utils.parseFilterVariable(operand.text);
var resultList = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source});
if(!operand._varTree) {
operand._varTree = $tw.utils.parseFilterVariable(operand.text);
}
var resultList = widgetClass.evaluateVariable(widget,operand._varTree.name,{params: operand._varTree.params, source: source});
if((resultList.length > 0 && resultList[0] !== undefined) || resultList.length === 0) {
operand.multiValue = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source}) || [];
operand.multiValue = widgetClass.evaluateVariable(widget,operand._varTree.name,{params: operand._varTree.params, source: source}) || [];
operand.value = operand.multiValue[0] || "";
} else {
operand.value = "";
operand.multiValue = [];
}
operand.isMultiValueOperand = true;
operand.isMultiValueOperand = true;
} else {
operand.value = operand.text;
operand.multiValue = [operand.value];
@@ -343,6 +348,7 @@ exports.compileFilter = function(filterString) {
return resultArray;
}
};
operationSubFunctions.push(operationSubFunction);
var filterRunPrefixes = self.getFilterRunPrefixes();
// Wrap the operator functions in a wrapper function that depends on the prefix
operationFunctions.push((function() {
@@ -372,6 +378,10 @@ exports.compileFilter = function(filterString) {
}
})());
});
// Detect single "or" run for fast path (bypass LinkedList)
var isSingleOrRun = filterParseTree.length === 1 &&
(!filterParseTree[0].prefix || filterParseTree[0].prefix === "");
var singleOrSubFunction = isSingleOrRun ? operationSubFunctions[0] : null;
// Return a function that applies the operations to a source iterator of tiddler titles
var fnMeasured = $tw.perf.measure("filter: " + filterString,function filterFunction(source,widget) {
if(!source) {
@@ -382,23 +392,30 @@ exports.compileFilter = function(filterString) {
if(!widget) {
widget = $tw.rootWidget;
}
var results = new $tw.utils.LinkedList();
self.filterRecursionCount = (self.filterRecursionCount || 0) + 1;
var resultArray;
if(self.filterRecursionCount < MAX_FILTER_DEPTH) {
$tw.utils.each(operationFunctions,function(operationFunction) {
var operationResult = operationFunction(results,source,widget);
if(operationResult) {
if(operationResult.variables) {
// If the filter run prefix has returned variables, create a new fake widget with those variables
widget = widget.makeFakeWidgetWithVariables(operationResult.variables);
if(singleOrSubFunction) {
// Fast path: single "or" run, return array directly without LinkedList
resultArray = singleOrSubFunction(source,widget);
} else {
var results = new $tw.utils.LinkedList();
$tw.utils.each(operationFunctions,function(operationFunction) {
var operationResult = operationFunction(results,source,widget);
if(operationResult) {
if(operationResult.variables) {
// If the filter run prefix has returned variables, create a new fake widget with those variables
widget = widget.makeFakeWidgetWithVariables(operationResult.variables);
}
}
}
});
});
resultArray = results.toArray();
}
} else {
results.push("/**-- Excessive filter recursion --**/");
resultArray = ["/**-- Excessive filter recursion --**/"];
}
self.filterRecursionCount = self.filterRecursionCount - 1;
return results.toArray();
return resultArray;
});
if(this.filterCacheCount >= 2000) {
// To prevent memory leak, we maintain an upper limit for cache size.
+12
View File
@@ -13,6 +13,18 @@ Filter function for [is[shadow]]
Export our filter function
*/
exports.shadow = function(source,prefix,options) {
// Fast path: when source is wiki.each (all real tiddlers), use shadow title list
if(source === options.wiki.each && prefix !== "!") {
// Return real tiddlers that are also shadow tiddlers (overridden shadows)
var results = [],
shadowTitles = options.wiki.allShadowTitles();
for(var i = 0, len = shadowTitles.length; i < len; i++) {
if(options.wiki.tiddlerExists(shadowTitles[i])) {
results.push(shadowTitles[i]);
}
}
return results;
}
var results = [];
if(prefix === "!") {
source(function(tiddler,title) {
+8
View File
@@ -13,6 +13,14 @@ Filter function for [is[system]]
Export our filter function
*/
exports.system = function(source,prefix,options) {
// Fast path: when iterating all tiddlers, use pre-partitioned arrays
if(source === options.wiki.each) {
if(prefix === "!") {
return options.wiki.allNonSystemTitles();
} else {
return options.wiki.allSystemTitles();
}
}
var results = [];
if(prefix === "!") {
source(function(tiddler,title) {
+7
View File
@@ -13,6 +13,13 @@ Filter function for [is[tiddler]]
Export our filter function
*/
exports.tiddler = function(source,prefix,options) {
// Fast path: wiki.each only iterates real tiddlers, all of which exist
if(source === options.wiki.each) {
if(prefix === "!") {
return []; // No real tiddler fails tiddlerExists
}
return source; // Return iterator directly; all real tiddlers pass
}
var results = [];
if(prefix === "!") {
source(function(tiddler,title) {
+15
View File
@@ -13,6 +13,21 @@ Filter operator returning all the tags of the selected tiddlers
Export our filter function
*/
exports.tags = function(source,operator,options) {
// Fast path: cache result when iterating all tiddlers
if(source === options.wiki.each) {
return options.wiki.getGlobalCache("filter-tags-all-tiddlers",function() {
var tags = {};
source(function(tiddler,title) {
var t, length;
if(tiddler && tiddler.fields.tags) {
for(t=0, length=tiddler.fields.tags.length; t<length; t++) {
tags[tiddler.fields.tags[t]] = true;
}
}
});
return Object.keys(tags);
});
}
var tags = {};
source(function(tiddler,title) {
var t, length;
+42 -8
View File
@@ -59,14 +59,48 @@ LinkedList.prototype.push = function(/* values */) {
LinkedList.prototype.pushTop = function(value) {
var t;
if($tw.utils.isArray(value)) {
for(t=0; t<value.length; t++) {
_assertString(value[t]);
}
for(t=0; t<value.length; t++) {
_removeOne(this,value[t]);
}
for(t=0; t<value.length; t++) {
_linkToEnd(this,value[t]);
if(this.length === 0) {
// Fast path for empty list: skip removal pass
for(t = 0; t < value.length; t++) {
_assertString(value[t]);
}
var prev = null,
useInline = true;
for(t = 0; t < value.length; t++) {
if(useInline) {
var v = value[t];
var old = this.next.get(v);
if(old !== undefined) {
// Duplicate found: switch to _linkToEnd for this and all remaining elements
useInline = false;
_linkToEnd(this,v);
} else {
// Inline the common case of _linkToEnd for new unique values
this.next.set(v,null);
this.prev.set(v,prev);
if(prev !== null) {
this.next.set(prev,v);
} else {
this.next.set(null,v);
}
this.prev.set(null,v);
this.length++;
prev = v;
}
} else {
_linkToEnd(this,value[t]);
}
}
} else {
for(t=0; t<value.length; t++) {
_assertString(value[t]);
}
for(t=0; t<value.length; t++) {
_removeOne(this,value[t]);
}
for(t=0; t<value.length; t++) {
_linkToEnd(this,value[t]);
}
}
} else {
_assertString(value);
+33 -6
View File
@@ -1400,15 +1400,15 @@ exports.search = function(text,options) {
fields.push("text");
}
// Function to check a given tiddler for the search term
var searchTiddler = function(title) {
var searchTiddler = function(tiddler,title) {
if(!searchTermsRegExps) {
return true;
}
var notYetFound = searchTermsRegExps.slice();
var tiddler = self.getTiddler(title);
if(!tiddler) {
tiddler = new $tw.Tiddler({title: title, text: "", type: "text/vnd.tiddlywiki"});
tiddler = self.getTiddler(title);
if(!tiddler) {
tiddler = new $tw.Tiddler({title: title, text: "", type: "text/vnd.tiddlywiki"});
}
}
var contentTypeInfo = $tw.config.contentTypeInfo[tiddler.fields.type] || $tw.config.contentTypeInfo["text/vnd.tiddlywiki"],
searchFields;
@@ -1424,6 +1424,33 @@ exports.search = function(text,options) {
} else {
searchFields = fields;
}
// Fast path for single search term (avoids array slice/splice per tiddler)
if(searchTermsRegExps.length === 1) {
var singleRegExp = searchTermsRegExps[0];
for(var fieldIndex=0; fieldIndex<searchFields.length; fieldIndex++) {
var fieldName = searchFields[fieldIndex];
if(fieldName === "text" && contentTypeInfo.encoding !== "utf8") {
continue;
}
var str = tiddler.fields[fieldName];
if(str) {
if($tw.utils.isArray(str)) {
for(var s=0; s<str.length; s++) {
if(singleRegExp.test(str[s])) {
return true;
}
}
} else {
str = tiddler.getFieldString(fieldName);
if(singleRegExp.test(str)) {
return true;
}
}
}
}
return false;
}
var notYetFound = searchTermsRegExps.slice();
for(var fieldIndex=0; notYetFound.length>0 && fieldIndex<searchFields.length; fieldIndex++) {
// Don't search the text field if the content type is binary
var fieldName = searchFields[fieldIndex];
@@ -1463,7 +1490,7 @@ exports.search = function(text,options) {
var results = [],
source = options.source || this.each;
source(function(tiddler,title) {
if(searchTiddler(title) !== invert) {
if(searchTiddler(tiddler,title) !== invert) {
results.push(title);
}
});