Yesterday I announced Fusion — a library helping to implement real-time UIs much faster, but more importantly, differently from how it’s done today. And Fusion’s documentation — with pages like Overview and Tutorial — is definitely useful for someone who has already decided to learn it, but a really simple explanation of what Fusion does is missing there. So here it is.
As a bonus, you’ll learn a lot about CAD systems and modern-day manufacturing.
What’s common between Fusion and Incremental Builds?
If you’re a developer knowing about incremental builds, this is the only section you need to read.
Fusion is like an incremental build system, but for any value your code computes:
- Our code is a composition of functions. And instead of “rebuilding” each function’s output from scratch, Fusion tries to either use its cached value, or rebuild it from cached outputs of other functions that are known to be consistent.
- To ensure every used value is consistent, it captures and remembers all of its dependencies. So ultimately, you have to invalidate only the “root dependencies” to instantly invalidate the whole graphs of values that are “based” on them. In other words, Fusion automates dependency tracking and cascading invalidation.
- Finally, Fusion does this mostly transparently for you — so in the end, you get a speed up provided by “incremental build for any value” without necessity to make significant changes to your code.
What’s common between Fusion and CAD systems?
Look at the video below — it shows how hardware is designed nowadays. The model designed in CAD system (e.g. Autodesk Inventor) is later transformed into a program controlling CNC machines, and these machines carve model parts from metal, 3D print them, etc.
Such models are almost always parametric, and the video is all about this. Parametric models have constraints, which are defined when the model is designed; some examples of such constraints are:
- Lines A and B must be perpendicular
- Circles A and B must be concentric
- The distance between two points is dimD
- The angle between the line A and B must be dimAlpha
Any non-toy CAD system has a geometric constraint solver — an algorithm that adjusts the “free” parts of the model to satisfy every constraint. The number of possible solutions for a given model is quite important:
- 0 solutions: there is no way to satisfy all of the constraints. Usually, it means you should change or remove some of them — e.g. change the dimension to “unblock” the desirable solutions.
- 1 solution: the model is fully constrained. That’s a perfect state, it means there are no components that can be freely moved, scaled, extended, etc., so if you ask someone else to manufacture the model by providing just a list of components/operations and constraints between them, you’ll get the exact copy of what you see. Purple lines on video are the ones fully constrained.
- many solutions: you need to add more constraints to make the model fully constrained. It’s obviously a bad idea to send the model in this state to manufacturing.
So as you see, constraints are super important in CAD. Asserting there is only one geometry that satisfies all of them is crucial for manufacturing, but importantly, they also allow engineers to adjust the model later. That’s how it works:
- You specify that all the holes in part_P have dimD diameter (dimD is a constraint), which matches the screw size you plan to use. Let’s say it is screw size #10.
- dimD may be used in other constraints, and in particular, contribute to a derived dimension dimXWidth (the width of part_P). Derived dimension is a dimension that is “measured” after the application of constraints contributing to it.
- If multiple instances of part_P are used in our final assembly, dimD (either directly or via dimXWidth & other constraints) may contribute to many other parts of the model.
Now, assume you already manufactured all the parts of your final assembly and discovered you have no #10 screws — the AI running your warehouse concluded it’s better to sell them as a scrap metal to buy all available WD-40.
The only screws left are M6 screws (guess why…), and you’re curious which parts of your model have to be re-manufactured.
When your model is, basically, a ball, the answer is easy, right?
The gist: dependencies and constraints are quite useful in real life, and having something capable to tell what’s impacted by seemingly a tiny change is super useful too. If you’re still not convinced, the famous “The Space Shuttle and the Horse’s Rear End” should tell a non-fictional story about dependencies.
How all of this is related to Fusion?
It’s actually super simple:
- “A part of the model” in the story above = “anything you can compute, including the “look” of UI you want to update in real-time”
- “Constraint” = “a function”, which actually is a constraint defining the relation between its inputs and outputs. Note that such a constraint is very easy to satisfy: all you need is to compute the function’s output for certain inputs.
- “Dimension” = one of “true inputs” — values that can’t be computed from something else (~ aren’t derived from constraints). E.g. anything you store in the database.
And Fusion is an efficient constraint solver. “Efficient” here means it re-computes only what’s outdated, and reuses as much of remaining components as possible — and that’s why Fusion improves performance. For the sake of clarity, a typical web app recomputes almost everything needed (directly or indirectly) to produce the result for each and every API call. I’ll explain this in one of the upcoming posts, but for now, just believe me.
The way Fusion achieves this is:
- When a “fusion function” runs, it records every “fusion value” used in it and attaches the information about what’s used to the output (which is also a “fusion value”).
- When one of “true inputs” (fusion values too) changes, it tells each of its dependants (“fusion values” too) that they are outdated now. They repeat the same for their own dependants, which triggers a kind of chain reaction (now you see why the library is named “Fusion”). The process completes when all the dependants of a certain “true input” are marked as outdated. The real-world analogy is answering a question like: “what parts I have to re-manufacture to switch from #10 to some other screw size?”. With Fusion, you mark the dimension corresponding to this screw size as outdated and get every other part dependent on it marked outdated automatically.
- Finally, “fusion values” can have their remote replicas living on other machines — in particular, in your browser. The closest real-world example of a similar process are apps on your phone — when they update to the newest version right after the update is released. Moreover, Fusion implements a very similar process for such updates — it doesn’t update replicas automatically, but only marks them outdated once the “source” gets outdated. You are the one who triggers the subsequent update, and you decide how fast it has to happen. This is one of the features allowing Fusion apps to scale nicely in cases when a very high message rate could be a problem.
Long story short, Fusion tracks all the dependencies down to the roots to precisely know when any part of UI becomes outdated. Once this happens, it lets you decide when an outdated part has to be re-rendered. Typically you decide what’s the delay between the moment you know something is invalidated and the actual update (re-render), and in vast majority of cases this decision is super simple:
- If the current user just performed an action, there should be no delay, because most likely these are the changes caused by his/her action
- Otherwise let’s wait for 0.5s — these are the changes made by someone (or something) else, so it’s unlikely the current user even notices such a delay (think if it matters whether you see your friend’s message instantly or with 1s delay). Of course, you can reduce it to zero too, but usually, it makes sense to have some delay to limit the max. per-user update rate.
P.S. Next time I’ll try to explain the difference between Fusion and such libraries as SignalR.