skip to content
Relatively General .NET

The latency of making a coffee cup

by Oren Eini

posted on: September 13, 2021

I recently had to discuss the issue on the impact of latency a few times, and I found the coffee cup analogy to be an excellent tool to explain exactly what is going on. Consider the humble coffee cup, without which there would be no code.It is a pretty simple drink, composed of coffee, water and milk. I’ll ignore coffee snobs and the like for now and focus strictly on the process of making a cup of coffee. I found this recipe:1 cup milk½ cup cold brewed coffee2 sweetener Mix milk, coffee, and sweetener together in a glass until sweetener is dissolved.If I was writing this in code, I would probably write something like this: This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters def make_coffee(self, cup): cup.put(self.coffee(0.5)) cup.put(self.milk(1)) cup.put(self.sweetener(2)) cup.mix() return cup view raw coffee.py hosted with ❤ by GitHub Simple enough, right? There is just a little bit of details to fill. How are the coffee() or sweetner() methods implemented? This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters def coffee(self, amount): if self._stored_coffee < amount: self.go_to_store(COFFEE_AMOUNT_TO_BUY) self._stored_coffee += COFFEE_AMOUNT_TO_BUY self._stored_coffee -= amount return Coffee(amount) def sweetner(self, amount): if self._stored_sweetner < amount: self.go_to_store(SWEETENER_AMOUNT_TO_BUY) self._stored_sweetener += SWEETENER_AMOUNT_TO_BUY self._stored_sweetener -= amount return Sweetner(amount) view raw coffee2.py hosted with ❤ by GitHub The nice thing about this code is that this is nicely abstracted, the coffee recipe and the code reads almost in the same manner. However, there is an issue with the actual implementation. We have the go_to_store() method, but we know that this is an expensive operation. To avoid making it too often, we calculate the amounts that we need to make 20 cups of coffee and make sure that we set the relevant XYZ_AMOUNT_TO_BUY appropriately. What do you think will happen on the 21th cup of coffee, however? We run out of coffee, so we’ll go to the store to get some. Once we got it, we can pour the coffee to the cup, but then we need to put the milk in, in which case we’ll discover that we run out. Off to the store we go, and all the way back. And then there is the sweetener that run out, so that is the third trip to the store.Abstraction, in this case, is actively hurting us. We ignore the fact that ingredients may be missing, and that isn’t something that we can afford to. The cost of going to the store outweigh anything else in the process of making a cup of coffee, and we just did that three times. In the context of software, of course, we are talking about the issue of making a remote call. For example, sending a separate query to the database for each datum that you need. The cost of the remote call far exceed any other costs you have in the system.To solve the coffee cup problem, you’ll need to do something like: This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters def make_coffee(self, cup): stuff_to_buy = [] if self._stored_coffee < 0.5: stuff_to_buy.append(COFFEE_AMOUNT_TO_BUY) if self._stored_milk < 1: stuff_to_buy.append(MILK_AMOUNT_TO_BUY) if self._stored_sweetner < 2: stuff_to_buy.append(SWEETENER_AMOUNT_TO_BUY) if len(stuff_to_buy) > 0: self.go_to_store(stuff_to_buy) cup.put(self.coffee(0.5)) cup.put(self.milk(1)) cup.put(self.sweetener(2)) cup.mix() return cup view raw coffee3.py hosted with ❤ by GitHub Abstraction? What abstraction? There are no abstractions here. We are very clearly focused on the things that need to happen to get it working properly. In fact, a better alternative would be to not check that we have enough for the current cup but to schedule a purchase when we notice that we are low. That, again, intermix the responsibilities of making the coffee and making sure that we have the ingredients at hand. That is not an actual problem, however. That is something that we are fine with, given the difference in performance that this entails. In the same manner, when I see people trying to hide (RPC, database calls, etc) behind an abstraction layer, I know that it will almost always end in tears. Because if you have what looks like a cheap function call go to the store for you, the end result is that you have to wait a lot of time for your coffee. Maybe enough to (gasp) not even have coffee.On that note, I have a cup of coffee to finish…

Investigating an infinite loop in Release configuration

by Gérald Barré

posted on: September 13, 2021

This post is part of the series 'Crash investigations and code reviews'. Be sure to check out the rest of the blog posts of the series!Investigating a performance issue with a regexInvestigating an infinite loop in Release configuration (this post)Investigating a crash in Enumerable.LastOrDefault w

Creating Pivot indexes in RavenDB

by Oren Eini

posted on: September 09, 2021

I got an interesting question by email and I thought that this is worth a post. The question was whatever RavenDB can handle Pivot tasks. Consider the case where I have orders data, and I want to see a summary product sales on a monthly basis, like so:This data was produced using the sample data in RavenDB and the following map/reduce index: This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters // map from order in docs.Orders from l in order.Lines select new { order.OrderedAt.Year, order.OrderedAt.Month, l.Product, Count = l.Quantity, Total = l.PricePerUnit * l.Quantity * (1-l.Discount) } // reduce from result in results group result by new { result.Product , result.Year, result.Month } into g select new { g.Key.Product, g.Key.Year, g.Key.Month, Count = g.Sum(x => x.Count), Total = g.Sum(x => x.Total) } view raw i1.cs hosted with ❤ by GitHub That works, but it gives each individual month on its own row. When using Excel, we can Pivot the whole thing so instead of rows, we’ll get columns. For certain types of data, that makes it much easier to work with. For example, let’s say that I want to compare monthly sales data across different products. The data we see is the same, it is just the way we process and show it that is different. Let’s see how we can do that in RavenDB. We can do that with a secondary aggregation step in the reduce, like so: This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters //map from order in docs.Orders from l in order.Lines select new { order.OrderedAt.Year, l.Product, MonthlySales = new []{ new { order.OrderedAt.Month, Count = l.Quantity, Total = l.PricePerUnit * l.Quantity * (1-l.Discount) } } } // reduce from result in results group result by new { result.Product , result.Year } into g select new { g.Key.Product, g.Key.Year, MonthlySales = g.SelectMany(x=>x.MonthlySales) .GroupBy(x=>x.Month) .Select(g2 => new { Month = g2.Key, Count = g2.Sum(x => x.Count), Total = g2.Sum(x => x.Total) }) } view raw i2.cs hosted with ❤ by GitHub The idea is that the reduce step in RavenDB can have its own complex processing, and the result of this process gives us the following output: This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters { "Product": "products/1-A", "Year": 1996, "MonthlySales": [ { "Month": 8, "Count": 63, "Total": 756 }, { "Month": 9, "Count": 20, "Total": 280 }, { "Month": 11, "Count": 27, "Total": 346.5 }, { "Month": 12, "Count": 15, "Total": 178.5 } ], "@metadata": { "@change-vector": null, "@index-score": 1 } } view raw pivot1.json hosted with ❤ by GitHub If we use JavaScript indexes, we can even manipulate the data to skip the nested values, the code is nastier (likely a product of my skill in JavaScript, I’ll freely admit), but the results are nice.

ASP.NET 6.0 Minimal APIs - Why should you care?

by Ben Foster

posted on: September 09, 2021

The response to ASP.NET 6.0 Minimal APIs has been fairly mixed. In this post I attempt to address some of the questions and concerns raised by the community and explain why Minimal APIs are an important step forward for ASP.NET.

Refactoring to Value Objects

by Ardalis

posted on: September 07, 2021

Value Objects are a part of Domain-Driven Design, and Julie Lerman and I cover them in our DDD Fundamentals course on Pluralsight. Even if…Keep Reading →

How not to read a string from an UTF-8 stream

by Gérald Barré

posted on: September 06, 2021

This post is part of the series 'Strings in .NET'. Be sure to check out the rest of the blog posts of the series!String comparisons are harder than it seemsHow to correctly count the number of characters of a stringCorrectly converting a character to lower/upper caseHow not to read a string from an

Database and Always-Valid Domain Model

by Vladimir Khorikov

posted on: August 31, 2021

Today, we’ll talk about an important question: how does the application database fit into the concept of Always-Valid Domain Model? In other words, is the database part of the always-valid boundary or should you consider it an external system and validate all data coming from it?