TADS3 Adv3: Advice on minimizing/optimizing inventory lag?

I’m looking into tips on optimizing the inventory command, if that’s possible. I’ve managed to keep most of my game fairly optimized, but I’ve noticed a slight lag when opening the inventory. I’m sure this is due to high item count as it gets worse when the inventory length grows. It’s not a terrible lag, but I worry that it could become more noticeable as time goes on.

For reference, my game has a lot of high count items. Example - when foraging, you can come away with 15 deadwood. So, your inventory might look something like this:

You're carrying: 
	(Creepy-crawlies) - eight locusts and 17 snails. 
	(Fish) - a catfish. 
	(Flowers) - three lavender flowers, six camas flowers, five marsh marigold flowers, and two elderberry flowers. 
	(Forage) - a turkey tail mushroom, 10 camas bulbs, nine cypress bark slabs, and three swamp cabbages. 
	(Tools) - a fishing basket. 

This is obviously where the lag comes from, but I was wondering if there was a way to keep the high count system while optimizing it for lag? I’ve considered imposing an item limit, which would kind of suck considering that you need a fair few items for crafting, and it would make gameplay very tedious.

I’ve also played with the idea of creating dummy items to imply multiple items when there’s actually only a single physical item. I’ve done something similar with a coin purse. So, instead of having a physical purse item with lots of physical, tangible coins, you can have a singular physical purse item with intangible coins that can be discerned by observing the purse. See below:

class coin: Currency '(copper) coin*coins'
    name = 'coin'
    value = 0
;

coinPurse: Thing 'coin purse'
    
    name = 'coin purse'
    desc = "A leather purse, plain and simple. At the moment, <<if coin.value == 0>>the coin purse is void of coins. <<else if coin.value == 1>>the coin purse contains a single coin. <<else if coin.value >1>>the coin purse contains <<spellIntBelow(coin.value, 10)>> coins. <<end>> "

I’m sure I could somehow implement something similar in the inventory for each of my items, but of course, that would be a massive hassle and I’m not sure it would even eliminate lag if the lag is caused by “counting”.

I’m sorry if this is phrased strangely, but I hope I’m making sense. Has anyone dealt with something similar, or have you experienced inventory lag? How have you implemented optimizing for such things?

I haven’t touched TADS since the early 90’s so I can only speak in general terms.

Is the inventory code walking only what you’re holding, or is it walking the entire object tree looking for objects that you are carrying?

If there are (say) 10 possible categories of objects, are you looping over the object list 10 times, once per category, or are you walking the object list once, adding each object to one of 10 lists as it’s encountered?

If there are 17 snails, are they all individual objects, or is there one “snail” object that knows it represents 17 of them, kind of like how you did your coin purse?

-Dave

Hm, interesting question! I believe the inventory function iterates over the actors contents, so only what the player is holding.

I implemented the categories myself using listers, which I’m sure has added some lag as opposed to the default inventory function, which is uncategorized. I implemented them because my inventory was becoming cluttered (as per my post here). You make a good point though, as I’m unsure exactly how my lister iterates over the inventory.

There are 17 individual objects or snails, but I am toying with the idea of a representative object, like the coin purse, if it can optimize the gameplay.

Do you have a self-contained example that illustrates the problem, and what’s the target environment?

My guess is it’s something that’s happening with the custom listers that you’re using. The T3 VM will start chugging with very large object counts in a single context. But that usually only starts being noticeable at larger object counts.

Here’s a self-contained example that adds 60 objects to the player’s inventory:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>
#include <date.h>
#include <bignum.h>


class TS: object
        ts = nil
        construct() { ts = new Date(); }
        getInterval(d?) {
                if((d == nil) || !d.ofKind(Date)) d = new Date();
                return(((d - ts) * 86400).roundToDecimal(5));
        }
;

class Pebble: Thing '(small) (round) pebble' 'pebble'
        "A small, round pebble. "
        isEquivalent = true
;
class Rock: Thing '(ordinary) rock' 'rock'
        "An ordinary rock. "
        isEquivalent = true
;
class Stone: Thing '(nondescript) stone' 'stone'
        "A nondescript stone. "
        isEquivalent = true
;

startRoom: Room 'Room' "This is a test room. ";
+me: Person, PreinitObject
        count = 20
        execute() {
                addStuff(Pebble);
                addStuff(Rock);
                addStuff(Stone);
        }
        addStuff(cls) {
                local i, obj;

                for(i = 0; i < count; i++) {
                        obj = cls.createInstance();
                        obj.moveInto(self);
                }
        }
;

modify Actor
        showInventory(tall) {
                local ts;

                ts = new TS();
                inherited(tall);
                aioSay('\n===TIME = <<toString(ts.getInterval())>>\n ');
        }
;

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

Does it exhibit the same lag?

Edit: updated code to include a wall-clock timestamp.

Thank you for the assistance! Your self-contained example does not exhibit the same lag. However, I was able to recreate it by adding more “stuff“. Modification below.

class RedFish: Thing 'red fish' 'red fish'
    isEquivalent = true
;

class BlueFish: Thing 'blue fish' 'blue fish'
    isEquivalent = true
;

class GreenFish: Thing 'green fish' 'green fish'
    isEquivalent = true
;

class RedBug: Thing 'red bug' 'red bug'
    isEquivalent = true
;

class BlueBug: Thing 'blue bug' 'blue bug'
    isEquivalent = true
;

class GreenBug: Thing 'green bug' 'green bug'
    isEquivalent = true
;

startRoom: Room 'Room' "This is a test room. ";
+me: Person, PreinitObject
        count = 20
        execute() {
                addStuff(Pebble);
                addStuff(Rock);
                addStuff(Stone);
                addStuff(RedFish);
                addStuff(BlueFish);
                addStuff(GreenFish);
                addStuff(RedBug);
                addStuff(BlueBug);
                addStuff(GreenBug);
        }
        addStuff(cls) {
                local i, obj;

                for(i = 0; i < count; i++) {
                        obj = cls.createInstance();
                        obj.moveInto(self);
                }
        }
;

The time clock jumped from 0 to 0.5, which is minimal, but I imagine it would be easy to accomplish with the sheer volume of portable objects in my game. Especially since these objects aren’t sitting around, but spawned when performing actions (like forage).

That being noted - as I was going over my inventory lister to look for optimizations and create a self-contained example, I decided to try out my game without the custom inventory lister (using the library default), and I faced the same lag. My conclusion is that I simply have WAY too many items.

I’m not sure there’s a way to simplify the inventory action further, so I’ll have to take a look at how I handle objects. Either, creating a representative object for everything (ouch), or maybe changing the inventory command itself in some way?

Spit-balling here…

Maybe I can get the inventory command to do the counting outside of the actual inventory, like on item spawn. A hidden global that keeps tally of the items I’m “holding”, and the inventory command only has to read that global instead of iterating over every single item in my inventory at the drop of a hat… Hm, I’ll play around with that when I have the spare time this evening.

I appreciate the consults here, they’re pointing me in (hopefully) fruitful directions.

Digging into the call graph, the expensive path is (all on Actor:

  • showInventory()showInventoryWith()
  • showInventoryWith()inventorySenseInfoTable()
  • inventorySenseInfoTable()knowsAbout()

Nearly all of the time is spent on knowsAbout(), which always evaluates canSee(), hasSeen(), and the object’s .knownProp.

How I determined this

There’s another little performance profiling class I use (it’s also in the dataTypes module):

class TSProf: object
        label = 'tsProf'

        _ts = perInstance(new LookupTable())
        _val = perInstance(new LookupTable())

        start(id) { _ts[id] = new TS(); }
        stop(id) {
                local ts;

                if((ts = _ts[id]) == nil) return(nil);
                _val[id] = (_val[id] ? _val[id] : 0) + ts.getInterval();
                _ts.removeElement(id);
                
                return(true);
        }

        total(id) { return(_val[id] ? _val[id] : 0); }

        report() {
                "\n=====<<label>> START=====\n ";
                _val.forEachAssoc({
                        k, v: "\n<<toString(k)>>: <<toString(v)>>\n "
                });
                "\n=====<<label>> END=====\n ";
        }
;

…and then went through and used modify Actor to overwrite the various methods with debug-happy versions, e.g.:

        inventorySenseInfoTable() {
                local ambient, cont, info, ts, visInfo;

                ts = new TSProf();

                ts.start('visibleInfoTable()');
                visInfo = visibleInfoTable();
                ts.stop('visibleInfoTable()');

                if((info = visInfo[self]) != nil)
                        ambient = info.ambient;
                else
                        ambient = 0;

                ts.start('appendHeldContents()');
                cont = new Vector(32);
                foreach(local cur in contents) {
                        cont.append(cur);
                        cur.appendHeldContents(cont);
                }
                ts.stop('appendHeldContents()');

                foreach(local cur in cont) {
                        ts.start('knowsAbout()');
                        if(knowsAbout(cur)) {
                                ts.stop('knowsAbout()');
                                ts.start('SenseInfo()');
                                visInfo[cur] = new SenseInfo(cur,
                                        transparent, nil, ambient);
                                ts.stop('SenseInfo()');
                        } else {
                                ts.stop('knowsAbout()');
                        }
                }

                ts.report();

                return(visInfo);
        }

Bumping the number of objects to several hundred produces something like:

>i
=====tsProf START=====
visibleInfoTable(): .003
appendHeldContents(): 0
knowsAbout(): .49
SenseInfo(): 0
=====tsProf END=====
You are carrying eighty pebbles, eighty rocks, eighty stones, and 320 balls.

Exact number will depend on the hardware the game is running on, but the exact values aren’t as important as the ratios.

This means you probably can’t tweak your lister(s) to fix this.

What I’d be asking myself is if each of these things needs to be a “real” simulation object. That is, if these are resources the player is gathering, instead of having 100 individual widgets, the places you can gather from have a widget dispenser and the player has a single widget bag/container that has a counter that reports how many widgets it represents.

That works better if you don’t want the player to be able to just pick up and drop resources arbitrarily. As in if you gather pebbles from the pebble tree in the woods, but you can’t just drop pebbles wherever, you can only deposit them in the pebble hopper in the workshop, or whatever.

Given @jbg’s findings, one could try to exempt some classes of items from the knowsAbout(...) check.

You could make a new subclass like CraftingItem: Thing, which would be for the numerous ingredients (not for normal story items), and then override inventorySenseInfoTable() with a version where the relevant loop does not invoke knowsAbout(...) for those items.

So, instead of the standard version:

inventorySenseInfoTable() {
	
	// [...]
	
	foreach (local cur in cont) {
        if (knowsAbout(cur))
            visInfo[cur] = new SenseInfo(cur, transparent, nil, ambient);
    }
	
	// [...]
	
}

… you’d have something like:

inventorySenseInfoTable() {
	
	// [...]
	
	foreach(local cur in cont) {
		if (cur.ofKind(CraftingItem)) {	
			// skip the knowsAbout check for crafting items
			visInfo[cur] = new SenseInfo(cur, transparent, nil, ambient);
		} else {
			if(knowsAbout(cur)) 
				visInfo[cur] = new SenseInfo(cur, transparent, nil, ambient);
		}
    }
	// [considering that || is a short-circuiting operator, we could also shorten
	// it to something like: if(cur.ofKind(CraftingItem) || knowsAbout(cur)) ...]
	
	// [...]
	
}

I’ve tried this out and saw a big reduction of time for performing the inventory command.

But honestly, I haven’t looked deeper into this to see what kind of consequences can result from skipping the knowsAbout(...). For all I know, it will break desired behaviour somewhere else (after all, the standard library will have a good reason for doing the check). So, best treat this as just a very tentative, possibly terrible idea for experimentation. :)

Other than that, I’d also tend to say that you could try to separate the crafting system, if that’s where all those objects are needed, from the “normal” systems, and do manual bookkeeping by just representing the ingredients as numbers instead of actual items. (As others said above.)

I appreciate everyone’s input, it’s helped me a lot in how to best tackle this issue. Thank you!

I toyed with a few ideas from here, and I ultimately ended up with a resolution that uses both HoldAlls & “representative” objects. While there are lots of workarounds for my issue, I felt this solution better suited the style of my game. I will do my best to kind of explain what I did, for anyone interested:

Summary

First, I created a Token class that can be applied to objects that the player can acquire a high quantity of (crafting materials, in my case). The Token class has a “count” property that keeps track of the hypothetical quantity of the item. When the item is spawned or gathered, I used a custom function to NOT actually, physically add the item past the first count, only increase the count property. The goal is to make it LOOK like the player is carrying lots of items when they are only carrying a single representative “token”.

I then created a HoldAll (knapsack) to hold these items and list them categorically on examine rather than on opening the player’s inventory. This serves as a way to minimize unnecessary clutter, providing information on crafting materials only when the player prompts it.

While I’m not super skilled with TADS, and there’s lots of refining to do, I thought I’d provide an example of my code for anyone looking to do something similar. The example isn’t entirely self-contained, but hopefully you get the idea:

class Token: Thing //Class for "crafting materials"
    displayName = self.name + ' (<<self.tokenCount>>)' //This will be displayed by our HoldAll

    tokenCount = 0 //hypothetical quantity
    give(amount) //custom add function
    {
        if(!self.isIn(myKnapsack)) //do not add a physical token if there is already one present
        {
            self.moveInto(myKnapsack);
            self.disambigName = '<<self.name>> in the <<myKnapsack.name>>';
        }
        self.tokenCount = self.tokenCount + amount; //increase count
    }

    desc() //not necessary but I thought it might be helpful to see the count of the item on examination
    {
        if(self.isIn(me))
        {
            "(You're currently carrying <<spellIntBelow(self.tokenCount, 10)>> <<if self.tokenCount > 1>><<self.pluralName>><<else>><<self.name>><<end>>.) ";
        }
    }
    
    dobjFor(Take)
    {
        verify()
        {
        }
        check()
        {
            if(gDobj.isIn(myKnapsack) == true)
            {
                "You're already carrying the <<gDobj.name>>. ";
                exit;
            }
        }
        action()
        {
            "You put the <<gDobj.name>> in your knapsack. ";
            gDobj.give(1);
        }
    }
    
    dobjFor(Examine)
    {
        verify()
        {
        }
    }
    
    dobjFor(Feel)
    {
        verify()
        {
        }
    } 
    
    dobjFor(Smell)
    {
        verify()
        {
        }
    }
    
    dobjFor(Taste)
    {
        verify()
        {
        }
    }
    
    dobjFor(Default) //Here, I made tokens inaccessible to the player beyond the above actions - I intend to add other actions later in order to incorporate them with my crafting systems. I don't want to deal with dropping/throwing or whatever else.
    {
        verify()
        {
            illogical('That isn\'t something you can do with crafting materials. ');
        }
    }
;


tokenLister: SimpleLister //our custom lister for examining the holdall
    showListItem(obj, options, pov, infoTab)
    {
        say(obj.displayName); //display name of the token
    }
;

class HoldAll: RestrictedContainer //this doesn't have to be a restricted container, but I felt it would be useful for my own purposes later down the road. It has to be some kind of container though.
    willHold = nil //this should be a class - again optional
    canPutIn(obj) { return obj.ofKind(self.willHold); } //optional
;

myKnapsack: HoldAll 'my your knapsack*knapsacks' 'knapsack'
    location = me //my actor
    willHold = Token //tied to restricted container
    
    contentsListed = nil //I don't want this displayed
    contentsListedInExamine = nil //we will do this one ourselves
    
    desc()
    {
        local selfContents = self.contents;
        "It's a simple, leather knapsack used to hold crafting and cooking materials. <<if selfContents.length > 0>>Currently, it contains: <.p><<self.display()>><<else>>Currently, it's empty. <<end>>";
    } //display our contents in our own way on examine
    
    display()
    {
        local lst = [];
        foreach(local x in self.contents)
        {
            lst = lst + x;
        }

//below are my custom categories, defined by an additional class, ex: flower. Not self contained, sorry.

        local flowerObjs = lst.subset({x:x.ofKind(Flower)}); //Flowers
        local forageObjs = lst.subset({x:x.ofKind(Egg) || x.ofKind(Fungi) || x.ofKind(Fruit) || x.ofKind(Green) || x.ofKind(Root)}); //"Forage"
        local fishObjs = lst.subset({x:x.ofKind(Fish)}); //Fish
        local bugObjs = lst.subset({x:x.ofKind(Bug)}); //Creepy-crawlies
        local ingredientObjs = lst.subset({x:x.ofKind(Ingredient) || x.ofKind(Satchet)}); //Ingredients & Satchets
        local resourceObjs = lst.subset({x:x.ofKind(Resource)}); //Resources
        
//format here is listed simply, no flourishes - only category, name, and the count of the object.
        if (bugObjs.length)
        {
            "\t\t(Creepy-crawlies) - ";
            tokenLister.showSimpleList(bugObjs);
            ". <br>";
        }
        if (fishObjs.length)
        {
            "\t\t(Fish) - ";
            tokenLister.showSimpleList(fishObjs);
            ". <br>";
        }
        if (flowerObjs.length)
        {
            "\t\t(Flowers) - ";
            tokenLister.showSimpleList(flowerObjs);
            ". <br>";
        }
        if (forageObjs.length)
        {
            "\t\t(Forage) - ";
            tokenLister.showSimpleList(forageObjs);
            ". <br>";
        }
        if (ingredientObjs.length)
        {
            "\t\t(Ingredients) - ";
            tokenLister.showSimpleList(ingredientObjs);
            ". <br>";
        }
        if (resourceObjs.length)
        {
            "\t\t(Resources) - ";
            tokenLister.showSimpleList(resourceObjs);
            ". <br>";
        }
    }
;

//The above will result in something like this:
//x knapsack
//It's a simple, leather knapsack used to hold crafting and cooking materials. Currently, it contains:	
//    (Creepy-crawlies) - mud snails (7).
//    (Forage) - coastal roc eggs (8), chicken eggs (32), and mushrooms (25).
//>

//an example of an item with the implemented class
eggCoastalRoc: Token, Egg 'coastal roc egg*eggs' 'coastal roc egg'
    desc()
    {
        if(self.tokenCount > 1)
        {
            "The unsurprisingly large eggs of a coastal roc. Their thick shells are bumpy and dull blue in color. "; //plural desc
        }
        else
        {
            "The unsurprisingly large egg of a coastal roc. Its thick shell is bumpy and dull blue in color. "; //singular desc
        }
        inherited; //say how many I'm carrying.
    }
;

The above example resolves my issue sufficiently. I’m now able to hold an ungodly amount of items without any sort of lag on examination… because everything is fake.

It’s going to be an uphill battle to implement this into my existing crafting systems, but it’s probably for the best to make this adjustment now - plus it gives me the opportunity to refine some old and janky systems.

While I consider my issue solved, I’ll leave the discussion open here for anyone wanting to show off how they themselves deal with cluttered inventory and lag.

Cheers! :slight_smile: