mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-06-17 02:38:51 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31f242a42b | |||
| ea84baa5a3 | |||
| 27c60ff58d | |||
| 1d8131704c | |||
| a1a191b504 | |||
| 34d013ca3d | |||
| 1e06098d20 | |||
| b378f3f462 | |||
| b20b578183 | |||
| 4b046884b1 | |||
| f272f718fa |
+1
-1
@@ -5,7 +5,7 @@
|
||||
# Default to the current version number for building the plugin library
|
||||
|
||||
if [ -z "$TW5_BUILD_VERSION" ]; then
|
||||
TW5_BUILD_VERSION=v5.4.0
|
||||
TW5_BUILD_VERSION=v5.5.0
|
||||
fi
|
||||
|
||||
echo "Using TW5_BUILD_VERSION as [$TW5_BUILD_VERSION]"
|
||||
|
||||
@@ -14,6 +14,8 @@ var _boot = (function($tw) {
|
||||
|
||||
"use strict";
|
||||
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-start"); }
|
||||
|
||||
// Include bootprefix if we're not given module data
|
||||
if(!$tw) {
|
||||
$tw = require("./bootprefix.js").bootprefix();
|
||||
@@ -1158,6 +1160,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) {
|
||||
@@ -1206,6 +1232,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,
|
||||
@@ -1246,6 +1280,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),
|
||||
@@ -1284,6 +1326,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(),
|
||||
@@ -2539,7 +2591,9 @@ $tw.boot.loadStartup = function(options){
|
||||
|
||||
// Load tiddlers
|
||||
if($tw.boot.tasks.readBrowserTiddlers) {
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-store-read-start"); }
|
||||
$tw.loadTiddlersBrowser();
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-store-read-end"); }
|
||||
} else {
|
||||
$tw.loadTiddlersNode();
|
||||
}
|
||||
@@ -2551,6 +2605,7 @@ $tw.boot.loadStartup = function(options){
|
||||
$tw.hooks.invokeHook("th-boot-tiddlers-loaded");
|
||||
};
|
||||
$tw.boot.execStartup = function(options){
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-exec-start"); }
|
||||
// Unpack plugin tiddlers
|
||||
$tw.wiki.readPluginInfo();
|
||||
$tw.wiki.registerPluginTiddlers("plugin",$tw.safeMode ? ["$:/core"] : undefined);
|
||||
@@ -2578,6 +2633,7 @@ $tw.boot.execStartup = function(options){
|
||||
$tw.boot.executedStartupModules = Object.create(null);
|
||||
$tw.boot.disabledStartupModules = $tw.boot.disabledStartupModules || [];
|
||||
// Repeatedly execute the next eligible task
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-startup-modules-start"); }
|
||||
$tw.boot.executeNextStartupTask(options.callback);
|
||||
};
|
||||
/*
|
||||
@@ -2645,6 +2701,7 @@ $tw.boot.executeNextStartupTask = function(callback) {
|
||||
}
|
||||
taskIndex++;
|
||||
}
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-complete"); }
|
||||
if(typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
|
||||
+4
-1
@@ -14,6 +14,8 @@ See Boot.js for further details of the boot process.
|
||||
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-bootprefix-start"); }
|
||||
|
||||
var _bootprefix = (function($tw) {
|
||||
|
||||
"use strict";
|
||||
@@ -23,7 +25,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)) {
|
||||
@@ -121,6 +123,7 @@ return $tw;
|
||||
if(typeof(exports) === "undefined") {
|
||||
// Set up $tw global for the browser
|
||||
window.$tw = _bootprefix(window.$tw);
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-bootprefix-end"); }
|
||||
} else {
|
||||
// Export functionality as a module
|
||||
exports.bootprefix = _bootprefix;
|
||||
|
||||
+34
-17
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -67,7 +67,57 @@ TagSubIndexer.prototype.rebuild = function() {
|
||||
};
|
||||
|
||||
TagSubIndexer.prototype.update = function(updateDescriptor) {
|
||||
this.index = null;
|
||||
// If the index hasn't been built yet, no update needed
|
||||
if(this.index === null) {
|
||||
return;
|
||||
}
|
||||
// Determine whether the old/new tiddler is visible to this iterator
|
||||
var oldVisible = this._isVisible(updateDescriptor.old),
|
||||
newVisible = this._isVisible(updateDescriptor["new"]),
|
||||
self = this;
|
||||
// Remove old tags from index
|
||||
if(oldVisible && updateDescriptor.old.tiddler) {
|
||||
var oldTitle = updateDescriptor.old.tiddler.fields.title,
|
||||
oldTags = updateDescriptor.old.tiddler.fields.tags || [];
|
||||
$tw.utils.each(oldTags,function(tag) {
|
||||
if(self.index[tag]) {
|
||||
var idx = self.index[tag].titles.indexOf(oldTitle);
|
||||
if(idx !== -1) {
|
||||
self.index[tag].titles.splice(idx,1);
|
||||
if(self.index[tag].titles.length === 0) {
|
||||
delete self.index[tag];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Add new tags to index
|
||||
if(newVisible && updateDescriptor["new"].tiddler) {
|
||||
var newTitle = updateDescriptor["new"].tiddler.fields.title,
|
||||
newTags = updateDescriptor["new"].tiddler.fields.tags || [];
|
||||
$tw.utils.each(newTags,function(tag) {
|
||||
if(!self.index[tag]) {
|
||||
self.index[tag] = {isSorted: false, titles: [newTitle]};
|
||||
} else if(self.index[tag].titles.indexOf(newTitle) === -1) {
|
||||
self.index[tag].titles.push(newTitle);
|
||||
self.index[tag].isSorted = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Determine whether a tiddler described by a descriptor is visible to this sub-indexer's iterator
|
||||
*/
|
||||
TagSubIndexer.prototype._isVisible = function(descriptor) {
|
||||
if(this.iteratorMethod === "each") {
|
||||
return descriptor.exists;
|
||||
} else if(this.iteratorMethod === "eachShadow") {
|
||||
return descriptor.shadow;
|
||||
} else {
|
||||
// eachTiddlerPlusShadows and eachShadowPlusTiddlers both visit all tiddlers and shadows
|
||||
return descriptor.exists || descriptor.shadow;
|
||||
}
|
||||
};
|
||||
|
||||
TagSubIndexer.prototype.lookup = function(tag) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
title: $:/config/OfficialPluginLibrary
|
||||
tags: $:/tags/PluginLibrary
|
||||
url: https://tiddlywiki.com/library/v5.4.0/index.html
|
||||
url: https://tiddlywiki.com/library/v5.5.0/index.html
|
||||
caption: {{$:/language/OfficialPluginLibrary}}
|
||||
|
||||
{{$:/language/OfficialPluginLibrary/Hint}}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"description": "Performance measurement edition",
|
||||
"plugins": [
|
||||
"tiddlywiki/performance"
|
||||
],
|
||||
"themes": [
|
||||
"tiddlywiki/vanilla",
|
||||
"tiddlywiki/snowwhite"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
title: $:/config/LocalPluginLibrary
|
||||
tags: $:/tags/PluginLibrary
|
||||
url: http://127.0.0.1:8080/prerelease/library/v5.4.0/index.html
|
||||
url: http://127.0.0.1:8080/prerelease/library/v5.5.0/index.html
|
||||
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease Local)
|
||||
|
||||
A locally installed version of the official ~TiddlyWiki plugin library at tiddlywiki.com for testing and debugging. //Requires a local web server to share the library//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
title: $:/config/OfficialPluginLibrary
|
||||
tags: $:/tags/PluginLibrary
|
||||
url: https://tiddlywiki.com/prerelease/library/v5.4.0/index.html
|
||||
url: https://tiddlywiki.com/prerelease/library/v5.5.0/index.html
|
||||
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease)
|
||||
|
||||
The prerelease version of the official ~TiddlyWiki plugin library at tiddlywiki.com. Plugins, themes and language packs are maintained by the core team.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"tiddlywiki/jszip",
|
||||
"tiddlywiki/confetti",
|
||||
"tiddlywiki/tour",
|
||||
"tiddlywiki/performance",
|
||||
"tiddlywiki/dom-to-image"
|
||||
],
|
||||
"themes": [
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,13 @@
|
||||
caption: 5.5.0
|
||||
created: 20260420195917090
|
||||
modified: 20260420195917090
|
||||
tags: ReleaseNotes
|
||||
title: Release 5.5.0
|
||||
type: text/vnd.tiddlywiki
|
||||
description: Under development
|
||||
|
||||
\procedure release-introduction()
|
||||
Release v5.5.0 is under development.
|
||||
\end release-introduction
|
||||
|
||||
<<releasenote 5.5.0>>
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tiddlywiki",
|
||||
"preferGlobal": true,
|
||||
"version": "5.4.0",
|
||||
"version": "5.5.0-prerelease",
|
||||
"author": "Jeremy Ruston <jeremy@jermolene.com>",
|
||||
"description": "a non-linear personal web notebook",
|
||||
"contributors": [
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/performance/perf-replay-command.js
|
||||
type: application/javascript
|
||||
module-type: command
|
||||
|
||||
Command to replay a recorded performance timeline against the current wiki
|
||||
|
||||
Usage: --perf-replay <timeline.json> [--no-coalesce]
|
||||
|
||||
Loads the wiki (use --load before this command), builds a widget tree
|
||||
using fakeDocument, then replays the recorded store modifications
|
||||
batch by batch, measuring refresh performance for each batch.
|
||||
|
||||
\*/
|
||||
|
||||
"use strict";
|
||||
|
||||
exports.info = {
|
||||
name: "perf-replay",
|
||||
synchronous: false
|
||||
};
|
||||
|
||||
var Command = function(params,commander,callback) {
|
||||
this.params = params;
|
||||
this.commander = commander;
|
||||
this.callback = callback;
|
||||
};
|
||||
|
||||
Command.prototype.execute = function() {
|
||||
var self = this,
|
||||
fs = require("fs"),
|
||||
path = require("path"),
|
||||
wiki = this.commander.wiki,
|
||||
widget = require("$:/core/modules/widgets/widget.js");
|
||||
// Parse parameters
|
||||
if(this.params.length < 1) {
|
||||
return "Missing timeline filename. Usage: --perf-replay <timeline.json>";
|
||||
}
|
||||
var timelinePath = this.params[0];
|
||||
// Load timeline
|
||||
var timelineData;
|
||||
try {
|
||||
timelineData = JSON.parse(fs.readFileSync(timelinePath,"utf8"));
|
||||
} catch(e) {
|
||||
return "Error reading timeline file: " + e.message;
|
||||
}
|
||||
if(!Array.isArray(timelineData) || timelineData.length === 0) {
|
||||
return "Timeline file is empty or invalid";
|
||||
}
|
||||
// Count tiddlers in wiki
|
||||
var tiddlerCount = 0;
|
||||
wiki.each(function() { tiddlerCount++; });
|
||||
// Build a widget tree against fakeDocument (mirroring what render.js does in the browser)
|
||||
var PAGE_TEMPLATE_TITLE = "$:/core/ui/RootTemplate";
|
||||
// Create root widget
|
||||
var rootWidget = new widget.widget({
|
||||
type: "widget",
|
||||
children: []
|
||||
},{
|
||||
wiki: wiki,
|
||||
document: $tw.fakeDocument
|
||||
});
|
||||
// Enable performance instrumentation
|
||||
var perf = new $tw.Performance(true);
|
||||
// Wrap filter execution with perf measurement
|
||||
var origCompileFilter = wiki.compileFilter;
|
||||
var filterInvocations = 0;
|
||||
wiki.compileFilter = function(filterString) {
|
||||
var compiledFilter = origCompileFilter.call(wiki,filterString);
|
||||
return perf.measure("filter: " + filterString.substring(0,80),function(source,widget) {
|
||||
filterInvocations++;
|
||||
return compiledFilter.call(this,source,widget);
|
||||
});
|
||||
};
|
||||
// Re-initialise parsers so filters get wrapped
|
||||
wiki.clearCache(null);
|
||||
// Build and render the page widget tree
|
||||
var pageWidgetNode = wiki.makeTranscludeWidget(PAGE_TEMPLATE_TITLE,{
|
||||
document: $tw.fakeDocument,
|
||||
parentWidget: rootWidget,
|
||||
recursionMarker: "no"
|
||||
});
|
||||
var pageContainer = $tw.fakeDocument.createElement("div");
|
||||
var renderStart = $tw.utils.timer();
|
||||
pageWidgetNode.render(pageContainer,null);
|
||||
var renderTime = $tw.utils.timer(renderStart);
|
||||
// Link root widget
|
||||
rootWidget.domNodes = [pageContainer];
|
||||
rootWidget.children = [pageWidgetNode];
|
||||
// Group timeline events by batch
|
||||
var batches = [];
|
||||
var currentBatch = null;
|
||||
$tw.utils.each(timelineData,function(event) {
|
||||
if(!currentBatch || event.batch !== currentBatch.batchId) {
|
||||
currentBatch = {batchId: event.batch, events: []};
|
||||
batches.push(currentBatch);
|
||||
}
|
||||
currentBatch.events.push(event);
|
||||
});
|
||||
// Replay each batch and measure refresh
|
||||
var results = [],
|
||||
totalRefreshTime = 0,
|
||||
totalFilterInvocations = 0;
|
||||
self.commander.streams.output.write("\nPerformance Timeline Replay\n");
|
||||
self.commander.streams.output.write("==========================\n");
|
||||
self.commander.streams.output.write("Wiki: " + tiddlerCount + " tiddlers\n");
|
||||
self.commander.streams.output.write("Timeline: " + timelineData.length + " operations in " + batches.length + " batches\n");
|
||||
self.commander.streams.output.write("Initial render: " + renderTime.toFixed(2) + "ms\n\n");
|
||||
self.commander.streams.output.write(padRight("Batch",8) + padRight("Ops",6) + padRight("Changed",10) +
|
||||
padRight("Refresh(ms)",14) + padRight("Filters",10) + "Tiddlers Changed\n");
|
||||
self.commander.streams.output.write(padRight("-----",8) + padRight("---",6) + padRight("-------",10) +
|
||||
padRight("-----------",14) + padRight("-------",10) + "----------------\n");
|
||||
$tw.utils.each(batches,function(batch,index) {
|
||||
// Apply all operations in this batch directly (bypassing the intercepted addTiddler
|
||||
// to avoid re-recording)
|
||||
var changedTiddlers = Object.create(null);
|
||||
$tw.utils.each(batch.events,function(event) {
|
||||
if(event.op === "add") {
|
||||
wiki.addTiddler(new $tw.Tiddler(event.fields));
|
||||
changedTiddlers[event.title] = {modified: true};
|
||||
} else if(event.op === "delete") {
|
||||
wiki.deleteTiddler(event.title);
|
||||
changedTiddlers[event.title] = {deleted: true};
|
||||
}
|
||||
});
|
||||
// Measure refresh
|
||||
filterInvocations = 0;
|
||||
var refreshStart = $tw.utils.timer();
|
||||
pageWidgetNode.refresh(changedTiddlers);
|
||||
var refreshTime = $tw.utils.timer(refreshStart);
|
||||
totalRefreshTime += refreshTime;
|
||||
totalFilterInvocations += filterInvocations;
|
||||
var changedTitles = Object.keys(changedTiddlers);
|
||||
var titlesDisplay = changedTitles.slice(0,3).join(", ");
|
||||
if(changedTitles.length > 3) {
|
||||
titlesDisplay += " (+" + (changedTitles.length - 3) + " more)";
|
||||
}
|
||||
results.push({
|
||||
batch: index + 1,
|
||||
ops: batch.events.length,
|
||||
changed: changedTitles.length,
|
||||
refreshMs: refreshTime,
|
||||
filters: filterInvocations,
|
||||
tiddlers: changedTitles
|
||||
});
|
||||
self.commander.streams.output.write(
|
||||
padRight(String(index + 1),8) +
|
||||
padRight(String(batch.events.length),6) +
|
||||
padRight(String(changedTitles.length),10) +
|
||||
padRight(refreshTime.toFixed(2),14) +
|
||||
padRight(String(filterInvocations),10) +
|
||||
titlesDisplay + "\n"
|
||||
);
|
||||
});
|
||||
// Summary statistics
|
||||
var refreshTimes = results.map(function(r) { return r.refreshMs; }).sort(function(a,b) { return a - b; });
|
||||
var p50 = percentile(refreshTimes,50);
|
||||
var p95 = percentile(refreshTimes,95);
|
||||
var p99 = percentile(refreshTimes,99);
|
||||
var maxRefresh = refreshTimes[refreshTimes.length - 1] || 0;
|
||||
var meanRefresh = batches.length > 0 ? totalRefreshTime / batches.length : 0;
|
||||
self.commander.streams.output.write("\nSummary\n");
|
||||
self.commander.streams.output.write("-------\n");
|
||||
self.commander.streams.output.write("Initial render: " + renderTime.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("Total refresh time: " + totalRefreshTime.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("Mean refresh: " + meanRefresh.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("P50 refresh: " + p50.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("P95 refresh: " + p95.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("P99 refresh: " + p99.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("Max refresh: " + maxRefresh.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("Total filters run: " + totalFilterInvocations + "\n");
|
||||
// Output filter breakdown
|
||||
self.commander.streams.output.write("\nTop Filter Execution Times\n");
|
||||
self.commander.streams.output.write("--------------------------\n");
|
||||
var measures = perf.measures;
|
||||
var orderedMeasures = Object.keys(measures).sort(function(a,b) {
|
||||
return measures[b].time - measures[a].time;
|
||||
}).slice(0,20);
|
||||
$tw.utils.each(orderedMeasures,function(name) {
|
||||
var m = measures[name];
|
||||
self.commander.streams.output.write(
|
||||
padRight(m.time.toFixed(2) + "ms",14) +
|
||||
padRight(String(m.invocations) + "x",10) +
|
||||
padRight((m.time / m.invocations).toFixed(3) + "ms avg",16) +
|
||||
name + "\n"
|
||||
);
|
||||
});
|
||||
// Write JSON results
|
||||
var jsonResultPath = timelinePath.replace(/\.json$/,"") + "-results.json";
|
||||
try {
|
||||
fs.writeFileSync(jsonResultPath,JSON.stringify({
|
||||
wiki: {tiddlerCount: tiddlerCount},
|
||||
timeline: {operations: timelineData.length, batches: batches.length},
|
||||
initialRender: renderTime,
|
||||
summary: {
|
||||
totalRefreshTime: totalRefreshTime,
|
||||
meanRefresh: meanRefresh,
|
||||
p50: p50,
|
||||
p95: p95,
|
||||
p99: p99,
|
||||
maxRefresh: maxRefresh,
|
||||
totalFilterInvocations: totalFilterInvocations
|
||||
},
|
||||
batches: results,
|
||||
topFilters: orderedMeasures.map(function(name) {
|
||||
return {name: name, time: measures[name].time, invocations: measures[name].invocations};
|
||||
})
|
||||
},null,"\t"),"utf8");
|
||||
self.commander.streams.output.write("\nDetailed results written to: " + jsonResultPath + "\n");
|
||||
} catch(e) {
|
||||
self.commander.streams.output.write("\nWarning: Could not write results file: " + e.message + "\n");
|
||||
}
|
||||
self.callback(null);
|
||||
return null;
|
||||
};
|
||||
|
||||
function percentile(sortedArray,p) {
|
||||
if(sortedArray.length === 0) return 0;
|
||||
var index = Math.ceil(sortedArray.length * p / 100) - 1;
|
||||
return sortedArray[Math.max(0,index)];
|
||||
}
|
||||
|
||||
function padRight(str,width) {
|
||||
while(str.length < width) {
|
||||
str += " ";
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
exports.Command = Command;
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "$:/plugins/tiddlywiki/performance",
|
||||
"name": "Performance",
|
||||
"description": "Record and replay wiki store modifications for performance testing",
|
||||
"list": "readme ui",
|
||||
"stability": "STABILITY_1_EXPERIMENTAL"
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
title: $:/plugins/tiddlywiki/performance/readme
|
||||
|
||||
! Performance Testing Plugin
|
||||
|
||||
This plugin provides a framework for measuring the performance of TiddlyWiki's refresh cycle — the process that updates the display when tiddlers are modified.
|
||||
|
||||
The idea is to capture a realistic workload by recording store modifications while a user interacts with a wiki in the browser, and then replaying those modifications under Node.js where the refresh cycle can be precisely measured in isolation.
|
||||
|
||||
!! Motivation
|
||||
|
||||
An important motivation for this framework is to enable LLMs to iteratively optimise TiddlyWiki's performance. The workflow is:
|
||||
|
||||
# An LLM makes a change to the TiddlyWiki codebase (e.g. optimising a filter operator, caching a computation, or restructuring a widget's refresh logic)
|
||||
# The LLM runs `--perf-replay` against a recorded timeline to measure the impact
|
||||
# The LLM reads the JSON results file to determine whether the change improved, regressed, or had no effect on performance
|
||||
# The LLM iterates: tries another approach, measures again, and converges on the best solution
|
||||
|
||||
This tight edit-measure-iterate loop works because `--perf-replay` runs entirely under Node.js with no browser required, produces machine-readable JSON output, and completes in seconds.
|
||||
|
||||
!! How It Works
|
||||
|
||||
The framework has two parts:
|
||||
|
||||
!!! 1. Recording (Browser)
|
||||
|
||||
The plugin intercepts `wiki.addTiddler()` and `wiki.deleteTiddler()` to capture every store modification as it happens. Each operation is recorded with:
|
||||
|
||||
* A sequence number and high-resolution timestamp
|
||||
* The full tiddler fields (so the exact state can be recreated)
|
||||
* A batch identifier that tracks TiddlyWiki's change batching via `$tw.utils.nextTick()`
|
||||
|
||||
The batch tracking is important because TiddlyWiki groups multiple store changes that occur in the same tick into a single refresh cycle. The recorder preserves these batch boundaries so that playback triggers the same pattern of refreshes.
|
||||
|
||||
!!! 2. Playback (Node.js)
|
||||
|
||||
The `--perf-replay` command loads a wiki and builds the full widget tree using TiddlyWiki's `$tw.fakeDocument` — the lightweight DOM implementation used for server-side rendering. It then replays the recorded timeline batch by batch, calling `widgetNode.refresh(changedTiddlers)` after each batch and measuring how long it takes.
|
||||
|
||||
This means we are measuring TiddlyWiki's own refresh logic (widget tree traversal, filter evaluation, DOM diffing) in isolation from browser layout and paint. This is intentional — it lets us identify performance bottlenecks within TiddlyWiki itself, independent of which browser is being used.
|
||||
|
||||
!! Why Store-Level Recording?
|
||||
|
||||
An alternative would be to record DOM events (clicks, keystrokes) and replay them in a headless browser. Store-level recording was chosen instead because:
|
||||
|
||||
* The refresh cycle responds to ''store changes'', not DOM events — store modifications are the natural input
|
||||
* Store changes are fully deterministic and reproducible
|
||||
* No DOM dependency means playback works in pure Node.js with no headless browser to install
|
||||
* A headless browser would add its own overhead, making measurements less precise
|
||||
|
||||
!! Recording
|
||||
|
||||
# Include this plugin in your wiki
|
||||
# Open the Control Panel and find the "Performance Testing Recorder" tab
|
||||
# Click "Start Recording"
|
||||
# Interact with the wiki — open tiddlers, edit, type, navigate, switch tabs
|
||||
# Click "Stop Recording"
|
||||
# Download the `timeline.json` file
|
||||
|
||||
!!! Draft Coalescing
|
||||
|
||||
When editing a tiddler, TiddlyWiki writes to draft tiddlers on every keystroke. By default, the recorder coalesces rapid draft updates within the same batch, keeping only the last update. This produces a more compact timeline that focuses on the refresh-relevant changes.
|
||||
|
||||
Uncheck "Coalesce rapid draft updates" to record every individual keystroke. This is useful when you specifically want to measure the performance impact of rapid typing.
|
||||
|
||||
!! Playback
|
||||
|
||||
```
|
||||
tiddlywiki editions/performance --load mywiki.html --perf-replay timeline.json
|
||||
```
|
||||
|
||||
Or from any edition that includes this plugin:
|
||||
|
||||
```
|
||||
tiddlywiki myedition --perf-replay timeline.json
|
||||
```
|
||||
|
||||
Playback runs at full speed with no delays between batches. The recorded timestamps are preserved in the timeline for reference but are not used for pacing.
|
||||
|
||||
!! What Gets Measured
|
||||
|
||||
* ''Initial render time'' — the time to build and render the full widget tree from scratch
|
||||
* ''Refresh time per batch'' — the time `widgetNode.refresh(changedTiddlers)` takes for each batch of store modifications
|
||||
* ''Filter execution'' — individual filter timings and invocation counts, showing which filters are the most expensive
|
||||
* ''Statistical summary'' — mean, P50, P95, P99, and maximum refresh times across all batches
|
||||
|
||||
!! Output
|
||||
|
||||
The command produces two forms of output:
|
||||
|
||||
!!! Text Report (stdout)
|
||||
|
||||
A human-readable table printed to the console showing per-batch timings, a summary with percentile statistics, and a breakdown of the most expensive filter executions.
|
||||
|
||||
!!! JSON Results File
|
||||
|
||||
A `<timeline-name>-results.json` file is written alongside the input timeline. This is the primary output for automated consumption. The file contains:
|
||||
|
||||
```json
|
||||
{
|
||||
"wiki": {
|
||||
"tiddlerCount": 2076
|
||||
},
|
||||
"timeline": {
|
||||
"operations": 156,
|
||||
"batches": 42
|
||||
},
|
||||
"initialRender": 55.46,
|
||||
"summary": {
|
||||
"totalRefreshTime": 234.5,
|
||||
"meanRefresh": 5.58,
|
||||
"p50": 4.12,
|
||||
"p95": 18.7,
|
||||
"p99": 31.2,
|
||||
"maxRefresh": 31.2,
|
||||
"totalFilterInvocations": 4821
|
||||
},
|
||||
"batches": [
|
||||
{
|
||||
"batch": 1,
|
||||
"ops": 1,
|
||||
"changed": 1,
|
||||
"refreshMs": 12.3,
|
||||
"filters": 293,
|
||||
"tiddlers": ["$:/StoryList"]
|
||||
}
|
||||
],
|
||||
"topFilters": [
|
||||
{
|
||||
"name": "filter: [subfilter{$:/core/config/GlobalImportFilter}]",
|
||||
"time": 5.65,
|
||||
"invocations": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All times are in milliseconds. The key fields for automated analysis:
|
||||
|
||||
* `summary.totalRefreshTime` — the single most important number: total time spent in refresh across all batches
|
||||
* `summary.meanRefresh` — average refresh time per batch
|
||||
* `summary.p95` / `summary.p99` — tail latency indicators
|
||||
* `initialRender` — time to build the widget tree from scratch (measures startup cost)
|
||||
* `batches[].refreshMs` — per-batch breakdown, useful for identifying which user actions are expensive
|
||||
* `topFilters[]` — the most expensive filters by total execution time, useful for identifying optimisation targets
|
||||
|
||||
!! Example: LLM Optimisation Workflow
|
||||
|
||||
An LLM optimising TiddlyWiki performance would follow this pattern:
|
||||
|
||||
!!! Step 1: Establish baseline
|
||||
|
||||
```
|
||||
node ./tiddlywiki.js editions/performance --load mywiki.html --perf-replay timeline.json
|
||||
```
|
||||
|
||||
Read `timeline-results.json` and note the baseline `summary.totalRefreshTime`.
|
||||
|
||||
!!! Step 2: Make a change
|
||||
|
||||
Edit a source file (e.g. optimise a filter operator in `core/modules/filters/`).
|
||||
|
||||
!!! Step 3: Measure impact
|
||||
|
||||
Run the same `--perf-replay` command again and read the new `timeline-results.json`.
|
||||
|
||||
!!! Step 4: Compare
|
||||
|
||||
Compare `summary.totalRefreshTime` and `summary.p95` between baseline and new results. If improved, keep the change. If regressed, revert and try a different approach.
|
||||
|
||||
!!! Step 5: Iterate
|
||||
|
||||
Repeat steps 2-4 until the target metric is optimised.
|
||||
|
||||
The JSON results file makes step 4 straightforward — an LLM can read two JSON files and compare numeric fields directly without parsing tabular text output.
|
||||
|
||||
!! Timeline Format
|
||||
|
||||
The timeline is a JSON array of operations:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"seq": 0,
|
||||
"t": 123.45,
|
||||
"batch": 0,
|
||||
"op": "add",
|
||||
"title": "$:/StoryList",
|
||||
"isDraft": false,
|
||||
"fields": {
|
||||
"title": "$:/StoryList",
|
||||
"list": "GettingStarted",
|
||||
"text": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
* `seq` — sequential operation number
|
||||
* `t` — milliseconds since recording started
|
||||
* `batch` — batch identifier (operations in the same batch trigger a single refresh)
|
||||
* `op` — `"add"` or `"delete"`
|
||||
* `isDraft` — whether this is a draft tiddler (used for coalescing)
|
||||
* `fields` — complete tiddler fields (null for delete operations)
|
||||
@@ -0,0 +1,143 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/performance/recorder.js
|
||||
type: application/javascript
|
||||
module-type: startup
|
||||
|
||||
Store modification recorder for performance testing.
|
||||
Intercepts wiki.addTiddler() and wiki.deleteTiddler() to capture
|
||||
a timeline of all store modifications with batch boundary tracking.
|
||||
|
||||
\*/
|
||||
|
||||
"use strict";
|
||||
|
||||
exports.name = "perf-recorder";
|
||||
exports.platforms = ["browser"];
|
||||
exports.after = ["load-modules"];
|
||||
exports.synchronous = true;
|
||||
|
||||
exports.startup = function() {
|
||||
var STATE_TIDDLER = "$:/state/performance/recording",
|
||||
TIMELINE_TIDDLER = "$:/temp/performance/timeline",
|
||||
COALESCE_CONFIG = "$:/config/performance/coalesce-drafts",
|
||||
timeline = [],
|
||||
seq = 0,
|
||||
startTime = null,
|
||||
recording = false,
|
||||
batchId = 0,
|
||||
currentBatch = 0,
|
||||
origNextTick = $tw.utils.nextTick,
|
||||
origAddTiddler = $tw.wiki.addTiddler.bind($tw.wiki),
|
||||
origDeleteTiddler = $tw.wiki.deleteTiddler.bind($tw.wiki);
|
||||
|
||||
// Patch nextTick to track batch boundaries
|
||||
$tw.utils.nextTick = function(fn) {
|
||||
origNextTick(function() {
|
||||
if(recording) {
|
||||
currentBatch = ++batchId;
|
||||
}
|
||||
fn();
|
||||
});
|
||||
};
|
||||
|
||||
// Patch addTiddler
|
||||
$tw.wiki.addTiddler = function(tiddler) {
|
||||
if(recording) {
|
||||
if(!(tiddler instanceof $tw.Tiddler)) {
|
||||
tiddler = new $tw.Tiddler(tiddler);
|
||||
}
|
||||
var title = tiddler.fields.title;
|
||||
// Skip our own state/timeline tiddlers
|
||||
if(title !== STATE_TIDDLER && title !== TIMELINE_TIDDLER) {
|
||||
timeline.push({
|
||||
seq: seq++,
|
||||
t: $tw.utils.timer(startTime),
|
||||
batch: currentBatch,
|
||||
op: "add",
|
||||
title: title,
|
||||
isDraft: tiddler.hasField("draft.of"),
|
||||
fields: tiddler.getFieldStrings()
|
||||
});
|
||||
}
|
||||
}
|
||||
return origAddTiddler.apply(null,arguments);
|
||||
};
|
||||
|
||||
// Patch deleteTiddler
|
||||
$tw.wiki.deleteTiddler = function(title) {
|
||||
if(recording) {
|
||||
if(title !== STATE_TIDDLER && title !== TIMELINE_TIDDLER) {
|
||||
timeline.push({
|
||||
seq: seq++,
|
||||
t: $tw.utils.timer(startTime),
|
||||
batch: currentBatch,
|
||||
op: "delete",
|
||||
title: title,
|
||||
isDraft: false,
|
||||
fields: null
|
||||
});
|
||||
}
|
||||
}
|
||||
return origDeleteTiddler.apply(null,arguments);
|
||||
};
|
||||
|
||||
// Listen for recording state changes
|
||||
$tw.wiki.addEventListener("change",function(changes) {
|
||||
if(STATE_TIDDLER in changes) {
|
||||
var state = $tw.wiki.getTiddlerText(STATE_TIDDLER,"").trim();
|
||||
if(state === "yes" && !recording) {
|
||||
// Start recording
|
||||
timeline = [];
|
||||
seq = 0;
|
||||
batchId = 0;
|
||||
currentBatch = 0;
|
||||
startTime = $tw.utils.timer();
|
||||
recording = true;
|
||||
console.log("performance: Recording started");
|
||||
} else if(state !== "yes" && recording) {
|
||||
// Stop recording and save timeline
|
||||
recording = false;
|
||||
var coalesce = $tw.wiki.getTiddlerText(COALESCE_CONFIG,"yes").trim() === "yes";
|
||||
var output = coalesce ? coalesceDrafts(timeline) : timeline;
|
||||
origAddTiddler(new $tw.Tiddler({
|
||||
title: TIMELINE_TIDDLER,
|
||||
type: "application/json",
|
||||
text: JSON.stringify(output,null,"\t")
|
||||
}));
|
||||
console.log("performance: Recording stopped. " + timeline.length + " operations captured" +
|
||||
(coalesce ? " (" + output.length + " after coalescing drafts)" : ""));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
Coalesce rapid draft tiddler updates within the same batch.
|
||||
Keeps only the last update for each draft tiddler per batch.
|
||||
*/
|
||||
function coalesceDrafts(events) {
|
||||
var result = [],
|
||||
i = 0;
|
||||
while(i < events.length) {
|
||||
var event = events[i];
|
||||
if(event.isDraft && event.op === "add") {
|
||||
// Look ahead for later updates to this same draft in the same batch
|
||||
var lastIndex = i;
|
||||
for(var j = i + 1; j < events.length; j++) {
|
||||
if(events[j].batch !== event.batch) {
|
||||
break;
|
||||
}
|
||||
if(events[j].title === event.title && events[j].op === "add") {
|
||||
lastIndex = j;
|
||||
}
|
||||
}
|
||||
// Keep only the last one, but fix its seq to maintain ordering
|
||||
result.push(events[lastIndex]);
|
||||
i = lastIndex + 1;
|
||||
} else {
|
||||
result.push(event);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
title: $:/plugins/tiddlywiki/performance/ui
|
||||
caption: Recorder
|
||||
|
||||
! Performance Testing Recorder
|
||||
|
||||
<$let
|
||||
state="$:/state/performance/recording"
|
||||
timeline="$:/temp/performance/timeline"
|
||||
coalesceConfig="$:/config/performance/coalesce-drafts"
|
||||
>
|
||||
|
||||
!! Recording
|
||||
|
||||
<$reveal state=<<state>> type="nomatch" text="yes">
|
||||
<$button set=<<state>> setTo="yes" class="tc-btn-big-green">Start Recording</$button>
|
||||
</$reveal>
|
||||
|
||||
<$reveal state=<<state>> type="match" text="yes">
|
||||
<$button set=<<state>> setTo="no" class="tc-btn-big-green" style="background: #d33;">Stop Recording</$button>
|
||||
//Recording in progress...//
|
||||
</$reveal>
|
||||
|
||||
!! Options
|
||||
|
||||
<$checkbox tiddler=<<coalesceConfig>> field="text" checked="yes" unchecked="no" default="yes"> Coalesce rapid draft updates</$checkbox>
|
||||
|
||||
!! Timeline
|
||||
|
||||
<$reveal type="nomatch" state=<<timeline>> text="">
|
||||
|
||||
<$let timelineText={{$(timeline)$}}>
|
||||
<$vars count={{{ [<timeline>get[text]jsonextract[]] +[count[]] }}}>
|
||||
Timeline contains <<count>> operations.
|
||||
</$vars>
|
||||
|
||||
<$button>
|
||||
<$action-sendmessage $message="tm-download-file" $param=<<timeline>> filename="timeline.json"/>
|
||||
Download timeline.json
|
||||
</$button>
|
||||
|
||||
</$let>
|
||||
</$reveal>
|
||||
|
||||
<$reveal type="match" state=<<timeline>> text="">
|
||||
No timeline recorded yet. Click "Start Recording", interact with the wiki, then click "Stop Recording".
|
||||
</$reveal>
|
||||
|
||||
</$let>
|
||||
Reference in New Issue
Block a user