]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
perf: resolver caching (#8435)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Tue, 16 Feb 2021 13:43:11 +0000 (15:43 +0200)
committerGitHub <noreply@github.com>
Tue, 16 Feb 2021 13:43:11 +0000 (15:43 +0200)
* perf: resolver caching

* Fix plugin caching

* resetCache

* Reduce duplication, cache only by keys cached

* Reduce lines

* reduce more lines

* Double plural, noop-caching of chart level options

src/core/core.config.js
src/core/core.controller.js
src/core/core.datasetController.js
src/core/core.plugins.js

index a8798965ccce32eb0418e15a03f7c990c437f649..a0562faad5aea3a35e72048c283d6be330799300 100644 (file)
@@ -103,9 +103,31 @@ function initConfig(config) {
   return config;
 }
 
+const keyCache = new Map();
+const keysCached = new Set();
+
+function cachedKeys(cacheKey, generate) {
+  let keys = keyCache.get(cacheKey);
+  if (!keys) {
+    keys = generate();
+    keyCache.set(cacheKey, keys);
+    keysCached.add(keys);
+  }
+  return keys;
+}
+
+const addIfFound = (set, obj, key) => {
+  const opts = resolveObjectKey(obj, key);
+  if (opts !== undefined) {
+    set.add(opts);
+  }
+};
+
 export default class Config {
   constructor(config) {
     this._config = initConfig(config);
+    this._scopeCache = new Map();
+    this._resolverCache = new Map();
   }
 
   get type() {
@@ -134,6 +156,8 @@ export default class Config {
 
   update(options) {
     const config = this._config;
+    this._scopeCache.clear();
+    this._resolverCache.clear();
     config.options = initOptions(config, options);
   }
 
@@ -144,7 +168,8 @@ export default class Config {
         * @return {string[]}
         */
   datasetScopeKeys(datasetType) {
-    return [`datasets.${datasetType}`, `controllers.${datasetType}.datasets`, ''];
+    return cachedKeys(datasetType,
+      () => [`datasets.${datasetType}`, `controllers.${datasetType}.datasets`, '']);
   }
 
   /**
@@ -154,7 +179,12 @@ export default class Config {
         * @return {string[]}
         */
   datasetAnimationScopeKeys(datasetType) {
-    return [`datasets.${datasetType}.animation`, `controllers.${datasetType}.datasets.animation`, 'animation'];
+    return cachedKeys(`${datasetType}.animation`,
+      () => [
+        `datasets.${datasetType}.animation`,
+        `controllers.${datasetType}.datasets.animation`,
+        'animation'
+      ]);
   }
 
   /**
@@ -166,51 +196,77 @@ export default class Config {
         * @return {string[]}
         */
   datasetElementScopeKeys(datasetType, elementType) {
-    return [
-      `datasets.${datasetType}`,
-      `controllers.${datasetType}.datasets`,
-      `controllers.${datasetType}.elements.${elementType}`,
-      `elements.${elementType}`,
-      ''
-    ];
+    return cachedKeys(`${datasetType}-${elementType}`,
+      () => [
+        `datasets.${datasetType}`,
+        `controllers.${datasetType}.datasets`,
+        `controllers.${datasetType}.elements.${elementType}`,
+        `elements.${elementType}`,
+        ''
+      ]);
+  }
+
+  /**
+   * Returns the options scope keys for resolving plugin options.
+   * @param {{id: string, additionalOptionScopes?: string[]}} plugin
+        * @return {string[]}
+   */
+  pluginScopeKeys(plugin) {
+    const id = plugin.id;
+    const type = this.type;
+    return cachedKeys(`${type}-plugin-${id}`,
+      () => [
+        `controllers.${type}.plugins.${id}`,
+        `plugins.${id}`,
+        ...plugin.additionalOptionScopes || [],
+      ]);
   }
 
   /**
         * Resolves the objects from options and defaults for option value resolution.
         * @param {object} mainScope - The main scope object for options
         * @param {string[]} scopeKeys - The keys in resolution order
+   * @param {boolean} [resetCache] - reset the cache for this mainScope
         */
-  getOptionScopes(mainScope = {}, scopeKeys) {
-    const options = this.options;
-    const scopes = new Set([mainScope]);
-
-    const addIfFound = (obj, key) => {
-      const opts = resolveObjectKey(obj, key);
-      if (opts !== undefined) {
-        scopes.add(opts);
-      }
-    };
+  getOptionScopes(mainScope, scopeKeys, resetCache) {
+    let cache = this._scopeCache.get(mainScope);
+    if (!cache || resetCache) {
+      cache = new Map();
+      this._scopeCache.set(mainScope, cache);
+    }
+    const cached = cache.get(scopeKeys);
+    if (cached) {
+      return cached;
+    }
 
-    scopeKeys.forEach(key => addIfFound(mainScope, key));
-    scopeKeys.forEach(key => addIfFound(options, key));
-    scopeKeys.forEach(key => addIfFound(defaults, key));
+    const scopes = new Set();
 
-    const descriptors = defaults.descriptors;
-    scopeKeys.forEach(key => addIfFound(descriptors, key));
+    if (mainScope) {
+      scopes.add(mainScope);
+      scopeKeys.forEach(key => addIfFound(scopes, mainScope, key));
+    }
+    scopeKeys.forEach(key => addIfFound(scopes, this.options, key));
+    scopeKeys.forEach(key => addIfFound(scopes, defaults, key));
+    scopeKeys.forEach(key => addIfFound(scopes, defaults.descriptors, key));
 
-    return [...scopes];
+    const array = [...scopes];
+    if (keysCached.has(scopeKeys)) {
+      cache.set(scopeKeys, array);
+    }
+    return array;
   }
 
   /**
         * Returns the option scopes for resolving chart options
         * @return {object[]}
         */
-  chartOptionsScopes() {
+  chartOptionScopes() {
     return [
       this.options,
       defaults.controllers[this.type] || {},
       {type: this.type},
-      defaults, defaults.descriptors
+      defaults,
+      defaults.descriptors
     ];
   }
 
@@ -222,19 +278,15 @@ export default class Config {
         * @return {object}
         */
   resolveNamedOptions(scopes, names, context, prefixes = ['']) {
-    const result = {};
-    const resolver = _createResolver(scopes, prefixes);
-    let options;
+    const result = {$shared: true};
+    const {resolver, subPrefixes} = getResolver(this._resolverCache, scopes, prefixes);
+    let options = resolver;
     if (needContext(resolver, names)) {
       result.$shared = false;
       context = isFunction(context) ? context() : context;
-      // subResolver os passed to scriptable options. It should not resolve to hover options.
-      const subPrefixes = prefixes.filter(p => !p.toLowerCase().includes('hover'));
+      // subResolver is passed to scriptable options. It should not resolve to hover options.
       const subResolver = this.createResolver(scopes, context, subPrefixes);
       options = _attachContext(resolver, context, subResolver);
-    } else {
-      result.$shared = true;
-      options = resolver;
     }
 
     for (const prop of names) {
@@ -248,11 +300,31 @@ export default class Config {
         * @param {function|object} context
         */
   createResolver(scopes, context, prefixes = ['']) {
+    const cached = getResolver(this._resolverCache, scopes, prefixes);
+    return context && cached.needContext
+      ? _attachContext(cached.resolver, isFunction(context) ? context() : context)
+      : cached.resolver;
+  }
+}
+
+function getResolver(resolverCache, scopes, prefixes) {
+  let cache = resolverCache.get(scopes);
+  if (!cache) {
+    cache = new Map();
+    resolverCache.set(scopes, cache);
+  }
+  const cacheKey = prefixes.join();
+  let cached = cache.get(cacheKey);
+  if (!cached) {
     const resolver = _createResolver(scopes, prefixes);
-    return context && needContext(resolver, Object.getOwnPropertyNames(resolver))
-      ? _attachContext(resolver, isFunction(context) ? context() : context)
-      : resolver;
+    cached = {
+      resolver,
+      subPrefixes: prefixes.filter(p => !p.toLowerCase().includes('hover')),
+      needContext: needContext(resolver, Object.getOwnPropertyNames(resolver))
+    };
+    cache.set(cacheKey, cached);
   }
+  return cached;
 }
 
 function needContext(proxy, names) {
index b30ff926d6fe6b4388b72a7d9c0b3aafbe4907aa..b84b22c33a60834115f2ac80ee8bc3ada2bb0dc7 100644 (file)
@@ -88,7 +88,7 @@ class Chart {
       );
     }
 
-    const options = config.createResolver(config.chartOptionsScopes(), me.getContext());
+    const options = config.createResolver(config.chartOptionScopes(), me.getContext());
 
     this.platform = me._initializePlatform(initialCanvas, config);
 
@@ -440,7 +440,7 @@ class Chart {
     const config = me.config;
 
     config.update(config.options);
-    me._options = config.createResolver(config.chartOptionsScopes(), me.getContext());
+    me._options = config.createResolver(config.chartOptionScopes(), me.getContext());
 
     each(me.scales, (scale) => {
       layouts.removeBox(me, scale);
index 9243352961cca31c51784dd8d444b7f886c2c4da..6f5a6c7faeaa0bcd2e52f13a12af9d39a497d986 100644 (file)
@@ -362,7 +362,7 @@ export default class DatasetController {
     const me = this;
     const config = me.chart.config;
     const scopeKeys = config.datasetScopeKeys(me._type);
-    const scopes = config.getOptionScopes(me.getDataset(), scopeKeys);
+    const scopes = config.getOptionScopes(me.getDataset(), scopeKeys, true);
     me.options = config.createResolver(scopes, me.getContext());
     me._parsing = me.options.parsing;
   }
index 847ba4a765c7a41a306ca88f21b2808926869c7f..5655ba3a934306dbeae351de6d52e2138d87fff1 100644 (file)
@@ -164,12 +164,7 @@ function createDescriptors(chart, plugins, options, all) {
  * @param {*} context
  */
 function pluginOpts(config, plugin, opts, context) {
-  const id = plugin.id;
-  const keys = [
-    `controllers.${config.type}.plugins.${id}`,
-    `plugins.${id}`,
-    ...plugin.additionalOptionScopes || []
-  ];
-  const scopes = config.getOptionScopes(opts || {}, keys);
+  const keys = config.pluginScopeKeys(plugin);
+  const scopes = config.getOptionScopes(opts, keys);
   return config.createResolver(scopes, context);
 }