Why Can't we Build Simple Software?
Jan 01, 2023
https://vimeo.com/780013486 by Peter van Hardenberg
via this toot https://hachyderm.io/@allafarce/109605444838150996
Transcript via whisper
You are many things, and I can't explain it properly. So you should introduce yourself. But yeah, your microphone's right here. Yeah, press hard for the next slide, backwards, and a pointer if you need it. Great, thanks. Thank you so much. Hi, everyone. I'm Peter. I'm going to kind of take things in a bit more of a philosophical kind of direction. You know, I work for a research lab these days, so that's kind of my tendency. The lab is called Ink and Switch. We are interested in carrying on kind of the work of Engelbart and Licklider and Alan Kay and these people. We're interested in how computers can augment human intelligence. And that's very highfalutin. It's a lot of fun. We get to dig into all kinds of weird corners of software and experimental interfaces and so on. But before that, I have written a lot of production software. So I'm not just speaking to you from kind of an ivory tower perspective. I was one of the early employees at a platform as a service company called Heroku. I've run a lot of Postgres databases. I've worked in game development as well. I've built titles for DS and GBA. I worked on this one, Teenage Zombies, Invasion of the Alien Brain Thingies, for a Victoria, BC game company. And I've worked on desktop software like Songbird, which was a cross-platform media player that lost fairly decisively. And besides that, I've also worked in research, spending some time on, for example, this ship here, the Sir Wilfrid Laurier. It's a Canadian Arctic research icebreaker. So I spent some time in the Arctic. And that actually helped inform my perspective on things too. But we'll come back to that. So I want to start by kind of saying, well, hold on a sec. Before we get into kind of talking about why we can't make simple software, we should probably define our terms a little bit and talk about what complexity actually is. So the first thing I want to clarify is that complexity is not difficulty. Just because something is hard doesn't mean it's complicated. The ideas of calculus might be very difficult to internalize, but they're actually very simple and elegant. And I also want to draw a distinction between something being complex or just being kind of big. A box of Lego is not necessarily a complicated thing, though you can make a lot of things with it. Complexity occurs when your systems interact with each other and when it takes a lot to get things done because of that. And more specifically, I think the problem with complexity is that your systems can become unreasonable. And when I say unreasonable, I don't mean in kind of like a sort of colloquial sense. What I mean is that you literally cannot reason about them. You don't have the ability to predict what's going to happen. And a lot of the time, that's a problem because you've got work to do and the thing is crashing and you don't know why. But a complex system can also be generative and surprising if you harness that complexity in positive ways. Last, before we get into it, I want to make just a very small detail point for the more pedantic in the audience. Some people draw a very specific difference between complex systems from sort of chaos math and emergent complexity. We'll talk a little bit about that. As distinguished from complicated systems where you have a lot of just kind of mess, I'm kind of going to float back and forth across both of these without a lot of distinction because I think in this context, either one can cause similar effects. OK. So we're going to begin with a non-comprehensive look at complexity. And more specifically, we're going to kind of cherry pick some examples from fake industrial scenarios where you can begin with something simple and end up with something complicated. So I'm going to start with the most basic of all reasons why your software gets complicated. And that's the first version of your code. You just write the happy path. Everything's glorious. And software can be really simple when it lives in an idealized world and everything is fine. And so here's like a really simple imaginary pseudocode web thing. Great. We've got a GET request to our application. We're going to go fetch something from the database and say, hello there. This is a Hello World app of web apps, server apps. Great. And it works fine if user 12 is me, and user 10 is my friend Keiko, and user foo is wait, what? Right. We've got to validate the input. OK, so we'll validate the request. Now, OK, so we're validating the request, and that's good. And oh, jeez, I'm not really handling it if we don't validate the request. And wait, do I have to do this on every request? I guess I have to do this on every request. And am I doing it the same way on every request? In fact, this exact pattern of validating user input and checking your arguments can lead to something that Bradis and Patterson call shotgun parsing. And shotgun parsing is kind of the idea that you're parsing your input, but it's like someone's taken a shotgun to your code base because it's just blown through the whole code base. And it can lead to very serious security vulnerabilities because you might carefully check an input in one place, but not in another. And there's a great talk from Brucon 2012 about this. So of course, now we're going to be a little more careful. So we're going to also, if the database is down, we don't want to send a 500. We're going to fail elegantly. And as you go, the more of these cases that you have to handle and the better the quality of your code is, the more conservative, defensive, thorough that you are. At this point now, the actual logic of this method is beginning to disappear into all the edge case handling. We're well off the happy path now. And so you can clean this up a little bit. So imagine here in this case that we have a slightly better set of libraries, more thoroughly written set of libraries. We've moved the complexity down out of this method. And we're sort of saying, well, the database won't throw an exception, because exceptions are ugly and kind of subtle to handle. Well, it'll return null if it can't do the job for whatever reason. And we're going to say, well, there's like a good type system that's handling parsing the input args before they get to us, so we don't have to worry about that anymore. And the value of well-written libraries and languages, type systems, and so on is that they can really simplify your code, make it less complex by preventing you from having to carry around all of this mental state everywhere that you go. And my observation of this world is that vigilance is not a strategy. We talked earlier about how if a system fails, it's not, oh, blameless postmortems. It's not that the developer failed. It's that the system failed. If you have security vulnerabilities in your code because you didn't handle input, the answer isn't to berate yourself for not handling the input. It's to step back to level up and say, well, hold on. Why do I have to do this manually every time? So strategically, you want to minimize the scope of failures and kind of design them out of your system. And one way to do that is to have a type system so you don't forget to check your nulls. There are other approaches. I don't think this is universally true, but it's one way. OK, so let's talk about scale. We all know as an array gets bigger, you can have an O of n log n kind of sort algorithm. Great, OK, well, scale's easy, right? So let's just imagine an admin panel for a web application. You list your users. It looks kind of like this. Go to your database, select all the users, and then in your front end, just show them all. Perfect, easy, it's fine. You've only got 10 users, who cares? OK, well, now we're at 10 to the 4. OK, you've got 1,000 users or so. OK, well, OK, we can't just render all that on one screen. OK, well, we're going to use one of those offset SQL things, right? We know about this. And then we're going to need pagination, I guess. And you've got to click through. OK, yeah, sure, we can still do this. This is fine. You don't want to request all that. It's a pain to scroll through. OK, well, now we're 10 to the 6 users. We've got a million. Actually, that offset thing, that's a problem if you have a lot of users, because in order to generate page 850, it actually selects all of those rows and scrolls through them and just takes all of those results and just chucks them on the floor until it gets to the ones that you asked about. And actually, now, even though nothing has changed about our problem statement, not only are the performance characteristics of the system starting to change and the user interface is starting to change, but the way that we interact with the problem is changing, too. Because if you've got a million users, the way to find a user is not to just keep clicking Next until you find them one page after another. Oh, page 350, OK, I'm on the Js. OK, click forward to change the query arg to force it. We've all done these kinds of things when the tools didn't keep up with our admin systems. But the point I'm trying to make here is that once you start to reach 100 million users, it's not even the same kind of problem anymore. The complexity that snuck in here is now that we have different responsibilities. There are ethical, there are legal, there are policy responsibilities. You probably have a team of people at your organization who are responsible for dealing with abuse. Some of these people are terrible, and you want them off your platform, and you can't find them, right? Like, it's a different thing. And it's complicated because the environment has changed as much as anything else. So everything changes when your scale changes. It's not just like an algorithmic thing. It's also the kinds of things that you're doing change. And in general, my advice to someone who's built a lot of high-scale production systems through many orders of magnitude is that it's as harmful to build for the system of the future as it is to build an insufficient system for the present. If you build the tools for 100 million users when you have 100 users, they're going to be completely unusable, too. And you're going to waste so much time trying to solve problems you don't have yet that you're going to miss out on all the benefits of being able to just look at all your users and see who they are and talk to them, right? So you need to be scale-appropriate and kind of be looking ahead. Another way software can get complicated is with leaky abstractions. And so here we have a platform that's a little bit skewed. And so complexity can kind of bubble up from under the waterline through these imperfect abstractions. This is how you copy a string. It's a nice handmade, low-level. This is an excerpt from Kernighan and Ritchie, 1988, second edition, C programming language. And this book was such a revelation for me, like how it showed me the way kernel functions were really implemented. No, I'm just kidding. They're not really implemented like this. They say here that this is how a C programmer would prefer to write string copy. The C programmer in question has never forgotten to null terminate a string, I see. But this is how string copy is actually implemented as of 2022 for alpha in Linux. And actually, this isn't how string copy is implemented. This is like one page of assembler. There's like 300 lines of assembly. And really importantly and intriguingly, although the interface remains the same, modern CPU architectures have so many more constraints around memory alignment and everything else that although the interface is preserved, you can pass any pointers in and get values back, the kind of performance characteristics of doing an unaligned versus an aligned string copy will be dramatically different. And so complexity here, I think we can kind of call this a win. We've got this super powered computer under the hood now, but it still kind of feels like that old jalopy they were running in 1988. And that's cool that it works. But in a sense, this API that's so simple is actually undermining the value of the system. And there's a lot of magic happening to kind of hide this. And so I feel very mixed about this, because on the one hand, it's sort of like SQL databases where you can add an index and change nothing else and things get faster. But there's a really important, subtle lie that's being told here. But maybe this is good and maybe it's bad. This is like a value judgment kind of question. And whether you understand this is happening could be completely irrelevant, or it could be the difference between the success and failure of a project. OK, so these are kind of like technical complexities that come in. I want to talk about other sources of complexity, different kinds of complexity. And one really big source of complexity in my projects, maybe you're smarter than me and that's cool, but it's when you have a gap between the problem you think you have or the problem you used to have and the problem you have right now. And again, we're going to use a toy example to illustrate this. So you've got a users table, just the same app as before. And your users is a bunch of fields. You've got a first name and a last name. And how do you get the first name and the last name? It's easy. Guys, you just split on a space. As a Van Hardenberg, you can see how this might get you in trouble. And in fact, on my California driver's license, it said that my middle name was Van for many years. And the problem here is that the model that you have for the problem doesn't actually map the problem domain successfully. So what do you do? And there are lots of things you can do. You can rewrite from scratch. You can patch around it. And actually, just as an aside for everybody in the room, the W3C has a wonderful essay about personal names around the world. And if you take one little thing from this talk, don't use first name and last name. Don't use family name. People's family names aren't accurate. Some people have them, and others don't. What you do is you have one field that is, what is the full name? And then the other one is, if you need it, what should we call you? It's a much better model. Also, Unicode. I know that's its own set of complexity, but. And so there's only so many things you can do when you have this problem. And I had a really specific, concrete example of this in a distributed system recently, where I had a mental model of how the system behaved. And my tests demonstrated that it behaved that way. And then in my production environment, it behaved completely differently because of behaviors that were not modeled by my understanding of the problem. And this is part of why distributed systems are so prone to this, is because there's so many new free variables and articulation points where things can be fast or slow, or where they can fail or arrive out of order, and all these kinds of things. And so when you have these kinds of model reality gaps, you have to bridge them somehow. And so what can you do? Well, you can fix the problem. You can improve your understanding and rewrite everything. And when you can do that, it's probably best. You can't really always do that, though. And more to the point, you could see the problem, but you may still not actually understand the solution. And so what can you do? Well, you can hack around it. I've put Van Hardenburg into a lot of text boxes without a space in it, where the first letter is capitalized and the H is lowercase. And that's not how I spell my name, but it is how a lot of systems spell my name. Or you can ignore the problem. Maybe it's fine. And genuinely, it could be fine for your use case. You're just like, well, I guess that Peter guy can suck it. He'll deal with having his name spelled wrong. And I do. These are options that you have. And there's a great meme series out there on the internet, lies programmers believe about pretty much anything. All time zones are one hour apart, half an hour later in Newfoundland. And so I think this is a very fundamental source of complexity. But where things get really bad is when your problems start to multiply off each other. And this, of course, is where you have compound interest on complexity. So what happens if your problems dimensionalize against each other? So again, imagine this web application. You've got a bunch of different browsers to support. You've got a bunch of different runtime environments to support, different screen sizes, network speeds, different OS or browser versions. And so if you want to actually understand what's happening, all of these things multiply against each other. And so you don't just have one runtime environment. You might have one code base. But you have lots of different contexts. So how can you know that you actually have correct functioning code? Because the old joke about Docker, like, well, it worked on my computer. Well, I guess we'll ship your computer. Well, even that doesn't really work. Because once you get out into the real world, there's different memory contexts, or you're up against different loads, or whatever else. You don't actually control the whole environment. And so this is what really starts to kill you with complexity. And it's where all of those smaller inconsistencies play off each other to create an unknowably complicated environment. And this is why you see so much popularity. And Ember Jen were kind of hinting at this, like, you don't use the native APIs, because God help you if you're trying to figure out how to map all of these totally different environments down to one consistent thing. And one solution here is just to only have one thing as best you can and to minimize the difference between those environments. And that's why you see ostensibly lazy Electron apps from big companies. Because even worse than having to support all these different things is having to coordinate all the different people and teams to try and build features. And many of us have been there, right? Like, if you have an iOS and an Android team, like, how do you get them to ship the same feature in the same quarter? It's tough. It's tough. OK, so I hope I've convinced you now that complexity is a complex problem itself and that it manifests in a lot of different ways. So we're going to change gears a little bit and talk about seatbelts. And more specifically, why seatbelts don't save lives. And I have an asterisk here, because this is a little bit of a controversial research topic. But the sort of upshot of this is that in at least some studies, they found that after seatbelts became mandatory in cars, people still died on the roads at similar rates. And that was really surprising, because seatbelts are a good idea. Like, they keep you safer in the car. And so somebody proposed this idea called risk homeostasis. The idea behind risk homeostasis is that actually we die at a rate on the road related to how much risk we're willing to take. And so if you now have a seatbelt and you're driving a car that's safer, you're going to go a little faster and you're going to take the corners a little tighter, because you feel better. So the issue with making people safer was that they then took on more risky behavior, conserving their total risk tolerance. And so I think we see a similar thing in software. I call this complexity homeostasis, which is to say that if you have kind of a system that's evolving over time, everything's fine, we're going along, life is good, and then we add some more things, and we're still happy. And then, oh, yeah, now I don't like it anymore. That's not good. It feels bad now. It's time for that rewrite that we were hearing about. It's time to bring things back. OK, now we can go back to making things complicated again. And so this is kind of like a set point. So homeostasis is the process where our, or it's any process where you sort of maintain a set point. And it's commonly used to talk also about how our bodies regulate temperature. But I think our organizations regulate complexity. And so all of us have different intuitions. And when people talk about wanting things to be simple, there's like an aesthetic preference here. And different people perceive that and pursue that differently. And like Devine's quest for just the right number of opcodes that we heard about yesterday is a great example, where in a certain sense, the correct number of opcodes is one, and you could do that if you really wanted to. But then you've got kind of like the CISC model, where actually the correct number is hundreds, because you're maintaining backwards compatibility. And anything that can squeeze you either better numbers out of a benchmark or more CPU sales is acceptable. And so how you perceive what constitutes complexity or how much complexity you're willing to tolerate is an individual or an organizational decision. And some things can actually move that up. Like I have abandoned projects because they got complicated and annoying to work on. But some people might just tolerate that and not perceive it. I've worked with some brilliant people who write the most complicated, insane, convoluted code from my perspective. But when I sort of go to them, I'm like, this is so complicated and weird. Why did you do it this way? They don't see it, because they're just much smarter than me. They're able to hold that complexity in their head comfortably. And so for them, it doesn't feel that way. Another way a system can become more complicated, if it's worth a lot of money. Because you can just hire another poor schmo to sling Java into the code base. I mean, has anybody here worked on an EA Sports title? Yeah, I'm so sorry. I have heard some things, man. And it ships every year on deadline without fail, right? That's not an environment that leads to healthy refactoring or reduction of complexity. But you know what? It makes a boatload of money. And so they'll hire people to work on it, because they can afford to. And you'll work on it, because they'll pay you enough. Because they know. They know. And so part of this, as well, is that also, if you just have more people working on things, you can tolerate more complexity. I can hold part of it in my head, and you can hold part of it in your head. And I kind of want to distinguish, there's sort of the breadth of complexity. And if you have a well-factored system, you can decompose the complexity, either into layers or modules. Like each individual LEGO brick is simple, but you can build very complicated things from it. And so that's sort of the system complexity versus the component complexity. CSS is incredibly complicated. And by that, what I mean, it's complex. You can't tell from looking at one rule what it will manifest as in a final document. So OK, well, we can just solve this problem with better tools, right? Well, no. Because as we've talked about, we have this homeostasis point that we fall towards over time. And we can choose where to set it, but we do move towards it. And the research on this actually goes all the way back to the 1860s. This is Jevin's paradox, which was like, why did coal consumption not decrease when engines became more efficient? And the answer is, people did more work, right? And so I feel very much that this is sort of the inevitable conclusion we have to draw from looking at the problem, which is that the degree of complexity of a system is tied to who we are and what we're doing over time, right? And so when we buy back some complexity by using better tools or by picking a simpler environment, we're going to spend that out again eventually. OK, so let's talk a little bit about some theories of complexity, because I'm from a research lab, and I like to read papers. So this is not a new problem. Have people heard of cyclometric complexity before? Yeah, we're not going to do this. You're welcome. But I do think that although the naive pursuit of software metrics as a way of measuring what we're doing is not wise, I think that the conceptual understanding that when you couple things together that can have a cost and be problematic is worth thinking about. In fact, people have been looking at this and thinking about it since the 1980s. This is Merrill Lyman in 1980s IEEE volume 68. So this is not a new domain. And so even back then, this edition was from 1980. The first paper published was in 1974 on these laws of software engineering. We're not going to read these super close. It's more that I want you to realize that this kind of recognition that systems grow to meet growing needs if they're successful is not a new idea. It's a common problem. And whether you're working in building tools or languages, you might start with an elegant and simple thing. But as a system has more demands on it, it responds by adding. That's a very common, and it's a reasonable consequence. And if you don't do those things, the thing will often starve and die. One great paper, if you haven't read it yet, on this topic is Out of the Tar Pit by Mosley. This is from 2006. And this paper differentiates between what is accidentally complex versus essentially complex. So essential complexity is the irreducible, non-eliminable part of your system. If you are trying to model certain physical processes like smoke or fire, the physics part where you're actually doing the work, you can't get rid of that. You don't want to get rid of that. That's what you're here for, right? But the accidental complexity is all that other stuff, like, oh, god, we've got to compile this so it works on. Windows 11 has changed the ABI for this DLL. All that stuff where we heard about Devine hoisting the phone up the mast to download 11 gigs of something. That's mostly accidental complexity for our purposes. And so it helps as you're working and thinking about complexity to think about where you're spending that budget and whether you're being deliberate in terms of how you're adopting complexity. The other thing I want to refer to is this great essay from the Berkeley DB team in The Architecture of Open Source. And honestly, I highly recommend reading this if you just like software engineering. The Architecture of Open Source Software is a cool series. And basically, what they do is interview open source communities about their software and have them write essays about what they've done, and then they publish and share those. But I think this Berkeley DB chapter in particular is exceptional. And I think about this all the time, which is that software architecture degrades with changes made to software. So you might have the most elegant, brilliant, carefully planned system in the world, but it does not exist at a single point in time in a vacuum. And as new demands come along, this architecture will decay. And so it requires a constant shoring up. And when you have big interfaces that you've invested heavily in and they're straining, it can be extremely expensive to change them. I also just love this, because it's a little more handmade, specific kind of vibe. The Excel team motto, at least according to Joel Spolsky, is find your dependencies and eliminate them. I have been told on reasonably good authority that Excel actually is built with its own C compiler, that they didn't even want to rely on other teams within Microsoft, and to the degree where they've built their own compilers. I'm not saying you necessarily should do that. I'm not saying you necessarily shouldn't. You've got to think about how you're spending your budget. And the last kind of point I want to make on this theme is, again, that complexity isn't necessarily bad. Complexity can lead to all kinds of wonderful emergent properties. The Legend of Zelda, Breath of the Wild, it has had such a wonderful community grow up around it precisely because they managed to tame complexity with their chemistry engine. So there's a lot of emergent gameplay properties and experiences that come out of interactions, complex interactions between systems, but the systems are factored in a way that enables and empowers this. And of course, if you're a roguelike fan, people have been doing this forever there. OK, so now what? We have now looked at complexity. I've told you you can't get rid of it. So we're just going to have to live with it. And better tools won't save us. So how are we going to spend this complexity budget that we have? I mean, one approach is you just put your head down and you work away and pretend it's not a problem. And that's a pretty common solution to all of our problems in life, so we could do that. But I'm going to maybe try and propose a few ways we can cut the Gordian Knot here. And so the story of the Gordian Knot was that there was this ox cart with a really complicated knot on the handle. And people were like, oh, whoever unties this knot will rule all of Asia. And many people had come. And then Alexander the Great came along. And depending on the version of the story you hear, he's like, OK, cool. And he cut it with a knife. And so instead of solving the problem by trying to be really smart or work really hard, can we just cheat and change the rules? I think that's a better approach. So one approach is to do what folks here like to do, just start over. You can't actually get rid of this problem, but you can reset the clock on it. Just build a new one. Start from the beginning. Everything's easy in the beginning. It's only when you have users and features that you have complexity. So make a new programming language. Start a new VM. It's good. You can't really change this long-run pressure. But genuinely, you can reset the clock. And I think that's part of why things like Excel are popular and successful is they don't have package managers. Every time you make a new Excel document, it's a brand new universe with none of the misery or mistakes that you made before. It's like the forgiveness of the blank page. And there's something to that. I went to a talk by John Romero at Strangeloop not that long ago. And he talked about how id Software had been making these shovelware games as contractors, or the id Software team, him and Romero and Carmack, had been making these sort of shovelware games. And their attitude was like, well, you should just always start from scratch. You get really good at it. You can be really fast. And each time, you do a little bit better than the one before. And we do, I think, do better. I love learning new languages and building new ecosystems. And I think a big part of the reason why they're successful is because when you have a new language, you do have the opportunity to clear away the standard library and also just the ecosystem of ideas and people and sort of start from a fresh starting point. Now, this won't cure complexity in the long run. But it can get better, at least in the midterm. And we can get further. So I think that's pretty cool. And it's also pretty fun. So I'm all for that. Please do more of that. And to some extent, it feels like we live in these unbreakable regimes where Unity rules or where the big companies, Google and Microsoft and everybody rules. But that's the kind of thing that's true until it isn't. And there's this lovely quote from Ursula Le Guin about capitalism, which is sort of the generalization of these problems. Capitalism was itself an invention. And I'll just read it. We live in capitalism. Its power seems inescapable. So did the divine right of kings. And any human power can be resisted and changed by human beings. So we should not doubt that the environment can be changed. The environment was created. It will be changed. We don't know how or when, but it is inevitable. OK. So you can start over. Revolution. Burn it down. Or just do less. Do less with less. Do I have my play date? Do people have these? Has anybody got a play date? Yeah, this is great. It's black and white. There's only one platform. It's small. It comes in one color. It only has one color, black or white, depending on how you think about it. Doesn't have a lot of buttons. Doesn't have much RAM. Doesn't really have any net. There's a little bit of networking. And there's only one hardware platform, so you don't have to worry about compatibility problems. Ish. There is the emulator, which behaves differently. And due to sourcing problems, they had to get a different microcontroller. But it transpiles natively. They're doing a really good job. Anyway, it's a heck of a lot simpler than building for PS5, Xbox, PC, Switch, 3DS, all on one code base. It is genuinely awesome. And because it's so small, it's kind of appealing. And when you have less scope, you can choose to spend that energy, that complexity budget. You can put it into polish. And on a small platform, a small idea can shine. So OK, well, we can't solve complexity, but we can make things worse. This is actually from Augmenting Human Intellect by Engelbart in 1962. And it's demonstrating that you can, in fact, change people's experience with design interventions. I said change, not improve. So yeah, we've made things pretty bad in terms of software development. This is my favorite slide always to show. This is the foundation cloud native landscape. I always get this wrong. This is ostensibly, I think, $20 trillion worth of companies. Anyway, please memorize all of this. There'll be a quiz at the end if you want to make a web application in 2022. I had to zoom out the browser just to fit it all on screen for this one. So I'm going to talk a little bit now about our research, which kind of ties back into this because I think it might be interesting. Our approach is less the kind of reboot model and more of the maybe do a little less with less model. And one of our sort of research interests kind of builds on these sort of tools for thought. A big part of what we're interested in as a research group is not simplicity versus complexity or decentralization or anything like that. But we do find that we have to work in that space because what we want are tools that we have agency over, that we have ownership of, that are ours and can't be taken. And so we call our research there local first software. And the idea is basically that instead of having to go learn that whole chart of technologies, that you can't actually do any of that stuff if you just run it on your computer because that stuff's all in the cloud. So if you want to build software that works on your computer, not only do you not get to use all that stuff, you don't have to use all that stuff. And so it's sort of like simplification like via amputation. So we just cut off most of the cloud, and then we build things locally. And so we've dabbled in a bunch of different platforms over the years. Right now, we're building things in the browser, but storing everything in IndexedDB. But one of our kind of core beliefs is that collaboration is such an important part of thinking and making and doing that it should be like a really fundamental part of our platforms. And so we've been exploring technology that allows you to build software that runs on your computer, but then collaborates with other people, even allowing online offline kind of cuts. And the model underpinning that is a data structure called AutoMerge. It's sort of like a portable version, JSON-like data structure. You could think of it as like Git for your data. I'm happy to talk at length about this stuff, but I don't really want to harangue you too much on our particular kind of development interests. But it's really incredible just how it feels to build software that's fast and simple and runs on your computer if you, like me, have spent the last chunk of your career building cloud services and running things in the sky. I had this really memorable outage where Amazon turned off US East 1 dirty because the generators didn't come on in a hurricane. And we were sort of like three days into some godforsaken system rebuild from backups of everybody's databases. And my coworker turns to me, and he goes, I'm fixing computers that don't exist in a data center I've never been to for people I've never met. And he just had the, like, 1,000-yard stare in his eyes. And I was like, yeah, do you need some more coffee? Yeah. And so in a sense, working on this stuff is almost like penance for me, because I created all these single points of failure. Anyway, so let's recap a little bit of all of this. How do we live our lives in this complicated world, right? So complexity occurs when our systems have internal interactions, right? Complexity doesn't mean, oh, there's a lot of stuff. It's when all that stuff starts to bump against each other and cause unpredictable outcomes. And complexity is also a natural consequence of system incentives. If you have a lot of people using a thing, and you're listening to what they need, and you're evolving as you learn more, you're going to end up with something complex. And better tools won't change this, right? The complexity is a consequence of who we are and the choices we make. Sure, you can burn through your budget faster. But ultimately, where your project ends up on the complexity scale is more about how much time and how many ideas are invested into it than anything else. So we can't beat complexity, but we can get beaten by it, right? And so what are our coping strategies? Well, we can start over. We can eradicate dependencies. We can cut scope, do less, right? We can simplify our architecture. And a really big one is being conscious of and learning to kind of identify when you're getting into these multiplicatory environments, right? Once you start porting things to multiple platforms and having to build those per-platform abstractions, how do you manage that complexity back down? How do you isolate complexity? And a big part of success is isolating complexity. But I also want to give a shout out to gazing into the abyss. And you can go for it, right? Embrace complexity. Harness it. Yeah, it's a deal with an elder god, and you may accomplish great and terrible things, but at a great and terrible price. Yeah, that's fine. But when you do this, you've got to be real careful, right? And you want to be really deliberate about how and when you take on that complexity. So I guess in closing, we can't solve complexity, but we can build better software. Or to put it in the words of cyclist Greg LeMond, it never gets easier. You just go faster. And that's all. Thank you.