One of the things I found very confusing when I first learned how to program was understanding when a certain piece of code runs. Of course, roughly speaking the code runs when you run the program. This timespan is called runtime. However, if the program contains asynchronous code runtime does not necessarily mean executing code which means there is another layer of timespans to consider. Depending on the language and setup we might also have a compile or build step and end up with another timespan called compile time. And to make everything really confusing, we can even write code that modifies our code at compile time such that compile time of one part is now runtime of another part of our code.
The code, tools and setup
// source.js import preval from 'babel-plugin-preval/macro' setTimeout(() => console.log(preval` const fs = require('fs') module.exports = fs.readFileSync(__dirname + '/message.txt', 'utf8') `), 1000)
If this looks super weird to you, that is totally fine.
We will go through it step by step.
After compiling it with
npm run build we get the compiled version that Babel generated in build.js:
// build.js setTimeout(() => console.log("Done.\n"), 1000);
Running this code with Node will do nothing for one second, then print the message to the screen and terminate. The steps we took in order to achieve this are:
- Write source code
- Compile source code
- Run compiled code
Now let’s map the different parts of the code to the different time spans.
const fs = require('fs') module.exports = fs.readFileSync(__dirname + '/message.txt', 'utf8')
This puts the content of the file message.txt directly into the compiled code. This works as long as there is such a text file in the directory where the code is compiled. For more detailed information about the hows, whats and whys of babel-plugin-preval have a look at its repository.
Therefore, one part of our source code actually executed at compile time! Although Babel and this plugin might be young projects, the basic concept is very old and present in many different programming languages. The C preprocessor can do similar things for example. There are important differences, but I just want to give a rough analogy. The important bit to understand is that the code above is part of our source code, but runs at compile time and is not part of our compiled code itself.
Runtime and Execution
Runtime is the time span from invoking the compiled program with Node to its termination.
This should be about one second for our example code.
However, most of this time none of our code is executed.
setTimeout call is synchronous and will run and terminate upon program invocation.
Then none of our code is executed for one second until the callback we gave to
setTimeout is called.
The complete process behind this is quite involved and asynchronous programming is a topic for itself.
In summary, our source code runs scattered over three different time spans. I admit that the example code is artificial, but the point is to have it as minimal as possible. The concept of the different time spans are not difficult, but can get blurred in real application code because there is a lot of other things to keep in your head. Distilling it into as few lines as possible really helped me and hopefully someone else too!