Skip to content

Commit 0861604

Browse files
committed
Improve caching
Refs #238
1 parent c361ba5 commit 0861604

File tree

3 files changed

+81
-26
lines changed

3 files changed

+81
-26
lines changed

klaus/repo.py

+77-23
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@
1313
from klaus.diff import render_diff
1414

1515

16+
NOT_SET = '__not_set__'
17+
18+
19+
def cached_call(key, validator, producer, _cache={}):
20+
data, old_validator = _cache.get(key, (None, NOT_SET))
21+
if old_validator != validator:
22+
data = producer()
23+
_cache[key] = (data, validator)
24+
return data
25+
26+
1627
class FancyRepo(dulwich.repo.Repo):
1728
"""A wrapper around Dulwich's Repo that adds some helper methods."""
1829
# TODO: factor out stuff into dulwich
@@ -31,30 +42,33 @@ def name(self):
3142

3243
def get_last_updated_at(self):
3344
"""Get datetime of last commit to this repository."""
34-
def _get_last_updated_at():
35-
refs = []
36-
for ref_hash in self.get_refs().values():
37-
try:
38-
refs.append(self[ref_hash])
39-
except KeyError:
40-
# Whoops. The ref points at a non-existant object
41-
pass
42-
refs.sort(key=lambda obj:getattr(obj, 'commit_time', float('-inf')),
43-
reverse=True)
44-
for ref in refs:
45-
# Find the latest ref that has a commit_time; tags do not
46-
# have a commit time
47-
if hasattr(ref, "commit_time"):
48-
return ref.commit_time
49-
return None
50-
5145
# Cache result to speed up repo_list.html template.
52-
# If self.refs.keys() as changed, we should invalidate the cache.
53-
cache_key = self.refs.keys()
54-
if cache_key != getattr(self, '_last_updated_at_cache_key', None):
55-
self._last_updated_at_cache_retval = _get_last_updated_at()
56-
self._last_updated_at_cache_key = cache_key
57-
return self._last_updated_at_cache_retval
46+
# If self.get_refs() has changed, we should invalidate the cache.
47+
all_refs = self.get_refs()
48+
return cached_call(
49+
key=(id(self), 'get_last_updated_at'),
50+
validator=all_refs,
51+
producer=lambda: self._get_last_updated_at(all_refs)
52+
)
53+
54+
def _get_last_updated_at(self, all_refs):
55+
resolveable_refs = []
56+
for ref_hash in all_refs:
57+
try:
58+
resolveable_refs.append(self[ref_hash])
59+
except KeyError:
60+
# Whoops. The ref points at a non-existant object
61+
pass
62+
resolveable_refs.sort(
63+
key=lambda obj:getattr(obj, 'commit_time', float('-inf')),
64+
reverse=True
65+
)
66+
for ref in resolveable_refs:
67+
# Find the latest ref that has a commit_time; tags do not
68+
# have a commit time
69+
if hasattr(ref, "commit_time"):
70+
return ref.commit_time
71+
return None
5872

5973
@property
6074
def cloneurl(self):
@@ -72,6 +86,21 @@ def get_description(self):
7286
"""Like Dulwich's `get_description`, but returns None if the file
7387
contains Git's default text "Unnamed repository[...]".
7488
"""
89+
# Cache result to speed up repo_list.html template.
90+
# If description file mtime has changed, we should invalidate the cache.
91+
description_file = os.path.join(self._controldir, 'description')
92+
try:
93+
description_mtime = os.stat(os.path.join(self._controldir, 'description')).st_mtime
94+
except OSError:
95+
description_mtime = None
96+
97+
return cached_call(
98+
key=(id(self), 'get_description'),
99+
validator=description_mtime,
100+
producer=self._get_description
101+
)
102+
103+
def _get_description(self):
75104
description = super(FancyRepo, self).get_description()
76105
if description:
77106
description = force_unicode(description)
@@ -275,3 +304,28 @@ def raw_commit_diff(self, commit):
275304
bytesio = io.BytesIO()
276305
dulwich.patch.write_tree_diff(bytesio, self.object_store, parent_tree, commit.tree)
277306
return bytesio.getvalue()
307+
308+
def freeze(self):
309+
return FrozenFancyRepo(self)
310+
311+
312+
class FrozenFancyRepo(object):
313+
"""A special version of FancyRepo that assumes the underlying Git
314+
repository does not change. Used for performance optimizations.
315+
"""
316+
def __init__(self, repo):
317+
self.__repo = repo
318+
self.__last_updated_at = NOT_SET
319+
320+
def __setattr__(self, name, value):
321+
if not name.startswith('_FrozenFancyRepo__'):
322+
raise TypeError("Can't set %s attribute on FrozenFancyRepo" % name)
323+
super(FrozenFancyRepo, self).__setattr__(name, value)
324+
325+
def __getattr__(self, name):
326+
return getattr(self.__repo, name)
327+
328+
def fast_get_last_updated_at(self):
329+
if self.__last_updated_at is NOT_SET:
330+
self.__last_updated_at = self.__repo.get_last_updated_at()
331+
return self.__last_updated_at

klaus/templates/repo_list.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ <h2>
1212
</h2>
1313
<ul class=repolist>
1414
{% for repo in repos %}
15-
{% set last_updated_at = repo.get_last_updated_at() %}
15+
{% set last_updated_at = repo.fast_get_last_updated_at() %}
1616
{% set description = repo.get_description() %}
1717
<li>
1818
<a

klaus/views.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ def repo_list():
3535
if 'by-name' in request.args:
3636
sort_key = lambda repo: repo.name
3737
else:
38-
sort_key = lambda repo: (-(repo.get_last_updated_at() or -1), repo.name)
39-
repos = sorted(current_app.repos.values(), key=sort_key)
38+
sort_key = lambda repo: (-(repo.fast_get_last_updated_at() or -1), repo.name)
39+
repos = sorted([repo.freeze() for repo in current_app.repos.values()],
40+
key=sort_key)
4041
return render_template('repo_list.html', repos=repos, base_href=None)
4142

4243

0 commit comments

Comments
 (0)