I created a State Machine class in Java to provide our FTC Robotics Teams a way to program a State Machine based Autonomous mode they can understand.
Here is the (really condensed) class. The real code has lots of comments, debugging info, and null protection stuff; but, I cut that out to shorten it for the post
public abstract class OpState {
private static Map<String, OpState> StateList = new HashMap<String, OpState>();
public final static void SetCurrentState( String state_name ){
OpState state = GetOpState(state_name);
CurrentState.OnExit();
CurrentState = state;
CurrentState.OnEntry();
}
public final static String GetCurrentState(){
return CurrentState.Name;
}
public final static void DoCurrentState() {
CurrentState.Do();
}
private final static OpState GetOpState(String name){
return StateList.get(name);
}
public final String Name;
public OpState(String name) {
Name = name;
StateList.put(Name, this);
}
protected void finalize() throws Throwable {
StateList.remove(Name);
super.finalize();
}
protected void OnEntry(){
}
protected abstract void Do();
protected void OnExit(){
}
}
The states are constructed as private members of the main class but the member variables are never used as they are looked up using the static HashMap. After that, the main class just calls the static OpState.DoCurrentState() and the class infrastructure takes care of the rest.
Ex:
private OpState forward = new DriveState("Forward", this, 0.50, 12.0, "Delay");
private OpState delay = new DelayState("Delay", this, 300, "Turn1");
private OpState turn = new TurnState("Turn1", this, 0.50, 180, "Delay2");
private OpState delay2 = new DelayState("Delay2", this, 300, "Forward2");
private OpState forward2 = new DriveState("Forward2", this, 0.50, 12.0, "Delay3");
private OpState delay3 = new DelayState("Delay3", this, 200, "Turn2");
public void start() {OpState.SetCurrentState("Forward");}
public void loop() {OpState.DoCurrentState();}
It works REALLY well except the as it is running, at some point it goes to do a SetCurrentState and can't find the state because the HashMap is empty. The Map is private and all the methods that access it have logs and there is no evidence in the log of a cause.
WHERE DID THEY GO!!! WHO TOOK MY STATES!!!
My only guess is that it is a java/android garbage collection issue; but, I don't understand why. There are references to the states but in the class where they are created and they are also in the Map. I don't see why they would be collected.
OK, so the problem definitely associated with Garbage Collection; but, as it turns out, the cause is a subtle code design flaw.
I was able to capture the event in the log:
11-22 16:38:00.922 8719-8807/? I/FIRST﹕ Doing OpState 'Turn1'
11-22 16:38:00.932 8719-8807/? I/FIRST﹕ Doing OpState 'Turn1'
11-22 16:38:00.942 8719-8807/? I/FIRST﹕ Doing OpState 'Turn1'
11-22 16:38:00.942 8719-8807/? I/FIRST﹕ Doing OpState 'Turn1'
11-22 16:38:00.942 8719-8723/? D/dalvikvm﹕ GC_CONCURRENT freed 1923K, 66% free 3827K/11088K, paused 3ms+3ms, total 36ms
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed 'Delay3' from StateMap
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed OpState 'Delay3'
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed 'Flash3' from StateMap
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed OpState 'Forward'
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed 'Delay1' from StateMap
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed OpState 'Delay'
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed 'Flash2' from StateMap
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed OpState 'Turn1'
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed 'Delay2' from StateMap
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed OpState 'Delay2'
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed 'Flash1' from StateMap
11-22 16:38:00.942 8719-8728/? I/FIRST﹕ Removed OpState 'Forward2'
11-22 16:38:00.942 8719-8807/? I/FIRST﹕ Doing OpState 'Turn1'
11-22 16:38:00.942 8719-8807/? I/FIRST﹕ Doing OpState 'Turn1'
11-22 16:38:00.952 8719-8807/? I/FIRST﹕ Doing OpState 'Turn1'
11-22 16:38:00.952 8719-8807/? I/FIRST﹕ Doing OpState 'Turn1'
11-22 16:38:00.962 8719-8807/? I/FIRST﹕ Could not find OpState'Delay2'
11-22 16:38:00.962 8719-8807/? I/FIRST﹕ StateMap has 0 members
11-22 16:38:00.962 8719-8807/? I/FIRST﹕ Exiting OpState 'Turn1'
11-22 16:38:00.962 8719-8807/? I/FIRST﹕ No OpState to Do
11-22 16:38:00.962 8719-8807/? I/FIRST﹕ No OpState to Do
The 'removed' messages are from the OpState.finalize(). One additional mystery exposed by this dump is how OpState Turn1 can continue to run if it has been collected. This and an excellent Memory Leak video by Patrick Dubroy lead me to what is causing the problem.
First some context. This code is runs as an OpMode in within the FTC Robotics Control Program Main App. It is the code the student writes to control the robot. You can have as many OpModes as you like and can choose between them.
What I believe is happening is the Main App is constructing a new copy my OpMode then (what I will call Original and Copy) then running the Copy. This may happen when the OpMode is Stopped and Restarted so that the new Start on a clean copy. This causes the OpStates for Copy to get added to the static StateList. Since they have the same name, they will push the Original OpModes out of the StateList and they will be available for GC. Then, when the get collected and the finalize gets called on the Original OpStates, it will cause the Copy OpStates to get removed from the StateList because they have the same name as the Original. This also explains how an OpState can continue to run as the one that are being collected are not the active ones.
So, the solution is easy. First, I remove the overloaded finalize() and let Java manage that. Second, rather than constructing the OpStates as members, I clear the StateList and construct the OpStates in the OpMode Start() method. Since I am pretty sure the Main App won't copy the OpMode while it is running, the list should stay intact and safe.