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 asu()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 to | Read |
|---|---|
| pick one | Which to use |
| keep one answer for a while | cache() |
| keep the answers of a function without them going stale | memo() |
| set how much memory memo() takes | memo() settings |
In the game, help cache() and help memo() cover the same functions in short form.
Which to use
Section titled “Which to use”cache() | memo() | |
|---|---|---|
| Where the answer is kept | an attribute you name | the server’s memory |
| After a restart | still there | gone |
| One answer for | everyone who calls it | each object, enactor and set of arguments |
| Goes stale when | the window passes | an attribute the code read changes, or the window passes |
| Code that changes things | allowed | refused |
rand(), secs() and other changing answers | allowed | refused |
| Who may call it | someone who controls the object | anyone 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()
Section titled “cache()”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)12Output: 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.0always 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()andu()read it like any other. - The executor must control
<object>. Nothing is printed when the value is written. cache()writes an attribute, so with thefunction_side_effectsoption off it answers#-1 FUNCTION DISABLED.
> think cache(me,NOW,one,0)one> think cache(me,NOW,two,0)twoOutput: 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)40Output: 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.
Clearing a cached answer
Section titled “Clearing a cached answer”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)13Output: 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)7Output: 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.
When an attribute was written
Section titled “When an attribute was written”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)1794600600Output: 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()
Section titled “memo()”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)832040Output: 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)25Output: 25, because TOTAL read BONUS and BONUS changed.
What memo() refuses
Section titled “What memo() refuses”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 EFFECTSwhen the code names a function that changes something, such asset(),pemit(),create()or a web request, anywhere in it, even in a branch that is not taken. Code reached throughu()while it runs counts too. A form of such a function that only reads, such asname(<object>), is allowed.#-1 MEMO REFUSES NONDETERMINISTIC FUNCTIONSwhen 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(), andtimefmt()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 EFFECTSOutput: #-1 MEMO REFUSES SIDE EFFECTS, and SEEN was not set.
When a kept answer is dropped
Section titled “When a kept answer is dropped”- 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()andgrep(), so a new attribute in a listing counts; - the code itself is changed;
memo_windowseconds 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.
memo() settings
Section titled “memo() settings”These are under @config limits:
| Option | Default | What it does |
|---|---|---|
memo_window | 3600 | Seconds an answer is kept at most. 0 means no time limit. |
memo_max_entries | 10000 | Answers kept at most; past it the least recently used goes. 0 keeps none. |
memo_max_bytes | 16777216 | Memory 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.