Skip to content

Caching Slow Softcode

Some softcode is slow and called often with the same result: counting every player with a lsearch(), or working out a character’s derived stats for each line of a listing. SharpMUSH has two functions that keep such an answer so the code does not run every time.

  • cache() stores the answer in an attribute and gives it back while the attribute is newer than a window you choose. The code may do anything.
  • memo() runs an attribute as u() does and keeps the answer in memory, per call. The code may not change anything, and the answer is dropped as soon as an attribute it read changes.
You want toRead
pick oneWhich to use
keep one answer for a whilecache()
keep the answers of a function without them going stalememo()
set how much memory memo() takesmemo() settings

In the game, help cache() and help memo() cover the same functions in short form.

Does the code change anything, or use rand() or the clock? Must the answer follow attribute changes at once, or differ by caller or arguments? cache() one stored answer, up to a window old memo() per call, dropped when reads change yes no no yes
cache()memo()
Where the answer is keptan attribute you namethe server’s memory
After a restartstill theregone
One answer foreveryone who calls iteach object, enactor and set of arguments
Goes stale whenthe window passesan attribute the code read changes, or the window passes
Code that changes thingsallowedrefused
rand(), secs() and other changing answersallowedrefused
Who may call itsomeone who controls the objectanyone who may call u() on it

Use cache() when an answer a few minutes old is fine and it is the same for everyone: the number of players, a summary built from many objects, a slow web lookup. Use memo() when the answer depends on its arguments and must never be out of date after an attribute changes: a character’s derived stats, a formatted sheet row.

cache(<object>, <attribute>, <value>[, <window>[, <sliding>]])

cache() looks at <attribute> on <object>. If it was written less than <window> seconds ago, it returns what is there. If not, it evaluates <value>, writes the result to the attribute and returns it. <value> is only evaluated on a miss, so the slow code does not run on a hit.

> think cache(me,CACHE`ROOMS,words(lsearch(all,type,room)),600)
12
> @dig Annex
> think cache(me,CACHE`ROOMS,words(lsearch(all,type,room)),600)
12

Output: 12 both times. The second call read CACHE`ROOMS instead of searching, so Annex is not counted until the 600 seconds have passed.

  • <window> is in seconds and is an hour when left out. 0 always misses, which runs the code and stores the result every time.
  • A true <sliding> writes the stored value again on every hit, which starts its window over. An answer read at least once a window never goes stale.
  • The attribute holds just the value, so get() and u() read it like any other.
  • The executor must control <object>. Nothing is printed when the value is written.
  • cache() writes an attribute, so with the function_side_effects option off it answers #-1 FUNCTION DISABLED.
> think cache(me,NOW,one,0)
one
> think cache(me,NOW,two,0)
two

Output: a window of 0 runs the code every time.

A sliding window suits an answer that is read often and costs a lot to build:

> think cache(me,CACHE`THINGS,words(lsearch(all,type,thing)),300,1)
40

Output: the number of things. Called again within five minutes, it returns 40 without searching and keeps it for five minutes more from then. Left unread for five minutes, the next call searches again.

Clear the attribute and the next call runs the code again. That is how code that changes the facts behind an answer makes it fresh at once:

> &CACHE`ROOMS me
> think cache(me,CACHE`ROOMS,words(lsearch(all,type,room)),600)
13

Output: 13, because CACHE`ROOMS was cleared, so the call searched again and counted Annex.

When the answer is built from a standard attribute, staff can have the game clear it whenever any copy of that attribute changes, with @attribute/watch. The event handler (#9) then runs its WATCH attributes for every change, by any command or function:

> @attribute/access LEVEL=none
> @attribute/watch LEVEL
> &WATCH`LEVEL`CACHE #9=&CACHE`LEVELS #9
> &LEVEL me=5
> think cache(#9,CACHE`LEVELS,ladd(iter(lsearch(all,type,player),get(##/LEVEL))),86400)
5
> &LEVEL me=7
> think cache(#9,CACHE`LEVELS,ladd(iter(lsearch(all,type,player),get(##/LEVEL))),86400)
7

Output: 7. The total is kept for a day, but setting LEVEL ran the watch, which cleared CACHE`LEVELS, so the second call added the levels up again. The watch runs as a queue entry of its own after the command that set LEVEL, so a read later in the same command list can still get the old total. Code that needs the new total at once clears the attribute itself.

If the game refuses to store the value, for example because it is too long or the attribute is locked, cache() still returns it and simply misses again next time.

Every attribute keeps the time its value was last written. mtime() and msecs() read it when given <object>/<attribute>:

> &NOTE me=hello
> think msecs(me/NOTE)
1794600600

Output: the seconds since 1970 when NOTE was written. mtime(me/NOTE) gives the same time as a date string. Setting an attribute under it, such as NOTEBELOW`, does not change NOTE’s time.

memo([<object>/]<attribute>[, <arg0>[, ... , <arg29>]])

memo() runs <attribute> as u() does and keeps the answer. A later call that runs the same code on the same object, from the same executor and enactor, with the same arguments, gets the kept answer back without running the code. Colour in an argument counts. <attribute> may also be a #lambda.

Memoizing pays most where the same answers are needed over and over. A recursive Fibonacci works out each smaller number again every time it needs one, so u() needs about 2.7 million calls for the 30th:

> &FIB me=if(lt(%0,2),%0,add(u(FIB,sub(%0,1)),u(FIB,sub(%0,2))))
> think u(FIB,30)
#-1 FUNCTION INVOCATION LIMIT EXCEEDED
> &MFIB me=if(lt(%0,2),%0,add(memo(MFIB,sub(%0,1)),memo(MFIB,sub(%0,2))))
> think memo(MFIB,30)
832040

Output: 832040. FIB runs out of function calls; MFIB keeps each answer, so it works out each number once and finishes at once.

A kept answer follows the attributes it read:

> &BONUS me=10
> &TOTAL me=add(%0,v(BONUS))
> think memo(TOTAL,5)
15
> &BONUS me=20
> think memo(TOTAL,5)
25

Output: 25, because TOTAL read BONUS and BONUS changed.

A kept answer is only safe if running the code again would give the same answer and do nothing else. So memo() refuses code that could do otherwise, and keeps nothing:

  • #-1 MEMO REFUSES SIDE EFFECTS when the code names a function that changes something, such as set(), pemit(), create() or a web request, anywhere in it, even in a branch that is not taken. Code reached through u() while it runs counts too. A form of such a function that only reads, such as name(<object>), is allowed.
  • #-1 MEMO REFUSES NONDETERMINISTIC FUNCTIONS when the code uses something whose answer changes from call to call: the random functions (rand(), shuffle(), die(), roll() and the like), the ones that read the clock (secs(), time(), and timefmt() without a time), the connection ones (idle(), conn(), pid() and the like), and the substitutions %c, %m, %u, %j, %>, %| and %?.
> &MARK me=[set(me,SEEN:1)]done
> think memo(MARK)
#-1 MEMO REFUSES SIDE EFFECTS

Output: #-1 MEMO REFUSES SIDE EFFECTS, and SEEN was not set.

  • an attribute the code read now reads differently, has appeared, or is gone; this is checked on every hit, and covers listings such as lattr() and grep(), so a new attribute in a listing counts;
  • the code itself is changed;
  • memo_window seconds have passed; or
  • memory is full, and it is the least recently used answer.

Other changes, such as a name, location, flag, lock or @function, are not seen until the window passes. An answer that hit a function limit, read attributes through indexed() or an alias, or is longer than 65,536 characters, is never kept. Messages the code sends, such as I can't see that here., are only sent when it runs.

These are under @config limits:

OptionDefaultWhat it does
memo_window3600Seconds an answer is kept at most. 0 means no time limit.
memo_max_entries10000Answers kept at most; past it the least recently used goes. 0 keeps none.
memo_max_bytes16777216Memory the answers may take, in bytes (16 MiB); past it the least recently used goes. 0 keeps none.

A long window is safe for code that only reads attributes, since a change to one drops the answer at once. Shorten it when the code reads names, locations, flags or locks.