Adv3 SenseConnectors and Game Performance

My crack team of playtesters has provided really great feedback so far. One thing that has come up, that I knew about but kind of ‘boiled frog’d’ my way to losing track of, was some extreme command line lag. Like, really extreme. It seems most prevalent when doing simple navigation (>E; >D; >ENTER; that kind of thing), but is not limited to that. A quick search did not turn up any relevant threads.

Before I start hardcore commenting/build reduction to isolate the issue thought I’d ask if anyone had experienced and solved this before?

Anecdotally, my game build FEELS quite large, but I have no scale for this kind of thing. I’m running just over 3M. Eliminating the debug build did not reduce that significantly. My total source file size comes to ~2M, with additional libraries to the tune of 100k. (I am including the massive “How to Play IF” Hint text though.)

2 Likes

Most of my experience in this issue happens when any math occurs which isn’t just simple integer math, or when trying to use the built-in pathfinding. I’ve had to implement custom pathfinding before, (based on @jbg 's work) just to keep lag down in some cases.

4 Likes

Speak my name and I am summoned.

Uh, no real general recommendations. I think most of the slowdowns I’ve discovered have been due to the TADS3 VM’s list update speed. Not counting things like programming errors on my part, and things I’ve architected around because they’re obviously too slow (like floating point arithmetic).

I think the place you’re most likely to run into this without lists being immediately visible as the root cause is in movement: if you’re moving more than a couple hundred objects every turn you’ll start to notice slowdown, independent of the movement logic.

And I think I’ve commented on this previously, but I had written a bunch of automated rule evaluation logic that tried to “optimize” by adding or removing rules from a list to be evaluated each turn, in the theory that minimizing the number of items evaluated would improve performance. But updating lists was much slower than using an active flag and checking it every turn, and as I recall the difference was more than an order of magnitude.

And I don’t know how useful this would be in your specific case, but what I’ve been doing is using a little timestamp object to do performance profiling in code I’m trying to streamline. This is the TS class from the dataTypes module, which works fine as a standalone thing, although you do need to #include both <date.h> and <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));
        }
;

Then on any builtin method that has a return value you can do something like:

modify someAdv3Object
        someMethod([args]) {
                local ts = new TS();
                local r = inherited(args...);
                _perf('someMethod()', ts.toInterval());
        }
;

…and elsewhere something like…

#ifdef __DEBUG
_perf(lbl, t) { aioSay('\n<<lbl>> took <<toString(t)>> seconds.\n '); }
#else // __DEBUG
_perf(lbl, t) {}
#endif // __DEBUG

That’s composed-in-the-browser code so there might be typos, but hopefully it illustrates the idea.

This doesn’t work for some bits of the library that rely on exception handlers for flow control and a few things like that, but generally it seems to be a fairly reliable, if tedious, way to troubleshoot performance traps.

3 Likes

I had to disable auto-travel/pathfinding in PQ in some of the outlying regions because it was taking so long to compute routes that the game looked hung. Another issue where a tester thought the game was hung because it took so long for the parser to compute the result of a command along the lines of verb obj1, obj2, obj5, obj2, obj2, obj3, where the commas were the killer factor.
I never did timestamp profiling, but there were ≈140 Actors in the game, and a lot of them I disabled from having a real “turn” each turn unless the PC were in their area.

2 Likes

Speak my name and I am summoned.

I think you mean “Say his name, and he appears.

Thankfully, I’ve never had a WiP get this large. OK, I’m out.

Thanks yet again for ever more tools in my debug toolbox. I think I have found the issue, one with additional weird artifacts! You probably don’t remember this tread, which was about SenseConnecting through doorways. I was bound and determined to implement a >LOOK THROUGH DOORWAY capability. Oh boy did I. I stitched these PassageSenseConnector things through the entire map.

Here’s a fun fact about adv3. Any command that operates on dObjs or iObjs starts with a connectionTable, intended to identify all game objects the PC can ‘see.’ This is the connectionTable() which collects everything contained in the PC and their container (usually Room or NestedRoom).

Here’s where things exploded on me. If you have a SenseConnector in the room, connectionTable() will add ITS containers’ items as well. Ie, if you have SenseConnected room A to room B, you will get everything in both rooms. If roomB has a DIFFERENT SenseConnector, THAT will get added (adding Room C), and so on and so on.

Because of my aggressive doorway sense connectors, I effectively stitched MY ENTIRE GAME INTO SCOPE FOR EVERY COMMAND, REGARDLESS OF LOCATION. The resolvers still managed to work those insanely long lists correctly, but boy did it take time. Note that conditional SenseConnectors (ie closed doors or the like) are ignored, the FACT of the connector is all that’s needed, not its state.

Here then is my advice for posterity: do not waste your time with doorway sense connectors. They will create more problems than solve. (Compounded by the fact that, to date, not one of my playtesters has tried to >LOOK THROUGH DOORWAY !)

If you feel you MUST use them, or have a different, unique SenseConnector model, at a minimum you should fix this:

//  Just a one line fix into this library routine, below
modify SenseConnector
    addDirectConnections(tab)
    {
        /* add myself */
        tab[self] = true;
            
        /* add my CollectiveGroup objects */
        foreach (local cur in collectiveGroups)
            tab[cur] = true;

        /* add my contents */
        foreach (local cur in contents)
        {
            if (tab[cur] == nil)
                cur.addDirectConnections(tab);
        }

        /* add my containers */
        foreach (local cur in locationList)
        {
            // if (tab[cur] == nil) // orig
            // JJMcC: omit other locations if connector 'closed'
            // this works, because recursion starts with PC, then the room they
            // occupy, THEN this connector in that room
            // meaning SenseConnectors will only ADD NEW ROOMS by
            // the time this method is invoked
            //  I think this would also work for PC portable SenseConnectors?
            //
            if ((tab[cur] == nil) && (connectorMaterial.senseThru(sight) != opaque)) 
                cur.addDirectConnections(tab);
        }
    }    
;
3 Likes

wilco.

For the rest, I suggest implementing an one depth level SenseConnector (that is, scoping only the directly connected location) or is still too much ?

Best regards from Italy,
dott. Piergiorgio.

1 Like

Wow, that’s a pretty huge gotcha! Being too lazy to go back and check this out myself, does the connectionTable still recursively include every other room if the SenseConnector object also inherits from Occluder? Most of my DistanceConnectors were also Occluders for one reason or another. Without digging I couldn’t say whether that would keep “downstream”-sensed objects from ending up in the connectionTable, if the Occluder occludes them.

1 Like

Lol, I can confirm this explicitly, in that my doorway connectors are custom PassageSenseConnector’s that are both SenseConnector and Occluders. More to the point, the code above is the SenseConnector addDirectConnections, which (before my hack) only cared that it was present, and did not check any Occlusions or blockages.

1 Like

I don’t think it would be too much, but it would need to be done in the same method, either under senseConnector, or the original in Thing. To some extent, I think the ‘right’ solution would strongly depend on the nature of the implementing game. You could easily see a lot of scenarios where you WANT chained senseconnected locations in scope, most especially for EXAMINEs.

For me, given my connectors (based on work by @jbg) are blocked by closed doors (which makes the connectionMaterial adventium), detecting these was easily implemented.

2 Likes

Huh. I’ve got a couple places where I’m doing some chained SenseConnector gymnastics but I haven’t run into any major logjams like this. That I noticed. Yet.

Now that I’m thinking about it: adv3 doesn’t have any sort of natural attenuation of “sense connection-ness”, does it? I haven’t started coding it, but I’ve got a number of situations where I care about line of sight across multiple outdoor locations, for example, and I want…or I think I want…some way of “naturally” attenuating visibility and audibility, specifically, based on lighting conditions, weather, and so on.

1 Like

One other interesting artifact of this default behavior. If you SC-chain your way into a location that explicitly ADDS items into scope, ie

    getExtraScopeItems(actor) { return [remoteCurtains]; }
    addToSenseInfoTable(sense, tab) {
        inherited(sense, tab);
        tab[remoteCurtains] =
            new SenseInfo(remoteCurtains, transparent, nil, 3);
    }

They are NOT later pruned out of scope during subsequent steps, like much of the rest of the list! You will find yourself able to >X CURTAINS anywhere the SC-chain exists. (Now, a player is unlikely to KNOW they can do this, depending on messaging. But still. Yuck.)

EDIT: FTR, easiest way to address is to qualify both methods with
if (gPlayerChar.isIn(roomOfInterest)) ...

1 Like

The good news is, you’ve got a way to assess performance impact above! There seems to be a tipping point at extreme sizes. I have some locations that are just STOCKED with items, hidden (which ARE initially part of connectionTable()) and otherwise that don’t stress performance once SC-chaining is cleaned up. Not obvious to me that addressing your scenario won’t be just fine with post-connectionTable object occluding/resolver pruning.

If you DO performance analyze those approaches would be curious what you find.

1 Like

Are you talking about the transparency levels like attenuated and obscured, and SenseConnector.transSensingThru?

Once again proving my posting is faster than my common sense, original solution needs some refinement. It ignores that some SenseConnector subclasses might use the transSensingThru(sense) method rather than connectorMaterial property. The latter is in fact referenced in the default transSensingThru.

//  Still just a one line fix into this library routine, below
modify SenseConnector
    addDirectConnections(tab)
    {
        /* add myself */
        tab[self] = true;
            
        /* add my CollectiveGroup objects */
        foreach (local cur in collectiveGroups)
            tab[cur] = true;

        /* add my contents */
        foreach (local cur in contents)
        {
            if (tab[cur] == nil)
                cur.addDirectConnections(tab);
        }

        // JJMcC NEW FIX:  omit other locations if 'closed'
        // note only works because if we get here, it was through
        // 'near' room meaning near room and contents already
        // in table
        //
        if (transSensingThru(sight) != opaque) {
            /* add my containers */
            foreach (local cur in locationList) {
                if (tab[cur] == nil) // orig
                cur.addDirectConnections(tab);
            }
        }
    }
;

Note that this, too, is perhaps incomplete. If you want sound, touch or odors to propagate across connectors that otherwise block sight, the above would need to be further refined. This is not my case at all. I humbly submit that maybe managing those directly in specific instances, or worst case creating a new Connector subclass would be the way to go there.

Or, honestly, no change needed if you don’t inadvertently stitch your entire game together with chained connectors.

1 Like

No, those are basically “static” filters for sense emissions. What I’m talking about is more some way to declare “when outdoors and it is dark, objects in lit locations can be seen from three rooms away, otherwise they can only be seen from an adjacent location”, and that kind of thing.

In the rev 0 kludged-together code for my WIP I have a number of things like this; a house with a light on in an upper story that can be seen from various distant locations, for example. My first pass at implementing this stuff just uses a bunch of special case spaghetti code, but I’d like to turn that into something that more or less sorts itself out. Like if I’m training the player that a light source lets them see things from a distance, if there’s an NPC wandering around with a light source it would be nice if that ended up working via the same logic, without having to hard-code a bunch of line-of sight logic for everyplace it might happen.

1 Like