tc39 / proposal-policy-map-set

A TC39 proposal for Maps and Sets with cache replacement policies like LRU and LFU.
BSD 3-Clause "New" or "Revised" License
15 stars 2 forks source link

Cache-reduction triggers #7

Open js-choi opened 2 years ago

js-choi commented 2 years ago

2 deals with replacement policies. This issue deals with replacement and reduction triggers.

The simplest replacement trigger is exceeding an integer cardinality, e.g.:

const cache = new CacheMap('lifo', 1);
// Or maybe with an options bag:
const cache = new CacheMap('lifo', { maxSize: 1 });

cache.set(0, 0);
cache.set(1, 0); // LIFO eviction triggered. 0 ⇒ 0 is now replaced.

But we can theoretically extend this to triggers from the host/environment, such as for memory pressure (as defined by the host). This would be useful for large, long-running applications, without having to resort to low-level WeakRefs or garbage-collection hooks (see #4). It might look like this:

// LIFO eviction would automatically occur at the discretion of the engine.
const cache = new CacheMap('lifo', 'memoryPressure');
// Or maybe with an options bag:
const cache = new CacheMap('lifo', { purgeable: true });

How should such trigger functionality work? This replacement-trigger API must be composable with the cache policy. It might be useful independently of the policy-map/set API itself. Maybe it should use the old proposed Emitter interface and/or Observable interface? But it would probably be simplest to keep it as a string or boolean flag.

CC: @syg

js-choi commented 2 years ago

There are some nuances to memory-pressure triggers.

Should the developer be able to specify both a memory-pressure trigger and a maximum cardinality?

My inclination is to allow both at once. For example, if I were using a CacheMap to implement a packrat parser on a very long stream in the background of a user-facing application, then I may wish to prevent the parser’s cache from ever becoming large enough to impact the memory of the user’s device and cause a memory-pressure signal. So I might give a constant maximum size to that cache, e.g., 256. But I would also want the cache to start removing entries if it causes memory pressure, even if this occurs before the cache reaches the maximum size.

There are several API choices for specifying both (along with the replacement policy and initial entries). I’ve opened #10 to bikeshed the API.

When should a cache remove values in response to memory pressure?

My inclination is to respond to memory-pressure signals at the end of the current engine job, like with WeakRef. Developers would observe memory-pressure-induced cache removal only between event-loop ticks. This hopefully would prevent race conditions when synchronously using cache.has then cache.get.

How many values should a cache remove in response to memory pressure?

My inclination is to leave this up to the engine. It is the engine that can best determine the effective memory sizes of objects. An engine could remove as many values from a cache as it deems optimal to alleviate memory pressure. Of course, the removal order would be determined by the cache policy, like LRU and LIFO.

Should memory-pressure triggers be opt-in or opt-out?

My inclination is to make memory-pressure triggers be opt-in. Otherwise, developers may be surprised when the engine silently removes values in response to memory pressure.

Should the memory-pressure signal be a generic Emitter/Observable?

I think this would be cool. But I don’t think it’s necessarily in scope of this proposal. This proposal already brings a lot of functionality for memory-constrained applications. And the Emitter and Observable proposals have been bogged down for the past four or five years.

If either Emitter or Observable does make further progress, we could always use the same machinery in this proposal to make a MemoryPressure global in a separate proposal.

(It might be worth considering an alternative to an Emitter/Observable: adding a CacheRef class to this proposal. It would hold a single value that would be subject to clearing when memory pressure increases. This may be good enough for many long-running, memory-constrained applications with heavy singletons.)

How can we keep memory pressure private?

A website could use memory pressure to fingerprint users’ devices. As long as the specification gives enough leeway to engine vendors, they could randomize the precise timing of their signals and decrease fingerprinting entropy.

Related web proposals