Skip to content

feat(cache-map): Promote makeLRUCacheMap into a new package#2822

Merged
gibson042 merged 9 commits intomasterfrom
gibson-2025-05-cache-map
Jul 11, 2025
Merged

feat(cache-map): Promote makeLRUCacheMap into a new package#2822
gibson042 merged 9 commits intomasterfrom
gibson-2025-05-cache-map

Conversation

@gibson042
Copy link
Member

@gibson042 gibson042 commented May 22, 2025

Description

  • Introduce @endo/cache-map.
  • Remove "LRU" eviction policy constraint (but keep that as the actual current implementation).
  • Support opting in to strongly-held keys (to be used in agoric-sdk for caching string-keyed board mappings).
  • Add a getMetrics() facet for evaluating cache efficiency (currently rudimentary).

Security Considerations

This package sits beneath SES, and so must be defensive without relying upon harden/assert/etc.

Scaling Considerations

The implementation remains a doubly-linked ring with permanent head and LRU eviction policy. All operations are O(1), mutating up to five nodes for promotion/insertion/removal (the target node and its former and new neighbors) but minimizing that when possible (e.g., bailing out early in moveCellAfter when the target is already in the right position). Cells are created up to the specified capacity and never destroyed, although their single-entry weak maps are necessarily replaced when evicting a tail in set. Cells are currently created lazily, although that could be made eager for better memory locality (probably with a boolean lazy option for opting out). All in all, it's probably about as good as an object-based implementation can be (and a bit better than the makeLRUCacheMap from which it is derived).

Documentation Considerations

Includes a README.md with explanation and example usage.

Testing Considerations

Existing tests have been ported and expanded. A future improvement could add GC assertions.

Compatibility Considerations

This continues to support SES, just now as a package that can additionally be used elsewhere.

Upgrade Considerations

None known; this is a new package.

@@ -0,0 +1 @@
export * from './src/cachemap.js';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I doing this right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno about endo these days, but I think agoric-sdk does it different. In general I don't like explicit types conditions in package.json, and prefer tsc to generate the .d.ts files as siblings much as possible.

@boneskull might have some more feedback here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve commandeered the branch and aligned this with the new policy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And it transpires that the types.d.ts was doing a better job of exporting the types than the implicit index.d.ts so I have gone back to @gibson042’s original solution in hopes that we can find a better resolution eventually.

Comment on lines +146 to +143
// * liveTouchStats/evictedTouchStats { count, sum, mean, min, max }
// * p50/p90/p95/p99 via Ben-Haim/Tom-Tov streaming histograms
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A former colleague referred to count/sum/mean/min/max/p50/p90/p95/p99/p99.9 as the "big ten" metrics1.

Footnotes

  1. I believe this is the result of that influence.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They’re pretty ubiquitous, to be sure.

Copy link
Member

@kriskowal kriskowal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish to rely on @erights review for the implementation. The packaging and compatibility story are in order.

Please change the version number to 1.0.0. When this lands, I can do a one-off publish so Agoric integration CI will continue without interruption.

@mhofman mhofman requested a review from michaelfig May 24, 2025 00:58
Copy link
Contributor

@mhofman mhofman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks reasonable.

Someday we'll have to make this durable compatible somehow, but we can cross that bridge then (an explicit linked list may not be appropriate in that case)

@@ -0,0 +1 @@
export * from './src/cachemap.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno about endo these days, but I think agoric-sdk does it different. In general I don't like explicit types conditions in package.json, and prefer tsc to generate the .d.ts files as siblings much as possible.

@boneskull might have some more feedback here.

@gibson042
Copy link
Member Author

Please change the version number to 1.0.0. When this lands, I can do a one-off publish so Agoric integration CI will continue without interruption.

Done. @kriskowal this should be ready to merge, which I'd like to leave in your hands for that initial publish.

@gibson042 gibson042 force-pushed the gibson-2025-05-cache-map branch from dfba2e1 to 582f79f Compare May 29, 2025 12:42
throw Error('internal: Unexpected non-singular cell data entry count');
}
const cellData = /** @type {Map<K, V>} */ (cell.data);
oldKey = /** @type {K} */ (cellData.keys().next().value);
Copy link
Contributor

@mhofman mhofman May 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thought of an alternative, not sure which is better

Suggested change
oldKey = /** @type {K} */ (cellData.keys().next().value);
([oldKey] = cellData.keys());

But regardless I'm also wondering if we shouldn't just call cellData.clear() and not fall through?

Copy link
Member Author

@gibson042 gibson042 May 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally had something like that, and it took me a minute to remember why I switched away so I added a comment explaining the paranoia. But I definitely want to use delete(oldKey) rather than clear(), primarily to use the bare minimum of the non-weak Map API—to which end, I've now made explicit that SingleEntryMap extends WeakMapAPI by at most size and keys.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking more of it, I would prefer if the minimum WeakMap-ish API was an optional clear() instead of a keys(). The former is technically implementable by a something that keeps weak keys, where the latter isn't.

Was there a reason to prefer keys over clear ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'm convinced. Done.

@gibson042 gibson042 force-pushed the gibson-2025-05-cache-map branch from 3017f39 to e4d7a80 Compare May 31, 2025 15:38
@gibson042 gibson042 closed this May 31, 2025
@gibson042 gibson042 reopened this May 31, 2025
@gibson042 gibson042 requested a review from boneskull June 2, 2025 15:29
@kriskowal kriskowal force-pushed the gibson-2025-05-cache-map branch 3 times, most recently from da41c9b to d916f3b Compare June 3, 2025 01:32
Copy link
Member

@boneskull boneskull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothing blocking. I care most about the macro type 😜

"name": "@endo/cache-map",
"version": "1.0.0",
"description": "bounded-size caches having WeakMap-compatible methods",
"keywords": [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might wanna add smth here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to ideas.

Copy link
Member

@boneskull boneskull Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"cache", "lru", "weakmap", etc. 😄

"files": [
"test/**/*.test.*"
],
"timeout": "2m"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this is probably the default in skel, but I imagine this could be significantly reduced 😄

Comment on lines +6 to +15
/**
* @param {import('ava').ExecutionContext} t
* @param {object} config
* @param {any} [config.makeMap]
* @param {string} config.expectTag
* @param {[unknown, unknown, unknown]} config.keys
* @param {unknown[]} [config.badKeys]
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a docstring on the function definition on L15, not on the macro itself.

suggestions:

  • Might be nicer to just use any instead of unknown to avoid extra type assertions
  • Use @import {x} from 'y' instead of import(y).x
  • ExecutionContext is generic on a context object, so you might want to use @template [T=unknown] / ExecutionContext<T>
  • There are a lot of assertions here; consider using t.plan()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took the middle two bullet-point suggestions, but moving the docstring produced a lot of errors that I'd rather not deal with.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look after merge


/** @type {WeakMapAPI<K, CacheMapCell<K, V>>} */
const keyToCell = makeMap();
// @ts-expect-error this sentinel head is special
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make a type for it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not; this is a singular exception.

@kriskowal kriskowal force-pushed the gibson-2025-05-cache-map branch from d916f3b to 3403bee Compare June 3, 2025 01:51
@gibson042 gibson042 force-pushed the gibson-2025-05-cache-map branch from 3403bee to 0dc492e Compare June 6, 2025 05:34
@gibson042
Copy link
Member Author

@kriskowal this should again be ready to merge, possibly pending re-review from @boneskull.

Copy link
Member

@boneskull boneskull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gibson042 LGTM

@gibson042 gibson042 force-pushed the gibson-2025-05-cache-map branch from adf8a20 to 9e3fa1c Compare July 11, 2025 17:27
@gibson042 gibson042 merged commit 97a538e into master Jul 11, 2025
29 of 30 checks passed
@gibson042 gibson042 deleted the gibson-2025-05-cache-map branch July 11, 2025 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants