3

In my application I have these TypeScript classes called Schedule and in the UI I would like to be able to represent these objects as natural language descriptions such as "Every 2nd Monday of the month at 6:00 AM".

To calculate these "Schedule Descriptions" I have an Angular service ScheduleDescriptionService which I call getScheduleDescription() and I pass it a Schedule and it returns the description back as a string.

Unfortunately I learned that this getScheduleDescription() function is very expensive and it gets called a lot in some parts of the application. What I want to do is to solve the performance issue by caching the descriptions after they're generated so they don't have to be generated every time the UI wants to display them.

The reason I put this function into an Angular service is because I need the function that generates these descriptions to have access to an internationalization service and as far as I know I can't inject an Angular service into a normal TypeScript class like Schedule otherwise I might have put getScheduleDescription() into the Schedule class that way I can just deal with each Schedule caching its own description.

I can't use an Angular pipe because I need to be able to get the descriptions in my code-behind so I need to somehow manage caching for a lot of schedule descriptions and I can't just handle the caching in any one Angular component because I will need to display these descriptions all over the place in my application. So that leaves me to believe that I have to cache these descriptions in my ScheduleDescriptionService Angular service.

Is this the right way to go? If so, how do I cache in this situation? If the service lasts the lifetime of my application what if my user looks at a lot of descriptions? Should I clear the cache if they stop looking at a page with schedule descriptions? How do I manage change detection?

Thanks in advance.

Kyle V.
  • 4,752
  • 9
  • 47
  • 81
  • How are you benchmarking this? A better question would be if you made improvements, how would you know they perform better? A simple solution would be to memoize the function. – bc1105 Apr 13 '18 at 18:10
  • @bc1105 my specific use case is to prevent a cell value from having to be recalculated every digest cycle in a third-party data grid control component. It's a virtual-scrolling grid so every time that cell came in and out of view it had to be recalculated and it was noticeably slow. In your opinion, knowing my specific use case, would you still recommend memoization over caching? – Kyle V. Apr 13 '18 at 19:47
  • That's not bad...the main thread is too busy hence the poor rendering. I would suggest trying memoization as it may be a quick win for you. Besides that a key/value cache to lookup the values and skip calculation may take the performance further. The best solution: offload the calculations to Service Worker. – bc1105 Apr 13 '18 at 19:55

3 Answers3

1

There are several ways to cache state in your angular application. You can do something like in a service serialize an object or objects and store them in sessionStorage, localStore, or a cookie. Then when retrieving the description first check in your cache for the object, if its there great use it, if its not go retrieve it and stick it in the cache.

If you want something more elaborate and robust I would look at implementing ngrx. It's a global redux store and provides your application with a global immutable store for holding your application state. The idea in your case would be to calculate the descriptions once, stick them in the store, and then have whatever components or services that care about the description subscribe to the store and get the value.

cobolstinks
  • 6,801
  • 16
  • 68
  • 97
  • How do I manage that cache though? I don't want the cache to grow unchecked the longer the user has the application open and I don't want to clear items from the cache prematurely so they have to be recalculated. – Kyle V. Apr 12 '18 at 14:36
  • You have to come up with how you want to manage that yourself and how you solve the problem depends somewhat on what your data structures look like, and how fresh your data needs to be. If you use NGRX and you need to update an element in the store, you can dispatch an action which will then get a new value from the server (through an effect) and then update the store. Then all things subscribed to that part of the store will get the new value. – cobolstinks Apr 12 '18 at 15:33
  • I'm actually not even sure how to structure the cache its self. I want to do something like use an array but with strings as keys to quickly query the cache but I also want to be able to limit the size of this data structure so that older entries get bumped out of the array as newer ones are added. – Kyle V. Apr 12 '18 at 18:15
1

It depends on how long the data should be available.

Data available on page refresh / across multiple tabs

If you need the data data to persis across page refresh, or be available when the user opens multiple tabs, I'd say to just use localStorage, which can store a few MBs. If you are just storing a schedule id as a key and a description as value, you should have enough for tens of thousands of descriptions.

You could manage a list and delete the oldest entries when the list has reached a certain size or when there is no more space in localStorage. I don't think you need to worry too much on retrieval time though, unless you litteraly call this object thousands of time on the page

Note: If you are planning on using angular universal, localStorage will not be available server-side.

Data available for session duration

You could just store the values in a JS object, or a Map object.

using a map

class ScheduleDescriptionService
{
    private map : Map<number, string> = new Map<number,string>();
    getScheduleDescription(schedule: Schedule)
    {
        let desc = this.map.get(schedule.id);
        if(!desc)
        {
            desc = this.internationalizationService.getDescription(schedule);
            this.map.set(schedule.id, desc);
        }
        return  desc;
    }
}

Side note If you are not already doing it, you could also use OnPush detection change strategy to try reducing the number of times the description service is called

David
  • 33,444
  • 11
  • 80
  • 118
  • This solution worked for me, thanks! As for your side note -- I assume that you were suggesting OnPush for the component that calls the service. I'm unable to do this since the third-party data grid control is deciding when to call the service for me. :/ – Kyle V. Apr 16 '18 at 15:32
1

I'd suggest to use memoization, so, you can basically wrap your expensive function with a further function which will cache its result and serve the cached output as long as its parameters don't change.

function veryExpensive(arg) {  
  for(let i = 0; i < 3000000000; i++);
  return `result of ${arg}`;
}


function memoize(worker) {
  let res, previousArgs;
  
  return (...args) => {
    console.time(`memoize(worker)("${args}")`);
  
    // shallow comparison
    const cacheable = previousArgs && args.every((arg, index) => arg === previousArgs[index]);
    
    if(!cacheable) {
      previousArgs = args;
      res = worker(...args);
    }
    
    console.timeEnd(`memoize(worker)("${args}")`);
    return res;
  };
}

const memoized = memoize(veryExpensive);

memoized('Cache Function');
memoized('Cache Function');
memoized('Cache Function');
memoized('Cache Function');
memoized('Cache Function');
memoized('Cache Function');
memoized('Cache Function');
memoized('Cache Function');
memoized('Cache Function');
memoized('Cache Function');
memoized('Cache Function');

memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
memoized('One More Cache');
Hitmands
  • 13,491
  • 4
  • 34
  • 69