In this chapter
In this chapter, we will start using timeline diagrams to represent sequences of actions over time. They help us understand how our software runs. They are particularly useful in a distributed system, like when a web client talks to a web server. Timeline diagrams help us diagnose and predict bugs. We can then develop a solution.
MegaMart support is getting a lot of phone calls about the shopping cart showing the wrong total. The customers add stuff to their cart, it tells them it will cost $X, but when they check out, they are charged $Y. That’s no good, and the customers are not happy. Let’s see if we can debug this for them.
The slow clickthrough seems to work. But clicking things quickly will result in a different outcome. Let’s check it out.
We can reproduce the bug by clicking on the buy button twice very quickly. Let’s see that:
We ran the same scenario (add shoes twice fast) several times. We got these answers:
** the correct answer
Here is the relevant code from the add-to-cart buttons: add_item_to_cart() is the handler function called when the button is pressed.
function add_item_to_cart(name, price, quantity) { ❶
cart = add_item(cart, name, price, quantity); ❷
calc_cart_total();
}
function calc_cart_total() {
total = 0;
cost_ajax(cart, function(cost) { ❸ ❹
total += cost;
shipping_ajax(cart, function(shipping) { ❺ ❻
total += shipping;
update_total_dom(total); ❼
});
});
}
❶ this function is run when the user clicks add to cart
❷ read and write to cart global variable
❸ AJAX request to products API
❹ callback when request is complete
❺ AJAX request to sales API
❻ callback when sales API answers
❼ add them up and show in DOM
It’s also useful to see the traditional use case diagram. Notice that the code talks to two different APIs sequentially:
Unfortunately, both the code and the use case diagrams look correct. And, in fact, the behavior of the system is correct if you add one item to the cart and wait a while before you add the next. We need a way to understand how the system operates when you don’t wait—when two things are running at the same time. Let’s look at timeline diagrams on the next page, which show us just that.
Following, you can see a timeline diagram showing two clicks that occur very quickly. A timeline is a sequence of actions. A timeline diagram graphically represents a sequence of actions over time. When put side by side, we can see how the actions can interact and interfere with each other.
Believe it or not, the diagram on the left clearly shows the problem that causes the incorrect behavior. In this chapter, we’re going to learn how to draw timeline diagrams from code. We’re going to learn how to read the diagram to see issues with timing. And we’re going to fix this bug (mostly!) using some of the principles of timelines.
There’s a lot to learn, and the angry customers won’t wait! So let’s get started learning to draw these diagrams.
Timeline diagrams show two main things: what actions will run in sequence and what will run in parallel. By visualizing those two things, we can get a good understanding of how our code will run—whether correctly or incorrectly. These two fundamental rules will guide us to translate our code into timeline diagrams. Let’s look at these two fundamentals.
Only actions need to be in timelines. Calculations can be left out because they don’t depend on when they are run.
We have different timelines when actions run in different threads, processes, machines, or asynchronous callbacks. In this case, we have two asynchronous callbacks. Because the timeout is random, we don’t know which will run first.
Summary
Once you can apply these rules, translating code is just a matter of understanding how the code runs over time.
It’s important to identify every action and to understand what order they are executed in. Every language has its own execution details, and JavaScript is no different. We’ve already seen this in part 1, but it’s worth emphasizing again because it’s going to be very important in timeline diagrams.
Two operators in JavaScript (and similar languages like Java, C, C++, and C#, among others) are very short to write. But their brevity hides the fact that there are three steps going on. Here’s the increment operator being used on a global variable:
total++; ❶
❶ this single operator does three steps
This increments the variable total. However, it’s just a shortcut for this:
var temp = total; ❶
temp = temp + 1; ❷
total = temp; ❸
❶ read (action)
❷ addition (calculation)
❸ write (action)
That’s three steps. First, it reads total, then it adds one to it, then it writes total back. If total is a global variable, then steps 1 and 3 are actions. The second step, adding one, is a calculation, so it doesn’t go on the diagram. This means that when you diagram total++ or total+=3, you will have to diagram two different actions, the read and the write.
If you call a function with an argument, the argument is executed before the function you’re passing it to. That defines the order of execution that needs to show up in the timeline diagram. Here’s an example where we are logging (action) the value of a global variable (action):
console.log(total) ❶
❶ the diagram for both is the same
This code logs a global variable total. To see the order clearly, we can convert it to equivalent code:
var temp = total; ❶
console.log(temp);
❶ the diagram for both is the same
This clearly shows that the read to the total global variable goes first. It’s very important to get all of the actions onto the diagram and in the right order.
We just learned the two main things that a timeline diagram shows—what is sequential and what is parallel. Now let’s draw the diagram for our add-to-cart code. There are three steps to drawing a timeline diagram:
We’ll just underline all of the actions. We can ignore calculations:
function add_item_to_cart(name, price, quantity) {
cart = add_item(cart, name, price, quantity); ❶
calc_cart_total();
}
function calc_cart_total() {
total = 0;
cost_ajax(cart, function(cost) { ❷
total += cost;
shipping_ajax(cart, function(shipping) {
total += shipping; ❸
update_total_dom(total);
});
});
}
Actions
❶ reading and writing global variables
❷ read cart then call cost_ajax()
❸ read total then write total
That’s 13 actions in this short section of code. We should also be aware that this has two asynchronous callbacks. One callback is passed to cost_ajax() and the other is passed to shipping_ajax(). We haven’t seen how to draw callbacks yet. Let’s put this code aside (remember, we just finished step 1) and come back to it after we’ve learned how to draw callbacks.
We’ve just seen that asynchronous callbacks happen in a new timeline. It’s important to understand how that works, which is why you’ll find a few pages describing the plumbing of JavaScript’s asynchronous engine. You should read those pages if you’re interested. Here, I’m just going to talk about why we’re using the dotted lines.
Here’s some illustrative code that saves the user and the document and manages loading spinners for them:
saveUserAjax(user, function() { ❶
setUserLoadingDOM(false); ❷
});
setUserLoadingDOM(true); ❸
saveDocumentAjax(document, function() { ❹
setDocLoadingDOM(false); ❺
});
setDocLoadingDOM(true); ❻
❶ save the user to the server (ajax)
❷ hide user loading spinner
❸ show user loading spinner
❹ save the document to the server (ajax)
❺ hide document loading spinner
❻ show document loading spinner
This code is really interesting because the individual lines of code are executed in an order that is different from how they’re written. Let’s walk through the first two steps of diagramming for this code to get a timeline diagram.
Three steps to diagramming
First, we underline all of the actions. We’ll assume that user and document are local vars, so reading them is not an action:
saveUserAjax(user, function() {
setUserLoadingDOM(false);
});
setUserLoadingDOM(true);
saveDocumentAjax(document, function() {
setDocLoadingDOM(false);
});
setDocLoadingDOM(true);
Actions
Step 2 is to actually draw it. We’ll step through the creation together over the next few pages. But here is what it will look like when we’re done. If you understand it, you can skip ahead.
JavaScript uses a single-threaded, asynchronous model. Whenever you have a new asynchronous callback, it creates a new timeline. But many platforms don’t use this same threading model. Let’s go over this threading model and some other common ways that threading works in languages.
Some languages or platforms do not allow multiple threads by default. For instance, PHP runs this way if you don’t import the threading library. Everything happens in order. When you do any kind of input/output, your whole program blocks while waiting for it to complete. Although it limits what you can do, those limits make reasoning about the system very easy. Your one thread means one timeline, but you can still have other timelines if you contact a different computer, like you would with an API. Those timelines can’t share memory, so you eliminate a huge class of shared resources.
JavaScript has one thread. If you want to respond to user input, read files, or make network calls (any kind of input/output), you use an asynchronous model. Typically, this means that you give it a callback that will be called with the result of the input/output operation. Because the input/output operation can take an unknown amount of time, the callback will be called at some uncontrollable, unknown time in the future. That’s why doing an asynchronous call creates a new timeline.
Java, Python, Ruby, C, and C# (among many others) allow multi-threaded execution. Multi-threaded is the most difficult to program because it gives you almost no constraints for ordering. Every new thread creates a new timeline. Languages in these categories allow unlimited interleaving between threads. To get around that, you need to use constructs like locks, which prevent two threads from running code protected by the lock at the same time. It gives you some control over ordering.
Erlang and Elixir have a threading model that allows for many different processes to run simultaneously. Each process is a separate timeline. The processes don’t share any memory. Instead, they must communicate using messages. The unique thing is that processes choose which message they will process next. That’s different from method calls in Java or other OO languages. The actions of individual timelines do interleave, but because they don’t share any memory, they usually don’t share resources, which means you don’t have to worry about the large number of possible orderings.
We’ve seen the final result of building the timeline, but it will be good to step through creating it one line of code at a time. Here’s our code again, and the actions that are in it:
1 saveUserAjax(user, function() {
2 setUserLoadingDOM(false);
3 });
4 setUserLoadingDOM(true);
5 saveDocumentAjax(document, function() {
6 setDocLoadingDOM(false);
7 });
8 setDocLoadingDOM(true);
Actions
JavaScript, in general, is executed top to bottom, so let’s start at the top with line 1. It’s easy. It needs a fresh timeline because none exist yet in the diagram:
Three steps to diagramming
Next up, line 2 is part of a callback. That callback is asynchronous, which means it will be called sometime in the future when the request completes. It needs a new timeline. We also draw a dotted line to show that the callback will be called after the ajax function. That makes sense because we can’t have the response come back before the request is sent.
Line 3 doesn’t have any actions on it, so we move onto line 4. It executes setUserLoadingDOM(true). But where does it go? Since it’s not in a callback, it happens in the original timeline. Let’s put it there, right after the dotted line:
We’ve already managed to draw half of the actions onto the diagram. Here’s the code, actions, and diagram for reference:
1 saveUserAjax(user, function() {
2 setUserLoadingDOM(false);
3 });
4 setUserLoadingDOM(true);
5 saveDocumentAjax(document, function() {
6 setDocLoadingDOM(false);
7 });
8 setDocLoadingDOM(true);
Actions
We just finished line 4, so now we look at line 5, which does another ajax call. The ajax call is not in a callback, so it’s part of the original timeline. We’ll put it below the last action we drew:
Three steps to diagramming
It is part of an asynchronous callback, which creates a new timeline that will start sometime in the future, when the response comes back. We don’t know when that will be, because networks are unpredictable. The diagram captures that uncertainty with a new timeline:
Line 8 has the last action. It’s in the original timeline:
We’ve completed step 2 for this code. We will do step 3 later. For now, let’s go back to our add-to-cart code and finish step 2.
A few pages ago, we identified all of the actions in this code. We also noted that there were two asynchronous callbacks. It’s time for step 2: Draw the actions on a diagram. Here’s what we had when we left off after identifying the actions:
Three steps to diagramming
function add_item_to_cart(name, price, quantity) {
cart = add_item(cart, name, price, quantity);
calc_cart_total();
}
function calc_cart_total() {
total = 0;
cost_ajax(cart, function(cost) {
total += cost;
shipping_ajax(cart, function(shipping) {
total += shipping;
update_total_dom(total);
});
});
}
Actions
Now that we have all the actions, our next step is to draw them, in order, on the diagram. Remember, ajax callbacks, of which we have two, require new timelines.
You can walk through the steps yourself to draw this diagram. We can check a few things: (1) All of the actions we identified (there were 13) are on the diagram, and (2) each asynchronous callback (there were two) resulted in a new timeline.
Before we move onto step 3, we’ll focus on what this diagram is telling us.
There are two ways that code can execute sequentially. Normally, any action can interleave between any two other actions in another timeline. However, in some circumstances, we can prevent interleaving. For example, in JavaScript’s threading model, synchronous actions don’t interleave. We’ll see more ways to prevent interleaving later.
That gives us our two kinds of sequential code. The timeline diagram can capture both.
Any amount of time can pass between two actions. We represent each action with a box. The time between them is represented with a line. We can draw the line to be short or long, depending on how much time it takes, but however long you draw it, it means the same thing: There is an unknown amount of time that may pass between action 1 and action 2.
Two actions run one after the other, and something is making it so that nothing can be run in between. What’s causing it? It could be due to the runtime or because of some clever programming (we’ll learn some of that later). We draw the actions in the same box.
These two timelines will execute differently. The timeline on the left might interleave, meaning an action 3 (not shown) may run between action 1 and action 2. In the timeline on the right, this is impossible.
The timeline on the left (interleavable actions) has two boxes, while the timeline on the right only has one box. Shorter timelines are easier to manage. We’d like fewer boxes rather than more.
We haven’t put multiple actions into one box yet. We usually do that in step 3, which we haven’t gotten to. But we will soon! There’s just a little bit more to learn about what the diagram is telling us.
In addition to representing sequential code, timeline diagrams express the uncertainty of ordering among parallel code.
Parallel code is represented by timelines drawn side by side. But just because they are side by side does not mean action 1 and action 2 will run at the same time. Actions in parallel timelines can run in three orders. In general, all three are possible.
When we read a timeline diagram, we have to see these three orders, regardless of how long the lines are and how the actions line up. The following diagrams all mean the same thing, even though they look different:
Being able to see these as the same is an important skill for reading timeline diagrams. You need to be able to imagine the possible orderings—especially those that may be problematic. We may draw diagrams differently to highlight one ordering, just for clarity.
Two timelines with one box each can run in three possible orderings. As timelines get longer or you get more timelines, the number of possible orderings goes up very quickly.
Like interleavings, the possible orderings are also dependent on your platform’s threading model. It’s also important to capture this in your timeline diagram, which we’ll do in step 3.
When we work with timelines, there are a few principles that guide us to improve our code so that it’s easier to understand and work with. Remember, one reason systems are hard is because of the number of possible orderings you have to account for. Although these five principles always apply, in this chapter we’re focusing on the first three. We’ll see the others in chapters 16 and 17.
The easiest system has a single timeline. Every action happens immediately after the action before it. However, in modern systems, we have to deal with multiple timelines. Multiple threads, asynchronous callbacks, and client-server communication all have multiple timelines.
Every new timeline dramatically makes the system harder to understand. If we can reduce the number of timelines (t in the formula on the right), it will help tremendously. Unfortunately, we often can’t control how many timelines we have.
Formula for number of possible orderings
Another lever we have is to reduce the number of steps in each timeline. If we can eliminate steps (decrease a in the formula on the right), we can reduce the number of possible orderings dramatically.
If two steps on different timelines don’t share resources, the order between them doesn’t matter. It doesn’t reduce the number of possible orderings, but it reduces the number of possible orderings that you have to consider. When looking at two timelines, you really only have to consider the steps that share resources across timelines.
If we eliminate as many shared resources as we can, we will still be left with some resources that we can’t get rid of. We need to ensure that the timelines share these resources in a safe way. That means ensuring they take turns in the right order. Coordinating between timelines means eliminating possible orderings that don’t give us the right result.
The ordering and proper timing of actions is difficult. We can make this easier by creating reusable objects that manipulate the timeline. We’ll see examples of those in the next couple of chapters.
In this and the next few chapters, we’re going to apply these principles to eliminate bugs and make our code easier to get right.
JavaScript’s threading model reduces the size of the problems of timelines sharing resources. Because JavaScript has only one main thread, most actions do not need separate boxes on the timeline. Here’s an example. Imagine this Java code:
int x = 0;
public void addToX(int y) {
x += y;
}
In Java, if I have a variable shared between two threads, doing the += operation is actually three steps:
+ is a calculation, so it doesn’t need to be on the timeline. That means that two threads running the addToX() method at the same time can interleave in multiple ways, resulting in different possible answers. Java’s threading model works that way.
However, JavaScript only has one thread. So it doesn’t have this particular problem. Instead, in JavaScript, when you have the thread, it’s yours for as long as you keep using it. That means you can read and write as much as you want with no interleaving. In addition, no two actions can run at the same time.
When you’re doing standard imperative programming, like reading and writing to shared variables, there are no timelines to worry about.
However, once you introduce an asynchronous call into the mix, you’ve reintroduced the problem. Asynchronous calls are run by the runtime at an unknown time in the future. That means the lines between the boxes can stretch and contract. In JavaScript, it is important to know whether you are doing synchronous or asynchronous operations.
The browser’s JavaScript engine has a queue, called the job queue, which is processed by the event loop. The event loop takes one job off of the queue and runs it to completion, then takes the next job and runs it to completion, and loops like that forever. The event loop is run in a single thread, so no two jobs are run at the same time.
The jobs on the job queue have two parts: the event data and the callback to handle that event. The event loop will call the callback with the event data as the only argument. Callbacks are just functions that define what should be executed by the event loop. The event loop just runs them with the event data as the first argument.
Jobs are added to the queue in response to events. Events are things like mouse clicks, typing on the keyboard, or AJAX events. If you put “click” callback function on a button, the callback function and event data (data about the click) are added to the queue. Because we can’t predict mouse clicks or other events, we say they arrive unpredictably. The job queue brings some sanity back.
Sometimes there are no jobs to process. The event loop might sit idle and save power, or it might use the time for maintenance like garbage collection. It’s up to the browser developers.
AJAX is a term for browser-based web requests. It stands for Asynchronous JavaScript And XML. Yes, it’s a silly acronym. And we’re not always using XML. But the term stuck. In the browser, we often communicate with the server using AJAX.
In this book, functions that make AJAX requests will have an _ajax suffix on them. That way, you know that the functions are asynchronous.
When you initiate an AJAX request in JavaScript, behind the scenes, your AJAX request is added to a queue to be processed by the networking engine.
After adding it to the queue, your code continues to run. It won’t wait for the request in any way—that’s where the asynchronous in AJAX comes in. Many languages have synchronous requests, which do wait for the request to complete before continuing. Because the network is chaotic, responses come back out of order, so the AJAX callbacks will be added to the job queue out of order.
You can register callbacks for various events on the AJAX request. Remember, a callback is just a function that will be called when an event fires.
Throughout the life of the request, many events are fired by the networking engine. There are two events that are particularly common to use: load and error. load is called when the response has been completely downloaded. error is when something goes wrong. If you register callbacks for those two events, you’ll be able to run code when the request is finished.
Here’s a simple page from the MegaMart site. Let’s look at all the steps to get the buy button to add items to the cart.
When the HTML page loads, we need to query the page for the button:
var buy_button = document.getElementByID('buy-now-shoes'); ❶
❶ find the button in the document
Then we need to set a callback for clicks to this button:
buy_button.addEventListener('click', function() { ❶
add_to_cart_ajax({item: 'shoes'}, function() { ❷ ❸
shopping_cart.add({item: 'shoes'});
render_cart_icon();
buy_button.innerHTML = "Buy Now"; ❹
});
buy_button.innerHTML = "loading"; ❺
});
❶ define a callback for ‘click’ events on the button
❷ initiate an ajax request
❸ this callback will be run when the ajax completes
❹ sometime later, when the ajax request is complete, we update the UI again
❺ immediately after initiating the request, change the button to say “loading”
Sometime later, the user clicks the button, which adds a job to the queue. The event loop will work its way through jobs in the queue until it gets to that click event job. It will call the callback we registered.
The callback adds an AJAX request to the request queue, which will be consumed by the networking engine sometime later. Then the callback changes the button text. That’s the end of the callback, so the event loop takes the next job off the queue.
Later, the AJAX request completes, and the networking engine adds a job to the queue with the callback we registered. The callback makes its way to the front of the queue, and then it is run. It updates the shopping cart, renders the cart icon, and sets the button text back to what it was.
We’ve finished step 2 of diagramming timelines. Now that we understand how our platform runs, we can simplify it in step 3. Here’s what we had:
1 saveUserAjax(user, function() {
2 setUserLoadingDOM(false);
3 });
4 setUserLoadingDOM(true);
5 saveDocumentAjax(document, function() {
6 setDocLoadingDOM(false);
7 });
8 setDocLoadingDOM(true);
Actions
We’re now starting step 3. This is where we simplify the diagram with knowledge of the threading model of our platform. Since all three of these timelines are running in JavaScript in the browser, we can apply our knowledge of the browser’s runtime to this diagram. In JavaScript, this boils down to two simplifying steps:
Three steps to diagramming
We have to perform these steps in order. Let’s turn the page and get to it.
On the last page, we had this diagram. Remember, this is the end of step 2. We have a complete diagram. Now we can simplify it in step 3:
Three steps to diagramming
In JavaScript, we have two simplifying steps we can perform thanks to the single-threaded runtime:
Let’s go through these two now.
Two JavaScript simplifications
Since JavaScript runs in a single thread, actions on a single timeline can’t be interleaved. A timeline runs to completion before any other timeline is started. If we have dotted lines, they are moved to the end of the timeline.
We can see how the JavaScript runtime simplifies the execution of code by eliminating lots of possible orderings.
Because the first timeline ends by creating two new timelines, this rule doesn’t apply. We’ll see it apply in our add-to-cart code. But that means we’re done with this one!
Before we move on, let’s look at what this timeline we just finished is telling us:
Remember, timeline diagrams show what possible orderings the actions can take. By understanding those orderings, we can know if our code will do the right thing. If we can find an ordering that won’t give the right result, we’ve found a bug. And if we can show that all orderings give us the right result, we know our code is good.
There are two kinds of orderings: certain and uncertain. Let’s look at the certain ones first. Because all of the actions on the main timeline (on the left) are in a single timeline, we know that these actions will happen in order. Further, because of the dotted line, we know the main timeline will complete before the others run.
Now let’s look at the uncertain orderings. Notice that the two callback timelines have different orderings. As we saw before, there are three possible orderings of one action in two timelines. Let’s look at them again:
In JavaScript, simultaneous actions are impossible since there is only one thread. So, we have two possible orderings, depending on which ajax response comes back first:
We’re always showing the loading spinner, and then hiding it, in that order. That’s good, so this code doesn’t have timing issues.
Alright, folks! We’ve been waiting for step 3 for a while. We can now apply it to our add-to-cart timeline. Here’s the result of step 2:
Because we’re still in JavaScript in the browser, we’re going to use the same two simplification steps we just used.
We have to perform these steps in order or it won’t work right.
Two JavaScript simplifications
Again, JavaScript runs in a single thread. No other thread will interrupt the current timeline, so there’s no possibility of interleaving between these timelines. We can put all actions in a single timeline into a single box per timeline:
Here’s where we were after consolidating all actions on a timeline into a single box on the last page:
Two JavaScript simplifications
Now we can do the second simplification.
Each timeline in our diagram ends by starting a new timeline. Each timeline ends with an ajax call where the callback continues the work. We can consolidate these three timelines into a single timeline:
Four principles for making timelines easier
** JavaScript’s threading model reduced our timelines from three to one, and went from 13 steps to 3.
Note that we can’t go back to step 1 at this point and put these all into one box. We have to leave it like this. Why? Because the separate boxes capture the possible interleaving that existed when they were shown on separate timelines.
This representation captures our intuition of callback chains—especially that they feel like a single timeline. It’s also easier to draw. And that’s the end of the three steps!
Let’s see how far we’ve come. Our first step was identifying the actions in our code. There were 13:
function add_item_to_cart(name, price, quantity) {
cart = add_item(cart, name, price, quantity);
calc_cart_total();
}
function calc_cart_total() {
total = 0;
cost_ajax(cart, function(cost) {
total += cost;
shipping_ajax(cart, function(shipping) {
total += shipping;
update_total_dom(total);
});
});
}
Actions
The second step was drawing the initial diagram. We captured two things in the timeline: whether the next action to draw was sequential or parallel. Sequential actions go in the same timeline. Parallel actions go in a new timeline:
Three steps to diagramming
Four principles for making timelines easier
The third step was simplification. Let’s review it on the next page.
On the last page we had completed a review of step 2:
Three steps to diagramming
The third and last step was to simplify our timeline using knowledge of our platform. Since it runs in the browser in JavaScript, we applied two steps. JavaScript’s single-threaded model allowed us to put every action in a single timeline into a single box. Then we could convert callbacks that continue the computation after an asynchronous action into a single timeline. The uncertainty of timing and the possibility of interleaving are captured in the diagram with multiple boxes.
Two JavaScript simplifications
The fact that we could simplify three timelines with 13 actions into a single, three-step timeline shows how much JavaScript’s threading model helps simplify things. However, the diagram also shows that it doesn’t completely eliminate the problem. Asynchronous actions still require separate boxes. On the next page, we’ll see how this diagram lets us diagnose the bug we discovered.
Four principles for making timelines easier
Here is the skill of drawing timeline diagrams in a nutshell.
Every action goes on the timeline diagram. You should dig into composite actions until you have identified the atomic actions such as reading and writing to variables. Be careful with operations that look like one action but that are actually multiple actions, such as ++ and +=.
Actions can execute in two ways: in sequence or in parallel.
Actions that execute in sequence—one after the other
If actions occur in order, put them on the same timeline. This usually happens when two actions occur on subsequent lines. Sequential actions also occur in other execution semantics such as left-to-right argument evaluation order.
Actions that execute in parallel—simultaneous, left first, or right first
If they can happen at the same time or out of order, put them on separate timelines. These can occur for various reasons, including these:
Draw each action and use dotted lines to indicate constrained order. For instance, an ajax callback cannot occur before the ajax request. A dotted line can show that.
The semantics of the particular language you are using might constrain the orderings further. We can apply those constraints to the timeline to help us understand it better. Here are general guidelines that apply to any language:
Actions in different timelines, in general, can occur in three different orders: simultaneous, left first, and right first. Evaluate the orders as impossible, desirable, or undesirable.
As we saw before, the steps the code takes to update the cart total look right for a single click to the button. The button only has a bug when we click it twice quickly. To see that situation, we have to put the timeline side by side with itself:
This shows that the two timelines, one for each click, can interleave with each other. There’s just one more touch-up we have to do. Since the original step on the timelines will be handled in order (the event queue guarantees that), we can adjust this slightly. We’ll add a dotted line to show that the second timeline can’t start until after the first step of the first timeline is done:
It may not seem like it now, but this diagram is screaming with problems. By the end of the chapter, you should be able to see it yourself.
Now that we’ve got our diagram set up for two clicks, let’s stretch out the lines between steps to emphasize different interleavings. Let’s first look at the easy interleaving that always gets the right result—two slow clicks.
Tracing through the steps in the diagram of a particular ordering shows how things play out. In this case, everything works out great. Now let’s see if we can find a possible ordering that produces the wrong answer, $16, which we saw in the real system.
We just saw an easy case where the second click happens after the first timeline is done. Let’s see if we can find an ordering where it gets the wrong answer. We’ll track the variables’ values on the right:
We found the bug! It has to do with the order that actions take on the click handler timelines. Since we can’t control the interleaving of the steps, sometimes it happens this way, and sometimes it happens one of the other ways.
These two relatively short timelines can generate 10 possible orderings. Which ones are correct? Which ones are incorrect? We could do the work and trace through them, but most timelines are much longer. They can generate hundreds, thousands, or millions of possible orderings. Looking at each one of them is just not possible. We need a better way to guarantee that our code will work. Let’s fix this code and make it easier to get it right.
We’ve got a pretty solid understanding of the timelines and our code. What in particular is causing the problem? In this case, the problem is caused by sharing resources. Both timelines are using the same global variables. They’re stepping all over each other when they run interleaved.
Let’s underline all of the global variables in the code.
function add_item_to_cart(name, price, quantity) {
cart = add_item(cart, name, price, quantity); ❶
calc_cart_total();
}
function calc_cart_total() {
total = 0; ❶
cost_ajax(cart, function(cost) {
total += cost;
shipping_ajax(cart, function(shipping) {
total += shipping;
update_total_dom(total);
});
});
}
❶ global variables
Actions sharing the total global variable have a
Actions sharing the cart global variable have a
Actions sharing the DOM have a
Then, for clarity, we can annotate the timeline steps with information about which steps use which global variables:
That’s a lot of sharing of resources! Every step reads and writes to total, which can cause bugs. If things happen in the wrong order, they could definitely mess with each other. Let’s start with the total global variable and convert it to a local one.
There is no reason to use a global variable for the total. The easiest improvement is to use a local variable instead.
function calc_cart_total() {
total = 0;
cost_ajax(cart, function(cost) {
total += cost; ❶
shipping_ajax(cart, function(shipping) {
total += shipping;
update_total_dom(total);
});
});
}
❶ the total might not be zero here. another timeline could write to it before the callback is called
function calc_cart_total() {
var total = 0; ❶
cost_ajax(cart, function(cost) {
total += cost;
shipping_ajax(cart, function(shipping) {
total += shipping;
update_total_dom(total);
});
});
}
❶ use a local variable instead
Well, that was easy! We got a lot of bang for converting total to a local variable. Our timeline still has three steps, so there are still 10 possible orderings. However, more of the orderings will be correct because they aren’t using the same global variable total.
But we still use the cart global variable. Let’s take care of that.
Remember the principle that stated fewer implicit inputs to an action were better? Well, it applies to timelines, too. This timeline uses the cart global variable as an implicit input. We can eliminate this implicit input and make the timelines share less in one go! The process is the same as for eliminating inputs to actions: Replace reads to global variables with an argument.
function add_item_to_cart(name, price, quantity) {
cart = add_item(cart, name, price, quantity);
calc_cart_total();
}
function calc_cart_total() {
var total = 0;
cost_ajax(cart, function(cost) { ❶
total += cost;
shipping_ajax(cart, function(shipping) { ❶
total += shipping;
update_total_dom(total);
});
});
}
❶ these two reads could read different values if cart is changed between reads
we still have one step that uses the cart global
function add_item_to_cart(name, price, quantity) {
cart = add_item(cart, name, price, quantity);
calc_cart_total(cart); ❶
}
function calc_cart_total(cart) { ❶
var total = 0;
cost_ajax(cart, function(cost) { ❷
total += cost;
shipping_ajax(cart, function(shipping) { ❷
total += shipping;
update_total_dom(total);
});
});
}
❶ add the cart as an argument
❷ these reads are not to the global variable anymore
We still have the one step that uses the global variable cart, but remember, the second timeline is constrained to run after the first step (hence the dotted line), so these first steps that use the cart will always run in order. They can’t interfere with each other. We’re going to use this property a lot throughout the rest of the book. It gives us a way to safely use global mutable state even in the presence of multiple timelines.
There’s still a bug in this code. We’re still sharing the DOM as a resource. We can’t just get rid of it because we need to manipulate the DOM. We’ll learn how to share resources in the next chapter.
Accounting wants to be able to use calc_cart_total() without modifying a DOM. They want the total that is calculated as a number they can use in other calculations, not as an update to the DOM.
But we can’t return the total as a return value from calc_cart_total(). It’s not available until the two asynchronous calls are completed. How can we get the value out? That is, how can we make this implicit output into a return value when using asynchronous calls?
In chapters 4 and 5, we saw how to extract implicit outputs into return values. The DOM modification is an implicit output, but it’s done in an asynchronous callback. We can’t use a return value. So what’s the solution? More callbacks!
When using asynchronous calls, we convert outputs into callbacks.
Since we can’t return the value we want, we’ll have to pass it to a callback function. At the moment, after we finish calculating the total, we pass total to update_total_dom(). We’ll extract that using replace body with callback:
Steps of replace body with callback
** it’s already in a function, so we don’t need to do this
Original
function calc_cart_total(cart) {
var total = 0;
cost_ajax(cart, function(cost) {
total += cost;
shipping_ajax(cart, function(shipping) {
total += shipping;
update_total_dom(total); ❶ ❷
});
});
}
function add_item_to_cart(name, price, quant) {
cart = add_item(cart, name, price, quant);
calc_cart_total(cart);
}
❶ body
❷ right now, we pass total to update_total_dom()
With extracted ballback
function calc_cart_total(cart, callback) { ❶
var total = 0;
cost_ajax(cart, function(cost) {
total += cost;
shipping_ajax(cart, function(shipping) {
total += shipping;
callback(total); ❶
});
});
}
function add_item_to_cart(name, price, quant) {
cart = add_item(cart, name, price, quant);
calc_cart_total(cart, update_total_dom); ❷
}
❶ replace with a callback argument
❷ pass update_total_dom() as the callback
Now we have a way of getting the total once it is completely calculated. We can do what we want with it, including writing it to the DOM or using it for accounting purposes.
We can’t return values from asynchronous calls. Asynchronous calls return immediately, but the value won’t be generated until later, when the callback is called. You can’t get a value out in the normal way, as you would with synchronous functions.
The way to get a value out in asynchronous calls is with a callback. You pass a callback as an argument, and you call that callback with the value you need. This is standard JavaScript asynchronous programming.
Synchronous functions
When doing functional programming, we can use this technique to extract actions from an asynchronous function. With a synchronous function, to extract an action we returned a value instead of calling the action within the function. We then call the action with that value one level up in the call stack. With asynchronous functions, we instead pass in the action as the callback.
Let’s look at two functions, one synchronous, one asynchronous, that otherwise do the same thing:
Asynchronous functions
Original synchronous function
function sync(a) { ❶
…
action1(b);
}
function caller() { ❷
…
sync(a);
}
❶ synchronous and asynchronous functions may look similar at first
❷ the way they are called will look similar
Extracted action
function sync(a) { ❶
…
return b;
}
function caller() { ❷
…
action1(sync(a));
}
❶ synchronous uses a return value: asynchronous uses a callback
❷ synchronous’s caller uses the return value to call action: asynchronous’s caller passes action as callback
Original asynchronous function
function async(a) { ❶
…
action1(b);
}
function caller() { ❷
…
async(a);
}
❶ synchronous and asynchronous functions may look similar at first
❷ the way they are called will look similar
Extracted action
function async(a, cb) { ❶
…
cb(b);
}
function caller() { ❷
…
async(a, action1);
}
❶ synchronous uses a return value: asynchronous uses a callback
❷ synchronous’s caller uses the return value to call action: asynchronous’s caller passes action as callback
In this chapter, we learned how to draw timeline diagrams and read them to discover bugs. We simplified the timelines using our knowledge of the JavaScript threading model, which shortened the timelines and reduced the number of timelines. We applied the principle of reducing shared resources to eliminate a bug.
We’ve still got that one shared resource, the DOM. Two add-to-cart timelines will both try to write different values to the DOM. We can’t get rid of that resource because we need to show the user their total. The only way to share the DOM safely is by coordinating between the timelines. We’ll see that in the next chapter.
18.116.62.45