Improved cache control, fewer memory leaks#689
Conversation
1c22f12 to
ba83a5b
Compare
elalish
left a comment
There was a problem hiding this comment.
Thanks for doing this! I have a few questions below...
index.html
Outdated
| <p>This static, writable property sets <model-viewer>'s internal | ||
| cache size for GLTF models. Note that any number of models may be cached | ||
| at any given time. This number just sets the maximum number of models | ||
| that can be cached even if they are not being used by a <model-viewer> |
There was a problem hiding this comment.
Reading this, I'm uncertain if this number has units of Mb or # of glTFs.
| suiteTeardown(() => { | ||
| // Reset the renderer once at the end of this spec, to clear out all | ||
| // of the heavy cached image buffers: | ||
| ModelViewerElementBase[$resetRenderer](); |
There was a problem hiding this comment.
Do we really want to destroy the gl context here? Would it be adequate to dispose of and recreate TextureUtils?
There was a problem hiding this comment.
As we have discussed before, the GL context isn't especially important. What this does do is clear out a few internal Three.js caches. It's possible that we could dispose of the artifacts held in these caches more discretely, but I'm not sure how to go about doing that comprehensively.
This is only ever done once in the entire test suite (after environmennt-spec.ts is run to completion), so it shouldn't be a huge cost 🤷♂
| suiteTeardown(() => { | ||
| // Ensure we free up memory from loading large environment maps: | ||
| Cache.clear(); | ||
| renderer.dispose(); |
There was a problem hiding this comment.
What does disposing of the renderer do that Cache.clear() doesn't do already? Every time we recreate a renderer we get a lot of console spew that makes reading the test output harder.
There was a problem hiding this comment.
See my comment above, I linked to the relevant lines. It's a hatchet, not a scalpel, for sure, but profiles bare out that there is a fair amount of memory (megabytes worth) being held by references in the renderer's internal cache.
| this[$evictionPolicy].retain(url); | ||
|
|
||
| Object.defineProperty(model, $releaseFromCache, { | ||
| value: (() => { |
There was a problem hiding this comment.
This doesn't seem like our usual style; what's the purpose of Object.defineProperty here?
There was a problem hiding this comment.
It's the only way I could get tsc to stop complaining about me decorating this object.
It might be possible to change the type of the identifier and get a cleaner result. I'll try that out.
| const cloneSkinnedMeshes: SkinnedMeshMap = {}; | ||
|
|
||
| if (hasScene && hasSkinnedMeshes) { | ||
| if (hasScene) { |
There was a problem hiding this comment.
Can we combine this with the same if statement above?
There was a problem hiding this comment.
Yeah, good call, I think we can merge the conditions.
src/three-components/TextureUtils.ts
Outdated
| environmentMap.dispose(); | ||
| } | ||
| } catch (e) { | ||
| // Suppress errors |
There was a problem hiding this comment.
What errors are we getting?
There was a problem hiding this comment.
If a promise is rejected (for example. say a file fails to load), the promise is rejected for all time. If you later await that promise, it will throw an error.
src/utilities.ts
Outdated
| export type Constructor<T = object> = { | ||
| new (...args: any[]): T, | ||
| prototype: T | ||
| prototype: T, |
|
|
||
| if (retainerCount === 0) { | ||
| this[$cache].delete(key); | ||
| this[$recentlyUsed].splice(i, 1); |
There was a problem hiding this comment.
I don't quite follow this splice. Why are we putting a new entry into $recentlyUsed when we just deleted that entry from the cache?
There was a problem hiding this comment.
The signature for JavaScript splice is:
var arrDeletedItems = array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
Added items start at the third argument. So, we are only removing things here, not adding anything.
There was a problem hiding this comment.
Ah, that's what I missed when I read that definition; I thought these splices were adding elements, but now I see they're removing them. I thought elements weren't being removed at all, hence my other comment. Thanks!
| this[$recentlyUsed].splice(recentlyUsedIndex, 1); | ||
| } | ||
|
|
||
| this[$recentlyUsed].unshift(key); |
There was a problem hiding this comment.
These two lines seem to be vaguely duplicate? Also, does the $recentlyUsed array grow without bound? Could we just make $recentlyUsed the same length as $retainerCount and just swap elements to the beginning? If only JS had a priority queue...
There was a problem hiding this comment.
The JavaScript Array is unbounded and can grow indefinitely, runtime permitting. One cannot limit the size of an Array. JavaScript Array is more like a map than an actual array, where the indices are the keys. You can overload its push / pop etc. methods, but you cannot overload the index operator, and that allows you to insert to arbitrary indices (e.g., this[$recentlyUsed][49283] = true will totally do a thing here, and grow the reported length accordingly).
[$retainerCount] being a Map, it is more expensive to count the keys than to just do the book keeping directly as we retain new keys. So, surgically splicing elements out of the Array is probably the best we can do.
| disconnectedCallback() { | ||
| super.disconnectedCallback && super.disconnectedCallback(); | ||
| // It's important to remove the polyfill event listener when we | ||
| // disconnect, otherwise we will leak the whole element via window: |
95edcdd to
b52e78b
Compare
99f520f to
cd371ad
Compare
|
This change has been rebased onto master |
This change proposes a first step towards better cache management and introduces limited cache control as a public API. Highlights include:
CacheEvictionPolicyhas been introduced, to offer a consistent model for purging items from caches<model-viewer>elements via our integration with:focus-visiblepolyfillCachingGLTFLoaderhas been integrated withCacheEvictionPolicyso that we no longer leak all of our models by designcloneGltfhas been fixed, which was leading us to incorrectly clone materialsmodelCacheSizehas been added, to enable imperative user control of GLTF caching behavior<model-viewer>instances and sets the "eviction threshold" forCachingGLTFLoadercustomElements.get('model-viewer').modelCacheSize = 10;.Future work:
Fixes #491