eonarheim / TypeState

A strongly typed finite state machine for TypeScript
http://eonarheim.github.io/TypeState/example/
BSD 2-Clause "Simplified" License
269 stars 28 forks source link

Shared states between FSM's #20

Open ghost opened 6 years ago

ghost commented 6 years ago

I have a situation where I have a lot of FiniteStateMachine instances (one per connected client), but they all have the same transitions defined.

To reduce memory consumption, it would be great if these could share the defined transitions.

Would it be possible to implement an API that creates an FSM based on an existing one?

I belive this is the information that needs to be shared:

public fromStates: T[];
public toStates: T[];

Any good workarounds in the current implementation?

eonarheim commented 6 years ago

Hi @notsonotso,

One possible workaround if you suspect TypeState as a memory culprit is to build a factory that constructs FSMs on demand given a set of shared states and transition functions.

If you are building FSMs from a state enum, those pieces should be relatively small (fromStates and toStates). The biggest memory hassle would be duplicate callback handlers and transition functions being defined. (Although depending on the complexity of your FSM this might not be a problem)

My example shows how to reuse handlers, and hack shared transition functions into such a factory. Profiling this with a 500 FSMs from the factory kept the heap to ~1kb (which is mostly just FSM instances and not duplicated handlers or transition functions).

Let me know if this isn't what you're looking for, or if I'm off track.

image

image

// typescript 2.4.2
enum States {
    A = "A",
    B = "B",
    C = "C",
    D = "D"
}

let ClientFSMFactory = function(){

    let onACallback = (from) => { console.log(from) };
    let onBCallback = (from) => { console.log(from) };
    let onCCallback = (from) => { console.log(from) };
    let onDCallback = (from) => { console.log(from) };

    let fromAtoB = new typestate.TransitionFunction<States>(null, States.A, States.B);
    let fromBtoC = new typestate.TransitionFunction<States>(null, States.B, States.C);
    let fromCtoD = new typestate.TransitionFunction<States>(null, States.C, States.D);
    let fromDtoA = new typestate.TransitionFunction<States>(null, States.D, States.A);

    let tnsFcns = [fromAtoB, fromBtoC, fromCtoD, fromDtoA];

    return { 
        getFSM: () => {
            let fsm = new typestate.FiniteStateMachine<States>(States.A);
            (<any>fsm)._transitionFunctions = tnsFcns;

            fsm.on(States.A, onACallback);
            fsm.on(States.B, onBCallback);
            fsm.on(States.C, onCCallback);
            fsm.on(States.D, onDCallback);

            return fsm;
        }
    }
}

let factory = ClientFSMFactory();

let fsms: typestate.FiniteStateMachine<States>[] = [];
for (let i = 0; i < 500; i++) {
    fsms.push(factory.getFSM());
}

let currentState = 1;
let possibleStates = [States.A, States.B, States.C, States.D];

function runfsms(){
    for (let state of possibleStates){
        for (let fsm of fsms) {
            fsm.go(state)

            console.log("Current state:", fsm.currentState);
            console.log("Current state:", fsm.currentState);
        }
    }
}

runfsms();

I'm a little hesitant to create an duplicate FSM feature that shares the internal state, shared state is a recipe for subtle bugs.

However, it may be worthwhile to allow transition functions to easily exist independent of a FSM since they only really describe a valid transition and contain no state so could easily be shared. This would allow for an easier experience building the factory I described.

I've opened an issue to allow this behavior https://github.com/eonarheim/TypeState/issues/21

ghost commented 6 years ago

Thanks for looking into this! I'll try your workaround.

Profiling this with a 500 FSMs from the factory kept the heap to ~1kb

To be honest, I wish this was a bit lower. This overhead will be added to all (~20.000) connected clients and is definitely enough to introduce some serious GC pressure. Not sure what can be done, though.

I'm a little hesitant to create an duplicate FSM feature that shares the internal state, shared state is a recipe for subtle bugs.

Fair point. That said, I would be fine with the library throwing an error if attempting to alter transitions once sharing has occurred.

eonarheim commented 6 years ago

If you are facing GC pause pressure from object overhead right now, we could reorganized that factory into a pool and pre-allocate ~20,000 FSM at process start. Then you'd only see memory increases/GCs when you need to resize the pool. This is a good example of what I'm talking about https://www.html5rocks.com/en/tutorials/speed/static-mem-pools/

Reseting a FSM after a client connection should be pretty simple (I think you'll only need to zero out the transition functions and reset .currentState to whatever your start state should be).

What kind of constraints are you working with right now on your server?