lib/models/per-bundle-addon-cache/index.js:46
PerBundleAddonCache
For large applications with many addons (and many instances of each, resulting in potentially many millions of addon instances during a build), the build can become very, very slow (tens of minutes) partially due to the sheer number of addon instances. The PerBundleAddonCache deals with this slowness by doing 3 things:
(1) Making only a single copy of each of certain addons and their dependent addons
(2) Replacing any other instances of those addons with Proxy copies to the single instance
(3) Having the Proxies return an empty array for their dependent addons, rather
than proxying to the contents of the single addon instance. This gives up the
ability of the Proxies to traverse downward into their child addons,
something that many addons do not do anyway, for the huge reduction in duplications
of those child addons. For applications that enable ember-engines
dedupe logic,
that logic is stateful, and having the Proxies allow access to the child addons array
just breaks everything, because that logic will try multiple times to remove items
it thinks are duplicated, messing up the single copy of the child addon array.
See the explanation of the dedupe logic in
{@link https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/utils/deeply-non-duplicated-addon.js}
What follows are the more technical details of how the PerBundleAddonCache implements the above 3 behaviors.
This class supports per-bundle-host (bundle host = project or lazy engine)
caching of addon instances. During addon initialization we cannot add a
cache to each bundle host object AFTER it is instantiated because running the
addon constructor ultimately causes Addon class setupRegistry
code to
run which instantiates child addons, which need the cache to already be
in place for the parent bundle host.
We handle this by providing a global cache that exists independent of the
bundle host objects. That is this object.
There are a number of "behaviors" being implemented by this object and its contents. They are: (1) Any addon that is a lazy engine has only a single real instance per project - all other references to the lazy engine are to be proxies. These lazy engines are compared by name, not by packageInfo.realPath. (2) Any addon that is not a lazy engine, there is only a single real instance of the addon per "bundle host" (i.e. lazy engine or project). (3) An optimization - any addon that is in a lazy engine but that is also in bundled by its LCA host - the single instance is the one bundled by this host. All other instances (in any lazy engine) are proxies.
NOTE: the optimization is only enabled if the environment variable that controls
ember-engines
transitive deduplication (process.env.EMBER_ENGINES_ADDON_DEDUPE)
is set to a truthy value. For more info, see:
https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/engine-addon.js#L396
Method Summary
Public Methods | |
---|---|
public |
allowCachingPerBundle(addonEntryPointModule): Boolean
The default implementation here is to indicate if the original addon entry point has the |
public |
bundleHostOwnsInstance((Object}, addonPkgInfo): Boolean
An optimization we support from lazy engines is the following: |
public |
createAddonCacheEntry(addonInstance, addonRealPath): Object
Create a cache entry object for a given (non-bundle-host) addon to put into an addon cache. |
public |
createBundleHostCacheEntry(bundleHostPkgInfo): Object
Creates a cache entry for the bundleHostCache. Because we want to use the same sort of proxy for both bundle hosts and for 'regular' addon instances (though their cache entries have slightly different structures) we'll use the Symbol from getAddonProxy. |
public |
findBundleHost(addonParent, addonPkgInfo): Object
Given a parent object of a potential addon (another addon or the project), go up the 'parent' chain to find the potential addon's bundle host object (i.e. lazy engine or project.) Because Projects are always bundle hosts, this should always pass, but we'll throw if somehow it doesn't work. |
public |
Called from PackageInfo.getAddonInstance(), return an instance of the requested addon or a Proxy, based on the type of addon and its bundle host. |
public |
getAddonProxy(targetCacheEntry, parent):
Returns a proxy to a target with specific handling for the |
public |
resolvePerBundleAddonCacheUtil(project): AllowCachingPerBundle: Function
Resolves the perBundleAddonCacheUtil; this prefers the custom provided version by the consuming application, and defaults to an internal implementation here. |
public |
validateCacheKey(realAddonInstance, treeType, newCacheKey)
Validates that a new cache key for a given tree type matches the previous cache key for the same tree type. To opt-in to bundle addon caching for a given addon it's assumed that it returns stable cache keys; specifically this is because the interplay between bundle addon caching and |
Public Methods
lib/models/per-bundle-addon-cache/index.js:116
public allowCachingPerBundle(addonEntryPointModule): Boolean
The default implementation here is to indicate if the original addon entry point has
the allowCachingPerBundle
flag set either on itself or on its prototype.
If a consuming application specifies a relative path to a custom utility via the
ember-addon.perBundleAddonCacheUtil
configuration, we prefer the custom implementation
provided by the consumer.
Return:
true if the given constructor function or class supports caching per bundle, false otherwise
lib/models/per-bundle-addon-cache/index.js:197
public bundleHostOwnsInstance((Object}, addonPkgInfo): Boolean
An optimization we support from lazy engines is the following:
If an addon instance is supposed to be bundled with a particular lazy engine, and
same addon is also to be bundled by a common LCA host, prefer the one bundled by the
host (since it's ultimately going to be deduped later by ember-engines
).
NOTE: this only applies if this.engineAddonTransitiveDedupeEnabled is truthy. If it is not, the bundle host always "owns" the addon instance.
If deduping is enabled and the LCA host also depends on the same addon, the lazy-engine instances of the addon will all be proxies to the one in the LCA host. This function indicates whether the bundle host passed in (either the project or a lazy engine) is really the bundle host to "own" the new addon.
Parameters:
Name | Type | Attribute | Description |
---|---|---|---|
(Object} | Object |
|
bundleHost the project or lazy engine that is trying to "own" the new addon instance specified by addonPkgInfo |
addonPkgInfo | PackageInfo |
|
the PackageInfo of the potential new addon instance |
Return:
true if the bundle host is to "own" the instance, false otherwise.
lib/models/per-bundle-addon-cache/index.js:145
public createAddonCacheEntry(addonInstance, addonRealPath): Object
Create a cache entry object for a given (non-bundle-host) addon to put into an addon cache.
Parameters:
Name | Type | Attribute | Description |
---|---|---|---|
addonInstance | Addon |
|
the addon instance to cache |
addonRealPath | String |
|
the addon's pkgInfo.realPath |
Return:
an object in the form of an addon-cache entry
lib/models/per-bundle-addon-cache/index.js:132
public createBundleHostCacheEntry(bundleHostPkgInfo): Object
Creates a cache entry for the bundleHostCache. Because we want to use the same sort of proxy for both bundle hosts and for 'regular' addon instances (though their cache entries have slightly different structures) we'll use the Symbol from getAddonProxy.
Parameters:
Name | Type | Attribute | Description |
---|---|---|---|
bundleHostPkgInfo | PackageInfo |
|
bundle host's pkgInfo.realPath |
Return:
an object in the form of a bundle-host cache entry
lib/models/per-bundle-addon-cache/index.js:158
public findBundleHost(addonParent, addonPkgInfo): Object
Given a parent object of a potential addon (another addon or the project), go up the 'parent' chain to find the potential addon's bundle host object (i.e. lazy engine or project.) Because Projects are always bundle hosts, this should always pass, but we'll throw if somehow it doesn't work.
Parameters:
Name | Type | Attribute | Description |
---|---|---|---|
addonParent | Project | Addon |
|
the direct parent object of a (potential or real) addon. |
addonPkgInfo | PackageInfo |
|
the PackageInfo for an addon being instantiated. This is only used for information if an error is going to be thrown. |
Return:
the object in the 'parent' chain that is a bundle host.
Throws:
if there is not bundle host
lib/models/per-bundle-addon-cache/index.js:247
public getAddonInstance(parent, addonPkgInfo): Addon | Proxy
Called from PackageInfo.getAddonInstance(), return an instance of the requested addon or a Proxy, based on the type of addon and its bundle host.
Parameters:
Name | Type | Attribute | Description |
---|---|---|---|
parent | Addon | Project |
|
the parent Addon or Project this addon instance is a child of. |
addonPkgInfo |
|
the PackageInfo for the addon being created. |
Return:
An addon instance (for the first copy of the addon) or a Proxy.
An addon that is a lazy engine will only ever have a single copy in the cache.
An addon that is not will have 1 copy per bundle host (Project or lazy engine),
except if it is an addon that's also owned by a given LCA host and transitive
dedupe is enabled (engineAddonTransitiveDedupeEnabled
), in which case it will
only have a single copy in the project's addon cache.
lib/models/per-bundle-addon-cache/addon-proxy.js:42
public getAddonProxy(targetCacheEntry, parent):
Returns a proxy to a target with specific handling for the
parent
property, as well has to handle the app
property;
that is, the proxy should maintain correct local state in
closure scope for the app
property if it happens to be set
by ember-cli
. Other than parent
& app
, this function also
proxies almost everything to target[TARGET_INSTANCE] with a few exceptions: we trap & return
[]for
addons, and we don't return the original
included(it's already called on the "real" addon by
ember-cli`).
Note: the target is NOT the per-bundle cacheable instance of the addon. Rather, it is a cache entry POJO from PerBundleAddonCache.
Parameters:
Name | Type | Attribute | Description |
---|---|---|---|
targetCacheEntry | Object |
|
the PerBundleAddonCache cache entry we are to proxy. It has one interesting property, the real addon instance the proxy is forwarding calls to (that property is not globally exposed). |
parent | Object |
|
the parent object of the proxy being created (the same as the 'parent' property of a normal addon instance) |
Return:
Proxy
lib/models/per-bundle-addon-cache/index.js:17
public resolvePerBundleAddonCacheUtil(project): AllowCachingPerBundle: Function
Resolves the perBundleAddonCacheUtil; this prefers the custom provided version by the consuming application, and defaults to an internal implementation here.
Parameters:
Name | Type | Attribute | Description |
---|---|---|---|
project | Project |
|
Return:
}
lib/models/per-bundle-addon-cache/addon-proxy.js:7
public validateCacheKey(realAddonInstance, treeType, newCacheKey)
Validates that a new cache key for a given tree type matches the previous
cache key for the same tree type. To opt-in to bundle addon caching for
a given addon it's assumed that it returns stable cache keys; specifically
this is because the interplay between bundle addon caching and ember-engines
when transitive deduplication is enabled assumes stable cache keys, so we validate
for this case here.
Parameters:
Name | Type | Attribute | Description |
---|---|---|---|
realAddonInstance | Addon |
|
The real addon instance |
treeType | String |
|
|
newCacheKey | String |
|
Throws:
If the new cache key doesn't match the previous cache key