Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server-side caching of viewmodels #704

Open
wants to merge 6 commits into
base: master
from

Conversation

@tomasherceg
Copy link
Member

commented Jun 16, 2019

Disclaimer: This is an experimental feature - it should not be merged in master any time soon without extensive testing.

I have implemented a relatively simple mechanism that can dramatically decrease the amount of data transferred between the client and the server on postbacks. Basically, the viewmodel is cached on the server and its hash is used as the cache key (that should keep the cache small for static pages or for pages with identical initial state).

  1. On HTTP GET, the viewmodel JSON (without CSRF token and encrypted values) is cached on the server.
  2. When the HTML is generated, the viewModelCacheId field is included to the serialized viewmodel.
  3. DotVVM stores a copy of the viewmodel in dotvvm.viewModels['root'].viewModelCache when the page is loaded.
  4. When a postback is made, the client sends viewModelDiff and viewModelCacheId instead of the full viewmodel.
  5. If the viewmodel is found in the server cache, the diff is applied to it and the request processing works normally. The response viewmodel is cached using the same way as in the first step.
  6. If the viewmodel is not in the cache, the server returns a special response with viewModelNotCached notice and the client repeats the postback with the full viewmodel.

Notes

The solution is backwards-compatible - the client may decide to always send the full viewmodel and server must support it.

We should make this feature optional and allow to turn it only for individual pages.

Also, a security review of this feature should be made.The CSRF token and encrypted values are not part of the cache mechanism and are sent (and verified) on all requests as it was before.

The viewmodel serialization and deserialization are using synchronous API right now. The cache may require using an async API (in case the user would like to use some distributed cache etc.) and maybe the serialization itself could benefit from async as well.

And finally, I haven't done any performance comparisons yet - I assume that hashing and caching the viewmodels should be way faster than transferring unnecessary data, however we should measure the impacts on a real-world application.

Tomáš Herceg

@quigamdev quigamdev requested a review from exyi Jun 28, 2019

@exyi
Copy link
Member

left a comment

Few comments about the server side, I'll go though the JS part later.

In general, I like the idea of this mechanism, it may improve the bandwidth requirements quite significantly. However, especially on s***ty connection the chaining of requests in case the VM is not on the server may cause issues (when combined with #705 there may be 3 requests). When there is a fixed timeout of few minutes, I think we could also explain that to the client, so it does not even try to send the diff.

{
private readonly IDotvvmCacheAdapter cacheAdapter;

public TimeSpan CacheLifetime { get; set; } = TimeSpan.FromMinutes(5);

This comment has been minimized.

Copy link
@exyi

exyi Jul 18, 2019

Member

This is quite short IMHO and it's not trivial to reconfigure.

This comment has been minimized.

Copy link
@exyi

exyi Jul 18, 2019

Member

It would however make sense to somehow remove entries that were used by client that got a new model from the server. For that we'd have to pass some client identifier to the cache.

This comment has been minimized.

Copy link
@tomasherceg

tomasherceg Jul 19, 2019

Author Member

The lifetime management will change definitely, I need to do some measurements. There should also be some limits per route. The problem with removing cache entries is that you don't know how many people are using them. We'd need to add some reference counting - maybe the short lifetime will work well enough.

@exyi

This comment has been minimized.

Copy link
Member

commented Jul 19, 2019

@exyi

This comment has been minimized.

Copy link
Member

commented Jul 19, 2019

@exyi

This comment has been minimized.

Copy link
Member

commented Jul 19, 2019

@exyi

This comment has been minimized.

Copy link
Member

commented Jul 19, 2019

@tomasherceg

This comment has been minimized.

Copy link
Member Author

commented Jul 19, 2019

What will be the benefit of ReadOnlyMemory over byte[]? I found that the lowest version of Newtonsoft.Json.Bson supports 10.0.3 which is minimum version of Newtonsoft.Json required by DotVVM, so it should not be an issue.
I have added an extensibility point in DefaultViewModelSerializer so someone might add compression or use any other method of getting bytes from the viewmodel JToken and vice-versa.

I'll continue working on this later.
I'd like to add code that emits some metrics, and enable this feature on a few websites to gather some data and usage patterns. I am thinking of collecting:

  • the number of cache entries created by a particular route
  • how many times these entries were used for a particular route
    If the first number is big and the second is low, caching is not useful on the particular route.
    If the first number is small and the second is big, the cache can help a lot.

I don't know yet if we want to have some auto-tuning in the framework that would decide whether the cache is good or not, or if we just give the user the tools to decide on their own. I would definitely start with the second way, but I am not sure if anyone will use it if it requires some work to set it up.

string viewModelCacheId = null;
if (context.Configuration.ExperimentalFeatures.ServerSideViewModelCache.IsEnabledForRoute(context.Route.RouteName))
{
viewModelCacheId = viewModelServerCache.StoreViewModel(context, (JObject)viewModelToken);

This comment has been minimized.

Copy link
@exyi

exyi Aug 11, 2019

Member

One more thing, we should only store properties that sent to client and also back to server. This way we are wasting quite a bit of space. Fortunately, the serializer should silently ignore properties that should not be sent to server, so there is no change in behavior.

Unless you use the Direction.ClientToServerNotInPostbackPath, then the serializer will take it into account even though it should not be sent at all (assuming it was not in the path). Unfortunately, these properties can't be just dropped as they might also be needed when they are in the path. And, on the server side, we have basically no way of knowing which object are in the path during the serialization phase, so I don't see a simple fix to that :/ Maybe we could figure out the JSON path on client and send it to the server.

@exyi exyi referenced this pull request Aug 14, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants
You can’t perform that action at this time.