An over-abundance of triggered updates

In $:/core/modules/startup/render.js, there’s an event listener for a “change” event, which seems to be triggered by any click on any link or widget or keypress in any field.

If I understand correctly, this is then forwarded to update all open and non-folded tiddlers, which means all macros, widgets, filters, transclusions etc? I don’t understand exactly how this works, and am not sure if this triggers saves as well.

I’m getting delays of 100-300 ms for every single click or keypress if I open a few large tiddlers (40 ms otherwise), and to me it seems unnecessary in most cases. Some examples:

  • When clicking “edit this tiddler”
  • When opening or closing dropdown menus
  • When editing a draft’s tags or fields
  • When navigating in the sidebar
  • When clicking tiddler links
  • When clicking the empty search box
  • For every single keypress in the search box

First I thought a simple “debounce” would suffice – as in “Defer the change if only drafts have changed”, but for all changes. This works for typing and searching, since you don’t expect or need to see the effects of the changes right away. But for clicking links, it’s a no-go, since you expect links to open immediately.

An improvement would be to handle tags and fields of drafts (and the search box) the same way the texts of drafts are handled.

But I have a hard time understanding why all tiddlers must be updated in all the cases listed above.

Thoughts?

Hi @cdaven good questions that get to the heart of how TW works internally.

The process you are describing is called the refresh cycle. The basic idea is that any change to the tiddler store triggers the refresh cycle which is then responsible for updating the DOM to match the new values in the store.

The triggering process is subtle. Rather than running the refresh cycle directly on each change to the store, it just queues up a deferred task (via setTimeout). That means that multiple changes to the store in a single thread only trigger a single refresh cycle.

At a high level, the refresh cycle walks the widget tree and detects widgets that need re-rendering. Widgets control how they refreshed when there is a change that affects them: they can either be completely destroyed and recreated, but it is also possible for them to be more selective about which changes should trigger a rebuild.

The reason that actions such as opening or closing dropdown menus trigger a full refresh cycle is a consequence of another key principle of TiddlyWiki’s architecture: every piece of state of the UI is held within the tiddler store. The way that dropdown menus work is that each menu is associated with a “state tiddler” that records the state of the menu: typically “hide” or “show”. The button that triggers the menu sets that state tiddler to “show”. Elsewhere in the widget tree, that triggers the display of the menu itself which only shown if the state tiddler is set to “show”. (It’s worth noting that a big part of the design of a TW-based application is figuring out how to generate unique state tiddler titles for each piece of UI – eg with the qualify macro).

So, the refresh cycle is the bottleneck of TiddlyWiki in terms of interactive performance. We have some tooling to help users measure performance, and there are a number of optimisations in place:

  • Modifications to draft tiddlers are debounced so that they don’t trigger the refresh cycle as often as ordinary tiddlers
  • The list widget can insert or delete tiddlers from the story river without disturbing the tiddlers that are already there

In turn, refresh performance is heavily impacted by the speed of filter evaluation. In general, filters used within the page will be re-evaluated on every refresh cycle. So we’ve always paid a lot of attention to performance optimisations for filter operations: for example, we index tags and fields, and use a custom high performance list structure for passing results between filter operators.

There’s always further to go to improve performance of the core primitives, but the biggest gains for individual wikis are often to be found by simplifying or customising the user interface. For example, if you have tens of thousands of tiddlers then the More → All sidebar tab is a performance hog, and not much practical use.

2 Likes

Don’t forget @Flibbles’ implementation of cached filter results in v5.3.2! :wink:

Have a nice day
Yaisog

Thank you for the explanation! I’m not quite convinced that we fully understand each other, however.

My question is more why tiddlers that are un-affected by “changes” are still re-rendered.

My TiddlyWiki has a footer with backlinks and “freelinks”, which is slow.

Every tiddler I open, increases the time for “mainRefresh”. This means that every new tiddler I open, opens more and more slowly. After 40 open tiddlers, it takes about 500 ms to open the next one.

And of course, every keypress in the search box takes 500 ms to process.

This sounds to me like every open tiddler is re-rendered unnecessarily. Why would the tiddlers and footers be re-rendered every time I “click a link”, when they are not affected by the open/closed status of other tiddlers, or the query in the search box?

So the decision to be rendered/refreshed is up to each and every widget, instead of the core knowing that certain changes are only supposed to affect e.g. system tiddlers? That sounds like the more flexible but less performant choice.

Maybe the question is; how do I make my code ignore all “changes” that are irrelevant? A DoS protection from the application itself, if you will.

This is only true for the title and text fields – not the tag or type fields, nor the custom field fields. Maybe this is a bug?


Again, why do we have to refresh widgets on every keypress? As long as I’m typing, I’m busy, and won’t mind if the tiddlers are updated after I’m done typing. Right? Maybe we could distinguish between “change” events that can always be debounced, and those that should trigger refreshes right away?

Currently we’re just caching the compiled filter; caching the filter results would be project for the future.

If a tiddler is unaffected by the changes then it should not be re-rendered. Where are you seeing that behaviour?

Freelinks are inherently super slow. I wouldn’t recommend them for a large wiki.

The refresh process still needs to iterate through those nodes to see if there are any that need updating.

The footers include backlinks and so they will need to be recomputed for every change to the store.

That’s correct. Only individual widgets know which attributes or other internal state should trigger a refresh. I can’t see how generic refresh processing wouldn’t be any faster.

I think the problem here is that those updates are not irrelevant. The store has changed and so things like backlinks need to recomputed.

I don’t think that’s correct. Here’s the code; it just tests for updates to tiddlers that are drafts:

We have to refresh the widget tree every time the store changes. We do indeed defer refreshes, but they have to happen eventually if things are going to appear consistent. I think we’re already doing what you are suggesting, but I might be misunderstanding.

With the default settings TW assumes that you finished typing after 400ms.

See: the Typing Refresh Delay setting

You can up this if you need.

Yes, it is correct.

I had another look now, and when editing a tiddler’s tags, type and custom fields, the changed tiddler isn’t the main tiddler, but instead a temporary tiddler that doesn’t have the draft.of field.

For tags, you’re editing a tiddler called something like $:/temp/NewTagName/input--981564737. That tiddler should have the draft.of field, right? Then it would work as I think you assume it already does.

Yes, but as I’m trying to explain, only in two places: the title and text of a tiddler. Not when typing in e.g. the search box.

Maybe some special tiddlers should/could have the throttle.refresh field set, to fix this?

  • $:/temp/search/input
  • $:/temp/advancedsearch and $:/temp/advancedsearch/input

As you say, the problem is that editing a tag involves modifying a temporary tiddler, and it is that temporary tiddler that is triggering the refresh. Just modifying the tags field of a draft without modifying other tiddlers will defer the refresh as expected.

Yes indeed, the optimisation we are discussing is specific to draft tiddlers. There may well be scope for more optimisations.

It’s worth trying, I don’t think anyone has experimented along those lines.

Did you read the “freelinks readme” tiddler. It contains information to speed up freelinks handling.

The second paragraph says:

Note that automatic link generation can be very slow when there are a large number of tiddlers.

Under Notes it says:

To change within which tiddlers freelinking occurs requires customising the shadow tiddler $:/plugins/tiddlywiki/freelinks/macros/view. This tiddler is tagged $:/tags/Macro/View which means that it will be included as a local macro in each view template. By default, its content is:

<$set name="tv-freelinks" value={{$:/config/Freelinks/Enable}}/>

That means that for each tiddler the variable tv-freelinks will be set to the tiddler $:/config/Freelinks/Enable, which is set to “yes” or “no” by the settings in control panel.

and

Or, we can make a filter that will only freelink within tiddlers with the tag “MyTags”:

<$set name="tv-freelinks" value={{{ [<currentTiddler>tag[MyTags]then[yes]else[no]] }}}/>

So if it is possible to narrow down the number of tiddlers, that need freelinking it can be achieved with a custom tag as mentioned above.

You can open the $:/plugins/tiddlywiki/freelinks/macros/view tiddler and use the following code. So it will only use tiddlers tagged: MyTags with freelinks. … This can dramatically reduce the work TW has to do.

title: $:/plugins/tiddlywiki/freelinks/macros/view
tags: $:/tags/Macro/View

<$set name="tv-freelinks" value={{{ [<currentTiddler>tag[MyTags]then[yes]else[no]] }}}>

<$set name="tv-freelinks-ignore-case" value={{$:/config/Freelinks/IgnoreCase}}/>

</$set>

There is a $:/config/Freelinks/Enable system tiddler, that you can switch on and of, if you don’t need it.

You wrote:

Right. … I could ask. Why don’t we switch off freelinking while editing?

eg: If there is any tiddler in draft mode we can switch freelinking off, otherwise it is on.
Important I did switch the logic to then[no]else[yes], which is different from the first example.

title: $:/plugins/tiddlywiki/freelinks/macros/view
tags: $:/tags/Macro/View

<$set name="tv-freelinks" value={{{ [all[tiddlers]is[draft]then[no]else[yes]] }}}>

<$set name="tv-freelinks-ignore-case" value={{$:/config/Freelinks/IgnoreCase}}/>

</$set>

OR you could make a Page Button that toggles the $:/config/Freelinks/Enable variable between “yes” and “no”

The action-createtiddler widget may give you some hints here.

Just saw you other comments. So this may be the option to go with

1 Like

This version uses the existence of the search popup tiddler to switch freelinks on and of. … It may be possible to create a “cascade” with the different options

<$set name="tv-freelinks" value={{{ [[$:/state/popup/search-dropdown--874113614]is[missing]then[yes]else[no]] }}}>

<$set name="tv-freelinks-ignore-case" value={{$:/config/Freelinks/IgnoreCase}}/>

</$set>

Just checking if you’re paying attention. :sweat_smile:

Can’t wait for future projects in this direction.

Have a nice day
Yaisog

I’ve been implementing a “throttled” version of the search field, which defers the main refresh cycle until after the user has stopped typing. It’s not much code, but adds e.g. a “throttleRefresh” attribute to <<keyboard-driven-input>>, which creates the search fields.

I will test and publish this as a PR soon enough.

In the meantime, based on some ideas from this thread, I created a StaleWidget, that I can wrap around my slow headers and footers. All it does is prevent its children from ever being refreshed, which has dramatically improved the performance of my TW with many open tiddlers.

2 posts were split to a new topic: Problem with Freelink-plugin Setting