This is a demonstration of the features of IJuliaTimeMachine. It also explains some of the design choices.

What IJulia Provides

IJulia already provides some historical information. In records the inputs to cells, and Out records their outputs. IJulia.n is the number of the current cell.

Setting up IJuliaTimeMachine

I recommend assigning IJuliaTimeMachine a shorter name, like TM as below.

If you use it a lot, and don't want to keep typing the TM prefix, you can instead type using IJuliaTimeMachine. This will import @past and vars.

Recalling past states

Say that I just accidentally change the value of the variable x, and I'd like to know what it's value was after cell 5. I can recover the state of variables from then with @past.

The value of ans was also recalled.

The Time Machine stores a deepcopy of every variable. While this is inefficient, it allows us to recover a vector, even if some other cell changes one of its entries.

By default @past also recalls the output of the past cell, and so that output appears in the display.

This can be very useful because IJulia's Out stores a pointer to an array, rather than the array. This means that the value recalled can be changed, like this.

Note that the last element changed to a 0. This does not happen with the values stored by the Time Machine.

The Time Machine only stores variables it can effectively copy. Right now, it can not copy functions.

But, if the only thing that changes in a function is a global variable, then you can essentially recover the function.

You can stop the Time Machine from saving.

And, you can make it start saving again.

If we really want to forget the past, or just save memory, we can clear some history. Just give a list of the cells to be cleared. If no list is specified, it clears all of them. This is the safest option, because to free up the memory used by a variable, one must clear every cell in which that variable exists.

But, this didn't get rid of the string "hi :)", because that string was also saved in cell 32. You can see that by using the vars function, which returns a dictionary of all the variables saved with a cell. To get rid of that string, we would need to clear cells 30 and 32.

The big table of all the stored variables is TM.VX.store

If you want to see the cell output that TM stored,look at TM.ans(cell).

Variables are stored by hash so that each piece of data is only stored once. This reduces wasted memory. Unfortunately, we can not use Julia's default hash function to do this, as small changes to data do not necessarily change this hash. Here are two examples of this problem.

TM uses its own hash function to avoid this problem.

Unfortunately, tm_hash is can be slow because it must examine every element of a data structure. But, you probably don't want to store a big chunk of data in the TM anyway.

If you have some big piece of data that you do not want stored, you can tell the Time Machine not to do that. The function dontsave tells the TM not to store the variable it is given. But, if you copy the variable, the copy will be saved. Similarly, it will be saved if you bind the variable to a new piece of data.

On this laptop, it takes about half a second to hash an 800MB array. If Jupyter is reacting slowly, hashing a big variable could be the reason. You can fix this by telling Jupyter to stop saving it. But, you won't loose it.

IJulia.n stores the number of a cell. We will keep using this to index cells to facilitate editing of the notebook.

It even works with plots.

Running big jobs in threads

The other feature of Time Machine is that it lets you run intensive jobs in threads, so that you can get other work done while they are running. If you have a multicore machine, you can also view this as a way to manage running a bunch of experiments from Jupyter. The key is to wrap the jobs in TM.@thread begin, followed by end. Jobs that are running inside a @thread block sandbox their variables. To access the values of the variables from the thread after it finishes, look at vars, ans, or use @past.

When the jobs finish, their result is stored in Out, and you can access their state from past. Unfortunately, if the jobs contain any print statements, they can show up in other cells.

To see which jobs are running, look at TM.running. TM.finished contains a list of those that have finished.

In the examples below, we will simulate the delay of a long-running job with sleep. As you will see, results will change after jobs finish.

There is a subtle reason that I put the sleep(10) statement on a separate line. The output of finished jobs is only inserted into Out at the start of the execution of the first cell that is run after the job finishes. So, it is possible to go one cell without the output being correct. If you want to test it, put the sleep(10) inside the next cell, and then run the cells in quick succession.

You can not put two @thread statements into one cell.

But, you can have many running at once. That's the point!

You can also get notifications of when jobs finish. These can be sent to Jupyter, in which case they will just print. You can also send them to the terminal in which Jupyter is running, or both.