mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-09-04 00:58:53 +00:00
Initial Commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"description": "Performance measurement edition",
|
||||
"plugins": [
|
||||
"tiddlywiki/performance"
|
||||
],
|
||||
"themes": [
|
||||
"tiddlywiki/vanilla",
|
||||
"tiddlywiki/snowwhite"
|
||||
]
|
||||
}
|
||||
@@ -13,7 +13,8 @@
|
||||
"tiddlywiki/menubar",
|
||||
"tiddlywiki/jszip",
|
||||
"tiddlywiki/confetti",
|
||||
"tiddlywiki/tour"
|
||||
"tiddlywiki/tour",
|
||||
"tiddlywiki/performance"
|
||||
],
|
||||
"themes": [
|
||||
"tiddlywiki/vanilla",
|
||||
|
||||
@@ -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_0_EXPLORATORY"
|
||||
}
|
||||
@@ -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