[SIT Belgium 2019 Recap] Debugging Node.js Applications in SCP CF. Part 4: Time Travel Debugging
This is a recap of the session “Troubleshooting Node.js applications: Debugging in SAP Cloud Platform Cloud Foundry” that took place in a recent SAP Inside Track Belgium 2019 event.
Slide deck of the session is available at SlideShare.
Overview of the session recap blog series:
|Part 1: Remote Debugging – Environment Preparation and General Techniques|
|Part 2: Remote Debugging in Production Environment|
|Part 3: Accessing Service Instance|
|Part 4: Time Travel Debugging|
Constraints of forward debugging technique and introduction to time travel debugging
When debugging applications and using breakpoints, a developer employs a traditional forward debugging technique when following program flow and stepping through executed statements. There are times when a breakpoint is set too late in the debugged program, and by time the program reaches the breakpoint and program’s execution is paused, a code snippet that could have caused a bug, is already missed. Usually the developer terminates current debugging session, sets a new breakpoint earlier in the program and repeats program execution with conditions that would lead program flow to hit a newly set breakpoint. The program reaches a new breakpoint, program’s execution is paused, and the developer gets back to debugging.
Alternatively, it is also common to set a breakpoint early in a source code of the program, but there is a risk of setting a breakpoint too early and facing necessity of stepping through executed statements for a long time until an appropriate code snippet is determined.
Both approaches of a forward debugging technique described above can contribute to increased time required by the developer to get to a root cause and pause execution of the debugged program at a code snippet that causes an issue.
It would have been convenient to be able to revert execution of the program from a breakpoint where its execution has been paused and take several steps back to analyse earlier state of the program. That is exactly a purpose of an alternative debugging technique – reverse debugging or time travel debugging – that will be discussed in this blog and that will be applied to a Node.js application.
Time travel debugging is mostly relevant when coping with two types of debugging scenarios:
- Reverse debugging of a running program – to analyze earlier states of the debugged program,
- Postmortem debugging of a program that does not run anymore (crashed or was terminated) – for root cause analysis of possible reasons of program termination.
A standard approach that is utilized to implement time travel debugging is based on record-replay-snapshot architecture – which is, to take snapshots of program’s state by recording non-deterministic runtime interactions within the program during normal execution of the program. Using generated and collected snapshots, it is then possible to restore state of the program and replay forward flow of the program, starting from any of available snapshots. Snapshots also make it possible to execute reverse flow – instead of stepping through snapshots in a chronological order of their collection, we can run through collected snapshots in a reverse order and explore states of the program in a reverse chronological order – essentially, reverse debugging the program. Given collected snapshots are persisted, they can be replayed even if the program does not run anymore – for example, the program might have crashed or terminated, but if earlier recorded snapshots of its execution are available, they can be replayed, making it possible to run postmortem debugging of the program.
Sounds wonderful, isn’t it? There are certain technical obstacles that get on the way and prevent advancement and wide adoption of this technique – most remarkable and significant are following:
- Runtime overhead caused by collection of snapshots during forward execution of the program – collection of large volume of information about program state per each snapshot, multiplied by number of required snapshots (which is driven by snapshots generation frequency and overall duration of recording). As a side effect, it also impacts disk space that is required to store collected snapshots,
- Response time and delays when stepping through snapshots in reverse debugging and snapshots replay modes that are caused by parsing snapshots and restoring state of the program at each discrete point of collection over time series.
There are technical implementations of time travel debugging systems for a number of runtimes. A goal of such systems is to provision affordable time travel debugging system that addresses above mentioned constraints and finds a compromise between ultimate reverse debugging functionality and overhead and costs that are incurred by a system and associated with such functionality.
Tools for time travel debugging in Node.js
- Alternative Node.js runtime named Node ChakraCore,
The extension also adds a new debugging configuration type that can be used as a shortcut to create an appropriate debugging configuration for launching a Node.js application and using Node ChakraCore runtime for it:
Alternatively, arguments and parameterization that are required for usage of time travel debugging functionality in Node ChakraCore, can be provided manually when starting the Node.js application or composing debugging configuration for it.
Another remark that shall be put in advance, is about Node ChakraCore engine: as per current documentation, “this project is still work in progress and not an officially supported Node.js branch”, so usage of Node ChakraCore engine shall be well-considered and taken with precaution.
Time travel debugging in action
In sake of simplicity, following debugging demo is conducted in a local environment. The debugged Node.js application has been launched using debugging configuration mentioned above, and when the breakpoint has been reached and program flow has been paused, it can be observed that a debugger got several new buttons that are a part of a time travel debugger and are used to perform reverse step and reverse step dynamic operations. Reverse step operation is used to navigate to a previous step in the current frame, whereas reverse step dynamic is used as an opposite action to step into functionality in a traditional forward debugging technique.
When using a time travel debugger in Visual Studio Code, it is possible to choose between forward debugging the program or switching to Time-Travel Replay mode and analyzing the program state based on collected snapshots, including possibility to step back to previously executed statements:
Stepping back through recorded snapshots can be conducted even if the debugged program has already crashed or has been terminated.
It is possible to download required Node.js runtimes and switch between them using interactive interface of the tool, or to utilize corresponding commands and handy shortcuts:
When switching over to a target Node.js runtime, Node Version Switcher dynamically overwrites path environment variable.
Together with usage of command line interface, it is also possible to make use of Node Version Switcher from within debugging configuration in Visual Studio Code and specify required Node.js runtime in there (assuming that target Node.js runtime has been added in Node Version Switcher already):
- Example with Node ChakraCore engine: