var counters = new Dictionary<string, int>();
// Dependencyvar getCounter = ToAwesome((Func<string, int>) (key
=> counters.GetValueOrDefault(key)));
// Dependent functionvar getCounterText = ToAwesome((Func<long, string>) (key
=> $"Count: {GetCounter(key)}"));
WriteLine(getCounterText("A")); // "Count: 0" - invokes both delegates
WriteLine(getCounterText("A")); // "Count: 0" - cache hit for getCounterText("A")
counters["A"] = 1;
Computed.Invalidate(() => getCounter("A")) // Invalidates both cached values
WriteLine(getCounterText("A")); // "Count: 1" - invokes both delegates again
Our new superpowers:
Call result caching
Dependency tracking
The same value is never computed concurrently
And it does this without changing neither the signature, nor the implementation of a function it gets!
"So, tell me, my little one-eyed one, on what poor, pitiful, defenseless planet has my monstrosity been unleashed?"
β Dr. Jumba Jookiba, #1 scientist in my list
The Incrementally-Build-EVERYTHING Decorator!
Let's remember where we started:
It's quite expensive to recompute everything on every update Cache use you must?
But what if some of our functions are impure? Not a problem this is anymore!
And quite time consuming, because a part of these functions require RPC. Client-side cache use you must? We'll get back to this part later.
Do we really need this syntax with delegates?
We don't. It's actually much more convenient to apply this decorator to virtual methods tagged by a special attribute by generating a proxy type in runtime that overrides them.
What else is missing?
Actual implementation of a "box"
Async support
GC-friendly "box" cache
GC-friendly refs to dependants
A lot more. But slides are to show the bright side of things, right?
What about eventual consistency?
What about React and Blazor?
Flash Slothmore is eventually consistent:
He will close all of his tasks-in-slow-progress eventually. OnceJudy Hopps stops distracting him with her problems
(stops giving him more tasks).
– Bro, do you have a cache?
– I do - but it's so tiny...
– We are doomed! The state is eventually consistent!
Imagine two eventually consistent systems -
what's their key difference?
#1
#2
I'm in the real-time business. How this is relevant?
Real-time updates require you to:
Know when a result of a function changes Invalidate all the things!
Recompute new results quickly Incrementally build all the things!
Send them over the network
*.NET all the things?
Ideally, as a compact diff to the prev. state Diff can be computed in O(diffSize) for immutable types (details).
"There are only two hard things in Computer Science: cache invalidation and naming things."
β Phil Karlton
Naming problem is of the same scale as the Ultimate Question of Life, the Universe, and Everything, so... Good we've made a meaningful progress with a simpler one!
Blazor is:
.NET running in your browser
Nearly 100% compatibility with .NET 5!
Expression.Compile(...), Reflection, etc. works
No threads yet, but Task<T> works
Blazor UI Components β React Components, but with .NET bells and whistles!
Blazor β cons:
No JIT / AOT compilation yet - in fact, everything is interpreted
It's .NET, so even a tiny project loads a fair number of assemblies.
There is linking with tree shaking, but even it leaves 2β¦4 MB of .dlls.
Blazor β pros:
.NET = so many ready-to-use NuGet packages + no need for JS, TS, etc.
.dlls are loaded once & stored in application cache.
They aren't updated even on F5 β unless you explicitly clear it.
Blazor Server helps to mitigate this by further letting your UI code to run on server side (e.g. for slow mobile devices). The JS payload is tiny in this case.
AOT and threads are expected in 2021. JS won't get threads - ever. CPU core count is increasing. So I bet in 1-2 years WASM (and Blazor) will be #1 choice for truly responsive UI.
There is experimental Blazor Mobile: like React Native, but relying on Blazor compontens and native .NET runtime on each platform.
Do the same, but deliver the invalidation event via RPC.
Your Web API call:
β How's my app doing?
β Still alive.
1 request = 1 response.
Fusion API call:
β How's my app doing? +publish
β Still alive. +watch pub-666 β Be brave, pub-666 is... Invalidated.
1 request = 1 or 2 responses,
the 2nd one might come much later.
The invalidation notifications are delivered via Publisher-Replicator channel. Fusion uses WebSocket connection for such channels now, but more options to be available eventually.
ComposerService - an example service relying on remote replicas
publicvirtualasync Task<ComposedValue> GetComposedValueAsync(string parameter, Session session)
{
// Fusion magic: all these seemingly RPC call complete instantly w/o// a real RPC while the result they produce is known to be consistent.var chatTail = await ChatService.GetChatTailAsync(1);
var uptime = await TimeService.GetUptimeAsync(TimeSpan.FromSeconds(10));
var sum = (double?) null;
if (double.TryParse(parameter, outvarvalue))
sum = await SumService.SumAsync(new [] { value }, true);
var lastChatMessage = chatTail.Messages.SingleOrDefault()?.Text ?? "(no messages)";
var user = await AuthService.GetUserAsync(session);
var activeUserCount = await ChatService.GetActiveUserCountAsync();
returnnew ComposedValue(
$"{parameter} - server", uptime, sum,
lastChatMessage, user, activeUserCount);
}
A real Blazor component that updates in real-time:
@inherits LiveComponentBase<ActiveTranscriptions.Model>
@using System.Threading
@using ServiceTitan.Speech.Abstractions
@inject ITranscriber Transcriber
@{
var state = State.LastValue; // We want to show the last correct model on errorvar error = State.Error;
}
<DataGrid TItem="Transcript"
Data="@state.Transcripts"
TotalItems="@state.Transcripts.Length"
Sortable="false"
ShowPager="false">
<DataGridCommandColumn TItem="Transcript"/>
<DataGridColumn TItem="Transcript" Field="@nameof(Transcript.Id)" Caption="#"/>
<DataGridColumn TItem="Transcript" Field="@nameof(Transcript.StartTime)" Caption="Start Time"/>
<DataGridColumn TItem="Transcript" Field="@nameof(Transcript.Duration)" Caption="Duration"/>
<DataGridColumn TItem="Transcript" Field="@nameof(Transcript.Text)" Caption="Text" Width="75%" />
</DataGrid>
@code {
publicclassModel
{
public Transcript[] Transcripts { get; set; } = Array.Empty<Transcript>();
}
protectedoverridevoidConfigureState(LiveState<Model>.Options options)
// Update delays are configurable per-state / per-component
=> options.WithUpdateDelayer(o => o.Delay = TimeSpan.FromSeconds(1));
protectedoverrideasync Task<Model> ComputeStateAsync(CancellationToken cancellationToken)
{
var transcriptIds = await Transcriber.GetActiveTranscriptionIdsAsync(cancellationToken);
var transcriptTasks = transcriptIds.Select(id => Transcriber.GetAsync(id, cancellationToken));
var transcripts = await Task.WhenAll(transcriptTasks); // Get them all in parallel!returnnew Model() { Transcripts = transcripts };
}
}
publicvirtualasync Task<User?> TryGetAsync(long userId)
{
await Everything(); // LMK if you know what's the role of this call!awaitusingvar dbContext = DbContextFactory.CreateDbContext(); // Pooledvar user = await dbContext.Users.FindAsync(new[] {(object) userId});
return user;
}
How efficient is Fusion caching?
The Reader async task (the test runs 3 readers per core):
async Task<long> Reader(string name, int iterationCount)
{
var rnd = new Random();
var count = 0L;
for (; iterationCount > 0; iterationCount--) {
var userId = (long) rnd.Next(UserCount);
var user = await users.TryGetAsync(userId);
if (user!.Id == userId)
count++;
extraAction.Invoke(user!); // Optionally serializes the user
}
return count;
}
There is also a similar Mutator, but only one instance of it is running.
How efficient is Fusion caching?
Sqlite EF provider: 16,070x
With Stl.Fusion:
Standard test:
Speed: 35708.280 K Ops/sec
Standard test + serialization:
Speed: 12481.940 K Ops/sec
Without Stl.Fusion:
Standard test:
Speed: 2.222 K Ops/sec
Standard test + serialization:
Speed: 2.179 K Ops/sec
In-memory EF provider: 1,140x
With Stl.Fusion:
Standard test:
Speed: 30338.256 K Ops/sec
Standard test + serialization:
Speed: 11789.282 K Ops/sec
Without Stl.Fusion:
Standard test:
Speed: 26.553 K Ops/sec
Standard test + serialization:
Speed: 26.143 K Ops/sec
And that's just a plain caching, i.e. no any extra benefits from the "incremental build for everything" that Fusion adds!
Fusion's Caching Sample
A very similar code, but exposing the service via Web API. The results:
20,000 β 130,000 RPS = 6.5x throughput
With server-side changes only, i.e. the same client.
20,000 β 20,000,000 RPS = 1000x throughput!
If you switch to Fusion client (so-called "Replica Service")
RestEase Client -> ASP.NET Core -> EF Core Service:
Reads: 20.46K operations/s
RestEase Client -> ASP.NET Core -> Fusion Proxy -> EF Core Service:
Reads: 127.96K operations/s
Fusion's Replica Client:
Reads: 20.29M operations/s
How 10x speed boost looks like?
What do you get with Fusion?
The feeling of flight:
Caching - with automatic dependency tracking and cascading invalidation
The same value is never computed concurrently
But: all [ComputeMethod]-s can run concurrently!
What do you get with Fusion?
The feeling of awesomeness of your clean code:
You describe the substance.
"That's what I want to get"
Fusion allows you to express this substance in the clean form.
By ensuring you get what you want as efficiently as possible.
Just imagine, Fusion's Replica Services resolve 99.9% of your RPC calls locally, but still produce the right answers. And to achieve that, they span the web of their invalidation chains across multiple servers. Awesome, right?
What do you get with Fusion?
The feeling of laziness: you get all of that with almost zero changes in code!
Just add waterComputed.Invalidate(...).
β AY, Fusion's creator
What do you get with Fusion?
The feeling of ultimate super power (together with Blazor):
You can run Fusion services on the client too!
Moreover, Fusion includes LiveComponent - a base base type for your Blazor components that has everything you need for real-time updates!
So you don't need a Knockout or MobX alternative for Blazor. Just use Fusion - everywhere!
Moreover, if you use the same interfaces for your Fusion services and their client-side replicas, your UI code will run equally well on the server side too! This is what allows Fusions samples to support both Blazor WebAssembly and Blazor Server mode.
What's the cost?
Money: thanks to ServiceTitan, Fusion is free (MIT license)
CPU: free your CPUs! The torture of making them to run recurring computations again and again must be stopped!
RAM: is where the cost is really paid. Besides that, remember about GC pauses and other downsides of local caching. But the upside is so bright + Fusion actually supports external caching via "swapping" feature.
Learning curve: is relatively shallow in the beginning, but getting steeper once you start to dig deeper. Though Fusion is definitely not as complex as e.g. TPL with its ExecutionContext, ValueTask<T>, and other tricky parts.
Other risks: First lines of Fusion code were written 9 months ago. What "other risks" are you talking about?
What's the cost?
If you need a real-time UI, Fusion is probably the lesser of many evils you'll have to deal with otherwise. *