Trees of timers. Timer trees. A timer forest?
Create timers in your code:
import { Timer } from 'gjallarhornjs';
fetchData = async () => {
let timer = new Timer('fetchData');
timer.start();
let requestTimer = timer.startChild('request');
requestTimer.startChild('fetch');
let response = await fetch(`https://api.github.com/users/${this.state.inputValue}`);
requestTimer.stopChild('fetch');
requestTimer.startChild('json');
let json = await response.json();
requestTimer.stop();
timer.startChild('setState');
this.setState({
github: json,
});
timer.stop();
console.log(JSON.stringify(timer.toJSON()));
timer.clear();
};
Get JSON output:
{
"name": "fetchData",
"duration": 224.00000000016007,
"children": [
{
"name": "fetchData:request",
"duration": 223.79999999975553,
"children": [
{
"name": "fetchData:request:fetch",
"duration": 223.5000000000582
},
{
"name": "fetchData:request:json",
"duration": 0.19999999858555384
}
]
},
{
"name": "fetchData:setState",
"duration": 0.10000000111176632
}
]
}
Inspired by heimdalljs, gjallarhorn.js is a library for timing things using trees. In its most basic form, gjallarhorn is just an abstraction over the built-in Performance API. However, in many cases when you're timing a chunk of code, you're not JUST interested in how long that code takes to execute, you're also interested in how long certain pieces of the code take to execute. More specifically, you probably know enough about the code you're testing that you don't see it as on big function, but rather a group of discrete steps, some of which may be dependent on other steps.
This is where the the trees come in!
Let's look at an example. Say you've got a function that makes an API request, and you're curious about what's taking it so long;
fetchData = async () => {
let response = await fetch(`https://api.github.com/users/${this.state.inputValue}`);
let json = await response.json();
this.setState({
github: json,
time: timer.value.duration,
});
};
The first step would be to simply time the function from beginning to end:
import { Timer } from 'gjallarhornjs';
fetchData = async () => {
let timer = new Timer('fetchData');
timer.start();
let response = await fetch(`https://api.github.com/users/${this.state.inputValue}`);
let json = await response.json();
this.setState({
github: json,
});
timer.stop();
console.log(JSON.stringify(timer.toJSON()));
timer.clear();
};
In this basic case, we'd get a simple JSON representation of single node in our Timer tree:
{
"name": "fetchData",
"duration": 230.7000000002081
}
But let's say that's not enough information. We want to know where the time is being spent, so let's measure things in a bit more detail. More specifically, we want to measure the fetch
request and the setState
call, and even more than that, we want to time the fetch
itself as well as the call to .json()
. In other words, we see the key points of this function's execution as the following tree:
fetchData
├── request
│ ├── fetch
│ └── json
└── setState
This is the most basic form of how we'd do this. Note: if this looks painfully verbose to you, read on. Abstractions incoming.
fetchData = async () => {
let timer = new Timer('fetchData');
timer.start();
let requestTimer = new Timer('fetchData:request');
timer.append(requestTimer);
let fetchTimer = new Timer('fetchData:request:fetch');
requestTimer.append(fetchTimer);
requestTimer.start();
fetchTimer.start();
let response = await fetch(`https://api.github.com/users/${this.state.inputValue}`);
fetchTimer.stop();
let jsonTimer = new Timer('fetchData:request:json');
requestTimer.append(jsonTimer);
jsonTimer.start();
let json = await response.json();
jsonTimer.stop();
requestTimer.stop();
let setStateTimer = new Timer('fetchData:setState');
timer.append(setStateTimer);
setStateTimer.start();
this.setState({
github: json,
});
setStateTimer.stop();
timer.stop();
console.log(JSON.stringify(timer.toJSON()));
timer.clear();
};
When all of the above is said and done, we'll get an output that looks like this:
{
"name": "fetchData",
"duration": 224.00000000016007,
"children": [
{
"name": "fetchData:request",
"duration": 223.79999999975553,
"children": [
{
"name": "fetchData:request:fetch",
"duration": 223.5000000000582
},
{
"name": "fetchData:request:json",
"duration": 0.19999999858555384
}
]
},
{
"name": "fetchData:setState",
"duration": 0.10000000111176632
}
]
}
Cool, right? The only problem is that the code we had to write to get this output was so tedious. Having to manually construct, start, and append each node to its parent, while also making sure to give them labels so we can see each node's ancestry was a pain. Luckily, Gjallarhorn has you covered. Rather than using the Timer
constructo along with append
and start
for every single timer, we can instead use the built-in startChild
method. startChild
will construct a new timer, append to the parent, create a new label that includes the ancestry information, and start the timer for you.
Here's what the simpler version looks like:
fetchData = async () => {
// create and start root timer
let timer = new Timer('fetchData');
timer.start();
// create and start the 'request' timer, and then call its `startChild` method to do the same for
// the `fetch` timer
let requestTimer = timer.startChild('request');
requestTimer.startChild('fetch');
let response = await fetch(`https://api.github.com/users/${this.state.inputValue}`);
// Use Timer's `stopChild` method to halt our `fetch` timer while the `requestTimer` keeps running
requestTimer.stopChild('fetch');
requestTimer.startChild('json');
let json = await response.json();
// Stop the request timer. Notice how we never called `requestTimer.stopChild('json')`.
// This is because calling `stop` on any time will automatically stop all of its still-running children.
requestTimer.stop();
timer.startChild('setState');
this.setState({
github: json,
});
// Again, now that we know we're done, we call `timer.stop()` knowing that it will also stop any
// still-running child timers as well
timer.stop();
console.log(JSON.stringify(timer.toJSON()));
timer.clear();
};
This version will give the exact same output as the previous timer code, but it takes a lot less work to get there. Here are the highlights:
- Every instance of
Timer
has astartChild
andstopChild
method, both of which take a string as their only argument. Use this to label what your timer is timing. startChild
will automatically take the label you provide and concatenate it with its parent timers using a:
, so you don't have to worry about including parent information in child labels,startChild
does it for you.startChild
will return the child timer that it creates, which you can use to get more fine-grained control over the child timer. In the example above, we use it to create more deeply-nested children.- Calling
stop
on anyTimer
instance will stop it as well as all of its still-running children. This is handy if you have a lot of nested timers and want to just clean everything up at the end. - All
Timer
instances have atoJSON
method that will give you a JSON representation of the Timer tree you've created, using itself as the root. - All
Timer
instances have aclear
method which will wipe away all the performance measures and marks. Usually you want to use this right before the code you're timing finishes executing (but make sure you do this after you've used the timing data!)
This library is heavily inspired by heimdalljs, specifically its use of tree structures. In fact, the name (gjallarhorn) is a reference to this inspiration.
yarn add gjallarhornjs
import { Timer } from 'gjallarhornjs';
let timer = new Timer('thisIsATimer');
timer.start();
// ...do stuff...
timer.stop();
let data = timer.value;
timer.clear();
git clone <repository-url>
cd gjallarhornjs
yarn install
This project is licensed under the MIT License.