-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Perfomance: reuse compiled format templates #2030
Comments
Maybe LRU-caching Is this worth an additional dependency? Otherwise, applying |
Yep, caching is one option. I don't think caching But it's worth also considering explicit reuse. That is, in commands like
|
I did some further testing yesterday, times for a
before modifications. Explicit reuseBasically does the same as @sampsyo's patch above (which I think did not have the intended effect, though, as it compiles the empty template diff --git a/beets/library.py b/beets/library.py
index 3d18a1b..a531a71 100644
--- a/beets/library.py
+++ b/beets/library.py
@@ -323,6 +323,16 @@ class LibModel(dbcore.Model):
_format_config_key = None
"""Config key that specifies how an instance should be formatted.
"""
+ _format_template = None
+ """Caches the default template specified by `_format_config_key`.
+ """
+
+ @classmethod
+ def _get_format_template(cls):
+ if not cls._format_template:
+ cls._format_template = Template(
+ beets.config[cls._format_config_key].as_str())
+ return cls._format_template
def _template_funcs(self):
funcs = DefaultTemplateFunctions(self, self._db).functions()
@@ -343,7 +353,7 @@ class LibModel(dbcore.Model):
def __format__(self, spec):
if not spec:
- spec = beets.config[self._format_config_key].as_str()
+ spec = self._get_format_template()
result = self.evaluate_template(spec)
if isinstance(spec, bytes):
# if spec is a byte string then we must return a one as well
diff --git a/beets/ui/commands.py b/beets/ui/commands.py
index 6ea9872..b78a121 100644
--- a/beets/ui/commands.py
+++ b/beets/ui/commands.py
@@ -35,6 +35,7 @@ from beets import plugins
from beets import importer
from beets import util
from beets.util import syspath, normpath, ancestry, displayable_path
+from beets.util.functemplate import Template
from beets import library
from beets import config
from beets import logging
@@ -1059,6 +1060,11 @@ def list_items(lib, query, album, fmt=u''):
"""Print out items in lib matching query. If album, then search for
albums instead of single items.
"""
+ if fmt:
+ # cache fmt if it's non-empty (i.e. not the default but user-provided)
+ # doing this for fmt==u'' would overwrite the default format
+ fmt = Template(fmt)
+
if album:
for album in lib.albums(query):
ui.print_(format(album, fmt))
Cachingdiff --git a/beets/util/functemplate.py b/beets/util/functemplate.py
index cdccc49..ab9b98d 100644
--- a/beets/util/functemplate.py
+++ b/beets/util/functemplate.py
@@ -505,11 +505,26 @@ def _parse(template):
class Template(object):
"""A string template, including text, Symbols, and Calls.
"""
+
+ CACHE = dict()
+
+ def __new__(cls, template):
+ if template in cls.CACHE:
+ return cls.CACHE[template]
+ else:
+ return super(Template, cls).__new__(cls, template)
+
def __init__(self, template):
- self.expr = _parse(template)
- self.original = template
- if six.PY2: # FIXME Compiler is broken on Python 3.
- self.compiled = self.translate()
+ # Will also be called when __new__ returns a cached instance
+ if template not in self.CACHE:
+ # TODO: Is it necessary to make this a lru cache or is the number
+ # of templates low enough for all of commands to remember them all?
+ self.CACHE[template] = self
+
+ self.expr = _parse(template)
+ self.original = template
+ if six.PY2: # FIXME Compiler is broken on Python 3.
+ self.compiled = self.translate()
def __eq__(self, other):
return self.original == other.original
ResultSo there's really only this one Template causing problems in the Or we could remove the default template from |
Great! Thanks for confirming the performance with both strategies is about the same. We should definitely do one of these, but I don't have a strong leaning either way.
|
The question arose recently whether our funky AST compiler in the
functemplate
module is actually worth it. I wrote this compiler some time ago hoping for a dramatic speedup inbeet list
time, but I don't think I ever documented evidence either way about the speedup, and I had a vague inkling that it wasn't having the effect we wanted. Since it's giving @jrobeson trouble with the Python 3 port, I decided to investigate whether the code was worth keeping at all.TL;DR: The compiler can yield a 1.6x speedup on my machine! But we're currently using it in a way that negates all the benefits.
I used
time beet ls > /dev/null
to measure the time it takes to list all the items in my 15265-track library on my really old Core 2 Duo server. The time was 41.5 ± 0.3 seconds (mean and standard error over 4 runs). Not great.Then I tried disabling the compiler (the
functemplate
module also includes a straightforward interpreter). Shockingly, the time actually got slightly better: 37.8 ± 0.3 seconds.I investigated and found that, as you might have guessed, the code is written to invoke the compiler every time---so instead of compiling the template once, beets was compiling it 15 thousand times. 😢
I patched the
list
command as a stopgap to use the compiler correctly:and got much better time: 25.5 ± 0.2 seconds. Hot dang!
So the conclusions are:
list
command if not for all commands that use the standard formats from the config file.Here are the raw timings:
The text was updated successfully, but these errors were encountered: