Skip to the content.

Part 5: Fusion on Server-Side Only

Even though Fusion supports RPC, you can use it on server-side to cache recurring computations. Below is the output of Caching Sample (slightly outdated):

Local services:
Fusion's Compute Service [-> EF Core -> SQL Server]:
  Reads         : 27.55M operations/s
Regular Service [-> EF Core -> SQL Server]:
  Reads         : 25.05K operations/s

Remote services:
Fusion's Compute Service Client [-> HTTP+WebSocket -> ASP.NET Core -> Compute Service -> EF Core -> SQL Server]:
  Reads         : 20.29M operations/s
RestEase Client [-> HTTP -> ASP.NET Core -> Compute Service -> EF Core -> SQL Server]:
  Reads         : 127.96K operations/s
RestEase Client [-> HTTP -> ASP.NET Core -> Regular Service -> EF Core -> SQL Server]:
  Reads         : 20.46K operations/s

Last two results are the most interesting in the context of this part:

And that’s the main reason to use Fusion on server-side only: 5-10x performance boost with a relatively tiny amount of changes. Similarly to incremental builds, the more complex your logic is, the more you are expected to gain.

The Fundamentals

You already know that Computed<T> instances are reused, but so far we didn’t talk much about the details. Let’s learn some specific aspects of this behavior before jumping to caching.

The service below prints a message once its Get method is actually computed (i.e. its cached value for a given argument isn’t reused) and returns the same value as its input. We’ll be using it to find out when IComputed instances are actually reused.

``` cs –editable false –region Part05_Service1 –source-file Part05.cs public class Service1 : IComputeService { [ComputeMethod] public virtual async Task Get(string key) { WriteLine($"{nameof(Get)}({key})"); return key; } }

public static IServiceProvider CreateServices() { var services = new ServiceCollection(); services.AddSingleton<ISwapService, DemoSwapService>(); services.AddFusion() .AddService() .AddService() // We'll use Service2 & other services later .AddService() .AddService(); return services.BuildServiceProvider(); }


First, `IComputed` instances aren't "cached" by default - they're just
reused while it's possible:

``` cs --region Part05_Caching1 --source-file Part05.cs
var service = CreateServices().GetRequiredService<Service1>();
// var computed = await Computed.Capture(() => counters.Get("a"));
WriteLine(await service.Get("a"));
WriteLine(await service.Get("a"));
GC.Collect();
WriteLine("GC.Collect()");
WriteLine(await service.Get("a"));
WriteLine(await service.Get("a"));

The output:

Get(a)
a
a
GC.Collect()
Get(a)
a
a

As you see, GC.Collect() call removes cached IComputed for Get("a") - and that’s why Get(a) is printed twice here.

All of this means that most likely Fusion holds a weak reference to this value (in reality it uses GCHandle-s for performance reasons, but technically they do the same).

Let’s prove this by uncomment the commented line:

``` cs –region Part05_Caching2 –source-file Part05.cs var service = CreateServices().GetRequiredService(); var computed = await Computed.Capture(() => service.Get("a")); WriteLine(await service.Get("a")); WriteLine(await service.Get("a")); GC.Collect(); WriteLine("GC.Collect()"); WriteLine(await service.Get("a")); WriteLine(await service.Get("a"));


The output:

```text
Get(a)
a
a
GC.Collect()
a
a

As you see, assigning a strong reference to IComputed is enough to ensure it won’t recompute on the next call.

So to truly cache some IComputed, you need to store a strong reference to it and hold it while you want it to be cached.

Now, if you compute f(x), is it enough to store a computed for its output to ensure its dependencies are cached too? Let’s test this:

``` cs –editable false –region Part05_Service2 –source-file Part05.cs public class Service2 : IComputeService { [ComputeMethod] public virtual async Task Get(string key) { WriteLine($"{nameof(Get)}({key})"); return key; }

[ComputeMethod]
public virtual async Task<string> Combine(string key1, string key2)
{
    WriteLine($"{nameof(Combine)}({key1}, {key2})");
    return await Get(key1) + await Get(key2);
} } ```

``` cs –region Part05_Caching3 –source-file Part05.cs var service = CreateServices().GetRequiredService(); var computed = await Computed.Capture(() => service.Combine("a", "b")); WriteLine("computed = Combine(a, b) completed"); WriteLine(await service.Combine("a", "b")); WriteLine(await service.Get("a")); WriteLine(await service.Get("b")); WriteLine(await service.Combine("a", "c")); GC.Collect(); WriteLine("GC.Collect() completed"); WriteLine(await service.Get("a")); WriteLine(await service.Get("b")); WriteLine(await service.Combine("a", "c"));


The output:

```text
Combine(a, b)
Get(a)
Get(b)
computed = Combine(a, b) completed
ab
a
b
Combine(a, c)
Get(c)
ac
GC.Collect() completed
a
b
Combine(a, c)
Get(c)
ac

As you see, yes,

Strong referencing an IComputed ensures every other IComputed instance it depends on also stays in memory.

Let’s check if the opposite is true as well:

``` cs –region Part05_Caching4 –source-file Part05.cs var service = CreateServices().GetRequiredService(); var computed = await Computed.Capture(() => service.Get("a")); WriteLine("computed = Get(a) completed"); WriteLine(await service.Combine("a", "b")); GC.Collect(); WriteLine("GC.Collect() completed"); WriteLine(await service.Combine("a", "b"));


The output:

```text
Get(a)
computed = Get(a) completed
Combine(a, b)
Get(b)
ab
GC.Collect() completed
Combine(a, b)
Get(b)
ab

So the opposite is not true.

But why Fusion behaves this way? The answer is actually super simple:

Default caching behavior - a summary:

Caching Options

As you probably already understood, Fusion allows you to implement any desirable caching behavior: all you need is your own service that will hold a strong reference to whatever you want to cache for as long as you want.

Besides that, Fusion offers two built-in options:

Let’s look at how they work.

ComputeMethodAttribute.MinCacheDuration

Let’s just add MinCacheDuration to the service we were using previously:

``` cs –editable false –region Part05_Service3 –source-file Part05.cs public class Service3 : IComputeService { [ComputeMethod] public virtual async Task Get(string key) { WriteLine($"{nameof(Get)}({key})"); return key; }

[ComputeMethod(MinCacheDuration = 0.3)] // MinCacheDuration was added
public virtual async Task<string> Combine(string key1, string key2)
{
    WriteLine($"{nameof(Combine)}({key1}, {key2})");
    return await Get(key1) + await Get(key2);
} } ```

And run this code:

``` cs –region Part05_Caching5 –source-file Part05.cs var service = CreateServices().GetRequiredService(); WriteLine(await service.Combine("a", "b")); WriteLine(await service.Get("a")); WriteLine(await service.Get("x")); GC.Collect(); WriteLine("GC.Collect()"); WriteLine(await service.Combine("a", "b")); WriteLine(await service.Get("a")); WriteLine(await service.Get("x")); await Task.Delay(1000); GC.Collect(); WriteLine("Task.Delay(...) and GC.Collect()"); WriteLine(await service.Combine("a", "b")); WriteLine(await service.Get("a")); WriteLine(await service.Get("x"));


The output:

```text
Combine(a, b)
Get(a)
Get(b)
ab
a
Get(x)
x
GC.Collect()
ab
a
Get(x)
x
Task.Delay(...) and GC.Collect()
Combine(a, b)
Get(a)
Get(b)
ab
a
Get(x)
x

As you see, MinCacheDuration does exactly what’s expected:

That’s basically it on MinCacheDuration.

A few tips on how to use it:

P.S. If you love algorithms and data structures, check out [ConcurrentTimerSet](https://github.com/servicetitan/Stl.Fusion/blob/master/src/Stl/Time/ConcurrentTimerSet.cs) - Fusion uses its own implementation of timers to ensure they scale much better than `Task.Delay` (which relies on [TimerQueue](https://referencesource.microsoft.com/#mscorlib/system/threading/timer.cs,29)), though this comes at cost of fire precision: Fusion timers fire only [4 times per second](https://github.com/servicetitan/Stl.Fusion/blob/master/src/Stl.Fusion/Internal/Timeouts.cs#L20). Under the hood, `ConcurrentTimerSet` uses [RadixHeapSet](https://github.com/servicetitan/Stl.Fusion/blob/master/src/Stl/Collections/RadixHeapSet.cs) - basically, a [Radix Heap](http://ssp.impulsetrain.com/radix-heap.html) supporting `O(1)` find and delete operations.

Next: Part 6 » | Tutorial Home