15 Isolating timelines

Image

In this chapter

  • Learn how to draw timeline diagrams from code.
  • Understand how to read timeline diagrams to find bugs.
  • Discover how to improve code design by reducing resources shared between timelines.

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.

Image There’s a bug!

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.

We can’t reproduce it when we click through slowly

Image
Image
Image

The slow clickthrough seems to work. But clicking things quickly will result in a different outcome. Let’s check it out.

Image Now we can try to click twice fast

Customers told us the bug happened when they clicked quickly

We can reproduce the bug by clicking on the buy button twice very quickly. Let’s see that:

Image
Image

We tested it a few more times and got a variety of results

We ran the same scenario (add shoes twice fast) several times. We got these answers:

  • $14**
  • $16
  • $22

    ** the correct answer

Image Noodle on it

It looks like the speed of the clicks is causing a problem. What do you think is happening?

Let’s read the code to understand the bug

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:

Image

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.

The timeline diagram shows what happens over time

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.

Image Vocab time

A timeline is a sequence of actions over time. There can be multiple timelines running at the same time in your system.

Image

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.

The two fundamentals of timeline 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.

1. If two actions occur in order, put them in the same timeline

Image

Only actions need to be in timelines. Calculations can be left out because they don’t depend on when they are run.

2. If two actions can happen at the same time or out of order, they belong in separate timelines

Image

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

  1. Actions either run sequentially or in parallel.
  2. Sequential actions go in one timeline, one after the other.
  3. Parallel actions go in multiple timelines side-by-side.

Once you can apply these rules, translating code is just a matter of understanding how the code runs over time.

Image It’s your turn

Here’s some code that handles dinner. Draw the timeline diagram that corresponds to it. Each of the functions that dinner() calls is an action.

function dinner(food) {

cook(food);

serve(food);

eat(food);

}

Image

Image

Image Answer

Image

Image It’s your turn

Here’s some code for three people eating dinner separately. Each happens as an asynchronous callback—dinner() is run when a button is clicked. Finish the timeline diagram that corresponds to three fast button clicks.

function dinner(food) {

cook(food);

serve(food);

eat(food);

}

 

button.addEventListener('click', dinner);

Image

Image Answer

Image

Two tricky details about the order of actions

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.

1. ++ and += are really three steps

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.

Image

2. Arguments are executed before the function is called

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

Image

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.

Drawing the add-to-cart timeline: Step 1

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:

  1. Identify the actions.
  2. Draw each action, whether sequential or parallel.
  3. Simplify using platform-specific knowledge.

1. Identify the actions

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

  1. 1. Read cart.
  2. 2. Write cart.
  3. 3. Write total = 0.
  4. 4. Read cart.
  5. 5. Call cost_ajax().
  6. 6. Read total.
  7. 7. Write total.
  8. 8. Read cart.
  9. 9. Call shipping_ajax().
  10. 10. Read total.
  11. 11. Write total.
  12. 12. Read total.
  13. 13. Call update_total_dom().

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.

Asynchronous calls require new timelines

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

  1. Identify actions.
  2. Draw each action.
  3. Simplify.

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

  1. 1. saveUserAjax()
  2. 2. setUserLoadingDOM(false)
  3. 3. setUserLoadingDOM(true)
  4. 4. saveDocumentAjax()
  5. 5. setDocLoadingDOM(false)
  6. 6. setDocLoadingDOM(true)

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.

Image

Different languages, different threading models

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.

Image

Single-threaded, synchronous

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.

Single-threaded, asynchronous

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.

Multi-threaded

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.

Message-passing processes

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.

Building the timeline step-by-step

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() {

2setUserLoadingDOM(false);

3 });

4 setUserLoadingDOM(true);

5 saveDocumentAjax(document, function() {

6setDocLoadingDOM(false);

7 });

8 setDocLoadingDOM(true);

Actions

  1. 1. saveUserAjax()
  2. 2. setUserLoadingDOM(false)
  3. 3. setUserLoadingDOM(true)
  4. 4. saveDocumentAjax()
  5. 5. setDocLoadingDOM(false)
  6. 6. setDocLoadingDOM(true)

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:

Image

Three steps to diagramming

  1. Identify actions.
  2. Draw each action.
  3. Simplify.

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.

Image

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:

Image

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() {

2setUserLoadingDOM(false);

3 });

4 setUserLoadingDOM(true);

5 saveDocumentAjax(document, function() {

6setDocLoadingDOM(false);

7 });

8 setDocLoadingDOM(true);

Actions

  1. 1. saveUserAjax()
  2. 2. setUserLoadingDOM(false)
  3. 3. setUserLoadingDOM(true)
  4. 4. saveDocumentAjax()
  5. 5. setDocLoadingDOM(false)
  6. 6. setDocLoadingDOM(true)
Image

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:

Image

Three steps to diagramming

  1. Identify actions.
  2. Draw each action.
  3. Simplify.

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:

Image

Line 8 has the last action. It’s in the original timeline:

Image

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.

Drawing the add-to-cart timeline: 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

  1. Identify actions.
  2. Draw each action.
  3. Simplify.

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

  1. 1. Read cart.
  2. 2. Write cart.
  3. 3. Write total = 0.
  4. 4. Read cart.
  5. 5. Call cost_ajax().
  6. 6. Read total.
  7. 7. Write total.
  8. 8. Read cart.
  9. 9. Call shipping_ajax().
  10. 10. Read total.
  11. 11. Write total.
  12. 12. Read total.
  13. 13. Call update_total_dom().

2. Draw each action, whether sequential or parallel

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.

Image

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.

Timeline diagrams capture the two kinds of sequential code

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.

Code that can be interleaved

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.

Image

Code that cannot be interleaved

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.

Image

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.

Image Vocab time

Actions on different timelines may interleave if they can occur between each other. This happens when multiple threads run at the same time.

Image

Timeline diagrams capture the uncertain ordering of parallel code

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.

Image

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:

Image

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.

Image Vocab time

Multiple timelines can run in different ways, depending on timing. The ways that timelines can run are known as possible orderings. A single timeline has one possible ordering.

Principles of working with timelines

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.

1. Fewer timelines are easier

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

Image

2. Shorter timelines are easier

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.

3. Sharing fewer resources is easier

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.

4. Coordinate when resources are shared

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.

5. Manipulate time as a first-class concept

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 single-thread

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;

}

  • JavaScript has one thread.
  • Synchronous actions, like modifying a global variable, cannot be interleaved between timelines.
  • Asynchronous calls are run by the runtime at an unknown time in the future.
  • No two synchronous actions can run simultaneously.

In Java, if I have a variable shared between two threads, doing the += operation is actually three steps:

  1. Read the current value.
  2. Add a number to it.
  3. Store the new value back.

+ 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.

Image

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.

JavaScript’s asynchronous queue

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.

Image

What is a job?

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.

What puts jobs on the queue?

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.

What does the engine do while there are no jobs?

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 and the event queue

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.

Image

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.

If it doesn’t wait for the request, how do you get the response?

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.

  • AJAX stands for Asynchronous JavaScript And XML.
  • AJAX is how we make web requests from JavaScript in the browser.
  • Responses are handled asynchronously by callbacks.
  • Responses come back out of order.

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.

A complete asynchronous example

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.

Image

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.

Image

Simplifying the timeline

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() {

2setUserLoadingDOM(false);

3 });

4 setUserLoadingDOM(true);

5 saveDocumentAjax(document, function() {

6setDocLoadingDOM(false);

7 });

8 setDocLoadingDOM(true);

Actions

  1. 1. saveUserAjax()
  2. 2. setUserLoadingDOM(false)
  3. 3. setUserLoadingDOM(true)
  4. 4. saveDocumentAjax()
  5. 5. setDocLoadingDOM(false)
  6. 6. setDocLoadingDOM(true)

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:

  1. Consolidate all actions on a single timeline.
  2. Consolidate timelines that end by creating one new timeline.

Three steps to diagramming

  1. Identify actions.
  2. Draw each action.
  3. Simplify.

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:

Image

Three steps to diagramming

  1. Identify actions.
  2. Draw each action.
  3. Simplify.

In JavaScript, we have two simplifying steps we can perform thanks to the single-threaded runtime:

  1. Consolidate all actions on a single timeline.
  2. Consolidate timelines that end by creating one new timeline.

Let’s go through these two now.

Two JavaScript simplifications

  1. Consolidate actions.
  2. Consolidate timelines.

1. Consolidate all actions on a single timeline

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.

Image

We can see how the JavaScript runtime simplifies the execution of code by eliminating lots of possible orderings.

2. Consolidate timelines that end by creating one new timeline

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!

Image It’s your turn

Here’s the code and diagram we used in a previous exercise. Since this is running in JavaScript, we have two simplification steps to do. Perform the first step where you consolidate actions. Assume cook(), serve(), and eat() are synchronous actions.

function dinner(food) {

cook(food);

serve(food);

eat(food);

}

Two JavaScript simplifications

  1. Consolidate actions.**

    ** just do this step

  2. Consolidate timelines.

button.addEventListener('click', dinner);

Image

Image Answer

Image

Image It’s your turn

Here’s the code and diagram we used in the previous exercise. Since this is running in JavaScript, we have two simplification steps to do. Perform the second step where you consolidate timelines. Assume cook(), serve(), and eat() are synchronous actions.

function dinner(food) {

cook(food);

serve(food);

eat(food);

}

Two JavaScript simplifications

  1. Consolidate actions.
  2. Consolidate timelines.**

    ** just do this step

button.addEventListener('click', dinner);

Image

Image Answer

Image

Reading our finished timeline

Before we move on, let’s look at what this timeline we just finished is telling us:

Image

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:

Image

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:

Image

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.

Image It’s your turn

Here is a timeline diagram with three actions in JavaScript. List the possible orderings that this diagram indicates. You can draw them if you want to.

Image

Image

Image

Image Answer

3. A B C

4. B A C

5. B C A

Simplifying the add-to-cart timeline diagram: Step 3

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:

Image

Because we’re still in JavaScript in the browser, we’re going to use the same two simplification steps we just used.

  1. Consolidate all actions on a single timeline.
  2. Consolidate timelines that end by creating one new timeline.

We have to perform these steps in order or it won’t work right.

Two JavaScript simplifications

  1. Consolidate actions.
  2. Consolidate timelines.

1. Consolidate all actions on a single timeline

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:

Image

Here’s where we were after consolidating all actions on a timeline into a single box on the last page:

Image

Two JavaScript simplifications

  1. Consolidate actions.
  2. Consolidate timelines.

Now we can do the second simplification.

2. Consolidate timelines that end by creating one new timeline

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:

Image

Four principles for making timelines easier

  1. Fewer timelines**
  2. Shorter timelines**

    ** JavaScript’s threading model reduced our timelines from three to one, and went from 13 steps to 3.

  3. Fewer shared resources
  4. Coordination when sharing resources

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!

Review: Drawing the timeline (steps 1–3)

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

  1. 1. Read cart.
  2. 2. Write cart.
  3. 3. Write total = 0.
  4. 4. Read cart.
  5. 5. Call cost_ajax().
  6. 6. Read total.
  7. 7. Write total.
  8. 8. Read cart.
  9. 9. Call shipping_ajax().
  10. 10. Read total.
  11. 11. Write total.
  12. 12. Read total.
  13. 13. Call update_total_dom().

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

  1. Identify actions.
  2. Draw each action.
  3. Simplify.

Four principles for making timelines easier

  1. Fewer timelines
  2. Shorter timelines
  3. Fewer shared resources
  4. Coordination when sharing resources
Image

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

  1. Identify actions.
  2. Draw each action.
  3. Simplify.
Image

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

  1. Consolidate actions.
  2. Consolidate timelines.
Image

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

  1. Fewer timelines
  2. Shorter timelines
  3. Fewer shared resources
  4. Coordination when sharing resources

Summary: Drawing timeline diagrams

Here is the skill of drawing timeline diagrams in a nutshell.

Identify actions

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 +=.

Draw actions

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:

  • Asynchronous callbacks
  • Multiple threads
  • Multiple processes
  • Multiple machines

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.

Simplify the timeline

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:

  • If two actions cannot be interleaved, combine them into a single box.
  • If one timeline ends and starts another, consolidate them into a single timeline.
  • Add dotted lines when order is constrained.

Reading timelines

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.

Timeline diagrams side-by-side can reveal problems

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:

Image

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:

Image

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.

Two slow clicks get the right result

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.

Image

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.

Two fast clicks can get the wrong result

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:

Image

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.

Timelines that share resources can cause problems

We can remove problems by not sharing resources

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

Image

Actions sharing the cart global variable have a

Image

Actions sharing the DOM have a

Image

Then, for clarity, we can annotate the timeline steps with information about which steps use which global variables:

Image

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.

Converting a global variable to a local one

The global variable total does not need to be shared

There is no reason to use a global variable for the total. The easiest improvement is to use a local variable instead.

1. Identify the global variable we would like to make local

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

Image

2. Replace the global variable with a local variable

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

Image

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.

Converting a global variable to an argument

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.

1. Identify the implicit input

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

Image

we still have one step that uses the cart global

2. Replace the implicit input with an argument

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

Image

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.

Image

Image Brain break

There’s more to go, but let’s take a break for questions

Q: We just eliminated all of the global variables from calc_cart_total(). Doesn’t that mean it’s a calculation?

A: Good question. The answer is no. calc_cart_total() still performs several actions. For one, it contacts the server twice. Those are definitely actions. Finally, it updates the DOM, which is an action.

However, by removing the reads and writes to global variables, it is certainly more like a calculation. And that’s a good thing. Calculations don’t depend at all on when they are run. But this function depends less on when it is run than it used to. It hasn’t crossed the line, but it is closer to it.

In the next few pages, we are going to move the DOM update from inside this function to outside, moving it even closer to the line and making it more reusable.

Q: We’ve had to talk about the JavaScript threading model, AJAX, and the JavaScript event loop quite a lot. Are you sure this isn’t a book about JavaScript?

A: Yes, I’m sure. This is a book about functional programming, for any language. However, just to make sure we all understand what’s happening, I’ve had to explain some of the inner workings of JavaScript.

I had to pick a language, and JavaScript is actually a great language for teaching functional programming for a number of reasons. One of them is that JavaScript is very popular. If I had chosen Java or Python, I would have had to teach some of their inner workings as well.

I’ve worked hard not to make JavaScript a distraction from the functional programming ideas. Try to see past the language and look at the reasoning that is happening.

Image It’s your turn

Mark the following statements as true or false:

  1. Two timelines can share resources.
  2. Timelines that share resources are safer than timelines that don’t share resources.
  3. Two actions on the same timeline should avoid using the same resource.
  4. You don’t need to draw calculations on the timeline.
  5. Two actions on the same timeline can happen in parallel.
  6. JavaScript’s single threaded model means you can ignore timelines.
  7. Two actions in different timelines can occur simultaneously, left first, or right first.
  8. A way to eliminate a shared global variable is to use arguments and local variables.
  9. Timeline diagrams help us understand the possible orderings our software can run in.
  10. Timelines that share resources can cause timing problems.

Image Answer

1. T, 2. F, 3. F, 4. T, 5. F, 6.F, 7. T, 8. T, 9. T, 10. T

Making our code more reusable

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.

Image

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

  1. Identify before, body, and after.
  2. Extract function.**

    ** it’s already in a function, so we don’t need to do this

  3. Extract callback.

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.

Image Principle: In an asynchronous context, we use a final callback instead of a return value as our explicit output

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

  • Return a value the caller can use.
  • Extract actions by returning values passed as the arguments to actions.

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

  • Call a callback with a result at some point in the future.
  • Extract actions by passing the action in as a callback.

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

Image Brain break

There’s more to go, but let’s take a break for questions

Q: Why can’t we return a value from an asynchronous function? I thought all functions could have return values.

A: Well, technically, you can. But you can’t use it in the normal way. Here’s an example:

function get_pets_ajax() {

var pets = 0;

dogs_ajax(function(dogs) {

cats_ajax(function(cats) {

pets = dogs + cats;

});

});

return pets;

}

the callback passed to dogs_ajax() and the callback passed to cats_ajax() won’t run until after the network responses come in. pets won’t be set until then

returns immediately, before ajax requests are finished

What does this function return? It returns whatever is in the pets variable, but that will always be zero. So, yes, it is returning a value, but not the thing you’re trying to calculate here.

What is happening is that get_pets_ajax() is calling dogs_ajax(), which sends the request to the network engine and immediately returns. The next statement is a return statement, so that happens. Sometime later, when the ajax request is completed, a completion event (called load) will be put on the job queue. And sometime after that, the event loop will pull it off the queue and call the callback.

Technically, you can return a value, but it has to be something that you have calculated in synchronous code. Anything done asynchronously can’t use a return because it’s run in another iteration of the event loop. The call stack will be empty at that point. In asynchronous code, result values need to be passed to a callback.

Image

Image It’s your turn

Here is some code for doing the dishes. It uses global variables and writes to the DOM to indicate how many dishes are done. Refactor it to eliminate implicit inputs and outputs. It should use arguments and local variables, and it should call a callback instead of writing to the DOM.

var plates = …;

var forks = …;

var cups  = …;

var total = …;

 

function doDishes() {

total = 0;

wash_ajax(plates, function() {

total += plates.length;

wash_ajax(forks, function() {

total += forks.length;

wash_ajax(cups, function() {

total += cups.length;

update_dishes_dom(total);

});

});

});

}

 

doDishes();

Image

Image

Image Answer

var plates = …;

var forks = …;

var cups  = …;

 

function doDishes(plates, forks, cups, callback) {

var total = 0;

wash_ajax(plates, function() {

total += plates.length;

wash_ajax(forks, function() {

total += forks.length;

wash_ajax(cups, function() {

total += cups.length;

callback(total);

});

});

});

}

 

doDishes(plates, forks, cups, update_dishes_dom);

Conclusion

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.

Summary

  • Timelines are sequences of actions that can run simultaneously. They capture what code runs in sequence and what runs in parallel.
  • Modern software often runs with multiple timelines. Each computer, thread, process, or asynchronous callback adds a timeline.
  • Because actions on timelines can interleave in ways we can’t control, multiple timelines result in many different possible orderings. The more orderings, the harder it is to understand whether your code will always lead to the right result.
  • Timeline diagrams can show how our code runs sequentially and in parallel. We use the timeline diagrams to understand where they can interfere with each other.
  • It’s important to understand your language’s and platform’s threading model. For distributed systems, understanding how things run sequentially and in parallel in your system is key.
  • Shared resources are a source of bugs. By identifying and removing resources, we make our code work better.
  • Timelines that don’t share resources can be understood and executed in isolation. That wipes out a mental burden you no longer have to carry.

Up next…

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.62.45