Merge lp:~seif/zeitgeist/add_cache_for_get_events into lp:zeitgeist/0.1
- add_cache_for_get_events
- Merge into 0.8-python
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~seif/zeitgeist/add_cache_for_get_events | ||||
Merge into: | lp:zeitgeist/0.1 | ||||
Diff against target: |
407 lines (+297/-8) 6 files modified
_zeitgeist/Makefile.am (+2/-1) _zeitgeist/engine/__init__.py (+7/-0) _zeitgeist/engine/main.py (+64/-6) _zeitgeist/lrucache.py (+125/-0) test/lrucache_test.py (+98/-0) zeitgeist/client.py (+1/-1) |
||||
To merge this branch: | bzr merge lp:~seif/zeitgeist/add_cache_for_get_events | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mikkel Kamstrup Erlandsen | Needs Information | ||
Review via email: mp+42327@code.launchpad.net |
This proposal has been superseded by a proposal from 2011-01-12.
Commit message
Description of the change
I added a cache to reduce the calls on get_events
basically i look at separate the cached ids from the ids handed to get_events
then i call the sql db to give me all uncached events then i sort them all and voila
the speed improvement is very nice
old DEBUG:zeitgeist
new DEBUG:zeitgeist
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
- 1644. By Seif Lotfy
-
add rewritten LRU cache with test cases
Seif Lotfy (seif) wrote : | # |
I rewrote the LRU cache since the old one gave me around 0.6 ms
and the new one 0.3 ms
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
Something is fishy in LRUCache.
Unless of course the deque is indexed internally - but that would surprise me. In this case my comments are moot.
Seif Lotfy (seif) wrote : | # |
I am not using deque :P
But I can push it
On Wed, Dec 1, 2010 at 4:46 PM, Mikkel Kamstrup Erlandsen <
<email address hidden>> wrote:
> Something is fishy in LRUCache.
> search for the id in self._id_seq by using the 'in' operator, and another
> linear search by using 'remove' afaik. So they become O(2N) in the worst
> case which is not a very desirable property for a cache ;-)
>
> Unless of course the deque is indexed internally - but that would surprise
> me. In this case my comments are moot.
> --
>
> https:/
> You are the owner of lp:~seif/zeitgeist/add_cache_for_get_events.
>
--
This is me doing some advertisement for my blog http://
Seif Lotfy (seif) wrote : | # |
scratch that i did not understand your comment properly...
I reduced the "in" statement by just using self._map.
On Wed, Dec 1, 2010 at 4:48 PM, Seif Lotfy <email address hidden> wrote:
> I am not using deque :P
> But I can push it
>
>
> On Wed, Dec 1, 2010 at 4:46 PM, Mikkel Kamstrup Erlandsen <
> <email address hidden>> wrote:
>
>> Something is fishy in LRUCache.
>> linear search for the id in self._id_seq by using the 'in' operator, and
>> another linear search by using 'remove' afaik. So they become O(2N) in the
>> worst case which is not a very desirable property for a cache ;-)
>>
>> Unless of course the deque is indexed internally - but that would surprise
>> me. In this case my comments are moot.
>> --
>>
>> https:/
>> You are the owner of lp:~seif/zeitgeist/add_cache_for_get_events.
>>
>
>
>
> --
> This is me doing some advertisement for my blog http://
>
--
This is me doing some advertisement for my blog http://
- 1645. By Seif Lotfy
-
reduced the 'in' statement by replacing it with self._map.
has_key( key) - 1646. By Seif Lotfy
-
made use of deque
- 1647. By Seif Lotfy
-
no need to use deque so replace popleft with del at position 0
Seif Lotfy (seif) wrote : | # |
Guys I reworked it.
If I recall properly I think Markus said its ok like this, since its not a linear search. I am not sure but please reconsider since it really brings a big improvement to the table.
Seif Lotfy (seif) wrote : | # |
OK I don't think Markus said its not a linear search.
I also asked in the python channel and
----
<seiflotfy> is it a linear search to find the item i want to remove
<papna> seiflotfy: Yes.
<seiflotfy> whoa
<papna> seiflotfy: Also, don't use list.remove
<papna> seiflotfy: Decribe your data.
<seiflotfy> i am implementing an lru cache
<seiflotfy> basically a list of ids in a list
<seiflotfy> then i remove a key when its used
<seiflotfy> and append it to the end of the list
<nosklo> seiflotfy: dict
<seiflotfy> yeah
<seiflotfy> i will sill have to go through the dict
<seiflotfy> ot change the value of each key
<seiflotfy> which is still a linear seach AFAIK
<papna> seiflotfy: Yeah, a list isn't appropriate for that.
* zobbo (~ian@88.211.55.77) has joined #python
<papna> seiflotfy: One approach is to keep a linked list and a dict.
<seiflotfy> papna, i did
<seiflotfy> but the list approach was faster
<papna> seiflotfy:
http://
<seiflotfy> papna, i can show u 2 code smaples
<papna> seiflotfy: For realistic n, sure.
-----
I implemented his proposal. And in real life it my list solution is 2x
faster at mimimum.
On Tue, Dec 7, 2010 at 3:37 PM, Seif Lotfy <email address hidden> wrote:
> Guys I reworked it.
> If I recall properly I think Markus said its ok like this, since its not a
> linear search. I am not sure but please reconsider since it really brings a
> big improvement to the table.
> --
>
> https:/
> You are the owner of lp:~seif/zeitgeist/add_cache_for_get_events.
>
--
This is me doing some advertisement for my blog http://
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
In lrucache.py:
__delitem__ must also update id_seq
Also can LRUCache maybe log a warning if you create it with maxsize > 1000 or something. This implementation scales very badly, although as you say, it may well be faster for small caches.
Lastly, have you actually benchmarked this with 1000 items in the cache? Because my guess says that that should be around the tipping point where the linear searches starts to make an impact...
Maybe reduce the cache size to 500 just to feel a little safer?
- 1648. By Seif Lotfy
-
added old lru cache
Seif Lotfy (seif) wrote : | # |
I did some testing an with a cache of 700 events my solution starts becoming
slower thus I removed it for a more consitent lru cache (the old one). This
removes 50% off the get_events
On Tue, Dec 7, 2010 at 8:01 PM, Mikkel Kamstrup Erlandsen <
<email address hidden>> wrote:
> Review: Needs Fixing
> In lrucache.py:
> __delitem__ must also update id_seq
>
> Also can LRUCache maybe log a warning if you create it with maxsize > 1000
> or something. This implementation scales very badly, although as you say, it
> may well be faster for small caches.
>
> Lastly, have you actually benchmarked this with 1000 items in the cache?
> Because my guess says that that should be around the tipping point where the
> linear searches starts to make an impact...
>
> Maybe reduce the cache size to 500 just to feel a little safer?
> --
>
> https:/
> You are the owner of lp:~seif/zeitgeist/add_cache_for_get_events.
>
--
This is me doing some advertisement for my blog http://
Markus Korn (thekorn) wrote : | # |
I don't think we want to cache events at all.
So far we have a simple policy, we are only caching objects we completely understand, where we know their size and how big (memory wise) they are (e.g mimetypes, interpretation, manifestation). We are not caching uris (because there can be an indefinite number of them), payload (because payload is unique for each event, and has an undefined max size), and so on.
For me there are three reasons why I don't want to cache events:
* we can't control the in-memory size of this cache, something like LRUCache(50) does not mean it's using less memory than LRUCache(500), simply because if only one event with a 5 GB (*) payload is in one cache the memory usage of the zg daemon will be HUGE.
* by adding an event cache we are indirectly caching objects like payload, uri, etc. we don't want to cache, for above mentioned reasons.
* I don't think we are able to find a performant Cache implementation for all possible clients. Just imaging we have two clients accessing the daemon, one which only gets a few events at a time (e.g. unity) and a one which is frequently getting *all* events (something like the GAJ histogram [not sure if this is still the case, but you should get my point ;)]) While any cache would do a good job for unity, only one GetEvents in GAJ will destroy/shuffle the complete cache in a way that the performance win for unity gets destroyed.
So I suggest two alternative solutions:
* encourage clients to maintain their own caches, and use FindEventIds() + GetEvents[(all ids whih are not in client's cache)] as much as possible. If this turns out to have performance issues, we should improve that
* if you really want a distributed cache, write an event-cache-daemon which only has one public API method "GetEvents" and which works like a cached proxy, similar to memcached
(*) random high number
Seif Lotfy (seif) wrote : | # |
It is easy to control the size of the cache if we don't cache events with a
payload.
Usually most applications are interested in the latest events. Ofcourse AJ
breaks that but still, u might be running 3 or 4 apps that are usually
interested in the latest activities.
AFAIK zeitgeist does not really free uri memory at all. So a cache is not an
issue here.
On Wed, Dec 8, 2010 at 9:24 AM, Markus Korn <email address hidden> wrote:
> I don't think we want to cache events at all.
> So far we have a simple policy, we are only caching objects we completely
> understand, where we know their size and how big (memory wise) they are (e.g
> mimetypes, interpretation, manifestation). We are not caching uris (because
> there can be an indefinite number of them), payload (because payload is
> unique for each event, and has an undefined max size), and so on.
> For me there are three reasons why I don't want to cache events:
> * we can't control the in-memory size of this cache, something like
> LRUCache(50) does not mean it's using less memory than LRUCache(500), simply
> because if only one event with a 5 GB (*) payload is in one cache the memory
> usage of the zg daemon will be HUGE.
> * by adding an event cache we are indirectly caching objects like payload,
> uri, etc. we don't want to cache, for above mentioned reasons.
> * I don't think we are able to find a performant Cache implementation for
> all possible clients. Just imaging we have two clients accessing the daemon,
> one which only gets a few events at a time (e.g. unity) and a one which is
> frequently getting *all* events (something like the GAJ histogram [not sure
> if this is still the case, but you should get my point ;)]) While any cache
> would do a good job for unity, only one GetEvents in GAJ will
> destroy/shuffle the complete cache in a way that the performance win for
> unity gets destroyed.
>
> So I suggest two alternative solutions:
> * encourage clients to maintain their own caches, and use FindEventIds() +
> GetEvents[(all ids whih are not in client's cache)] as much as possible. If
> this turns out to have performance issues, we should improve that
> * if you really want a distributed cache, write an event-cache-daemon
> which only has one public API method "GetEvents" and which works like a
> cached proxy, similar to memcached
>
>
> (*) random high number
> --
>
> https:/
> You are the owner of lp:~seif/zeitgeist/add_cache_for_get_events.
>
--
This is me doing some advertisement for my blog http://
Markus Korn (thekorn) wrote : | # |
A quick & dirty graph comparing lp:zeitgeist with this branch is at [0]
I've benchmarked engine.get_events() on an activity log with 50k events for different batch sizes (means length of arguments to get_events())
I'm running each query two times, the first time the cache is cold (marked by "_C") and the second time the cache is HOT ("_H")
Results:
* there is no difference between each run for trunk, which is expected
* cold cache (getting an event the first time) is always slower in this branch
* running the queries with hot cache is fast for small batch sizes
* for huge batch sizes no caching is always faster
Seif Lotfy (seif) wrote : | # |
Awesome
But what is a batch?
On Wed, Dec 8, 2010 at 12:33 PM, Markus Korn <email address hidden> wrote:
> A quick & dirty graph comparing lp:zeitgeist with this branch is at [0]
>
> I've benchmarked engine.get_events() on an activity log with 50k events for
> different batch sizes (means length of arguments to get_events())
>
> I'm running each query two times, the first time the cache is cold (marked
> by "_C") and the second time the cache is HOT ("_H")
>
> Results:
> * there is no difference between each run for trunk, which is expected
> * cold cache (getting an event the first time) is always slower in this
> branch
> * running the queries with hot cache is fast for small batch sizes
> * for huge batch sizes no caching is always faster
>
> [0] https:/
> --
>
> https:/
> You are the owner of lp:~seif/zeitgeist/add_cache_for_get_events.
>
--
This is me doing some advertisement for my blog http://
Markus Korn (thekorn) wrote : | # |
batchsize is number of ids in the get_events() argument,
so,
get_events([1, 2]) -> batchsize == 2
get_events(
Seif Lotfy (seif) wrote : | # |
Thanks for the detailed test. For that I have a solution that I know some of
you might disagree with but I would like to put it out for discussion:
We can create a filter in the cache module to not accept events with
payloads != null
Also we can avoid jumping to look into the cache for batchsizes that exceed
"n" ids.
This is just brainstorming here, because I see a small benefit of LRU cache
for small batches.
On Wed, Dec 8, 2010 at 12:46 PM, Markus Korn <email address hidden> wrote:
> batchsize is number of ids in the get_events() argument,
> so,
> get_events([1, 2]) -> batchsize == 2
> get_events(
> --
>
> https:/
> You are the owner of lp:~seif/zeitgeist/add_cache_for_get_events.
>
--
This is me doing some advertisement for my blog http://
Seif Lotfy (seif) wrote : | # |
I ran some test with bigger cache size (10k) and the change was noticeable that the cache gave a big improvement when Hot.
So I ran some tests and observed that if "cache size < batch size" slowdowns might occur.
My solution for this is to ignore "caching and using cache" for batch_sizes that exceed the cache_size. 1 big batch event can just destroy the cache. Thus using something like batch_size_
Seif Lotfy (seif) wrote : | # |
Just updated the merge request with some batch_size_
- 1649. By Seif Lotfy
-
add batch_tolerance support
Seif Lotfy (seif) wrote : | # |
Here is a csv file based on markus script (for some reason plotting doesnt work for me)
You can see the resuls are pretty good
basically unitl batch size 1000 the hot cache is always faster, then it is just like the noncached version in trunk
---
;50;500;
zeitgeist_
zeitgeist_
add_cache_
add_cache_
---
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
I think Markus has a very good observation. If we are serving clients that request 10k events and other clients that request 10-100 events then It's really hard to get a good caching scheme. The big requests will just always blow the cache (and a cache size of 10k is a no-go by me).
Adding some logic on top to control when and how the cache is applied is a logical next step. It does however set off my internal alarm system. Unless very carefully tested and maintained it might very well end up slowing us down and giving clients unreliable response times because the cache logic is too hard to predict.
My gut tells me that if we too much logic for cache management then we are doing it wrong. It should be simple.
Regarding the cache size, I'm really only worried about the payloads. If we have a cache size of 500 and URIs have a typical length of 100 letters then we'll use 50.000 bytes, ~50kb, for the URIs. So wrt the payloads let's assume that someone stored 100k XML blobs with all events. That gives us 500*100k ~= 50mb. That is too much. Either not caching events with payloads, or fetching payloads lazily could fix this though.
Markus Korn (thekorn) wrote : | # |
Btw, there is another way to blow up the cache: having events with thousands of subjects, so we should limit the number of subjects for cached events too.
Also, please do not hardcode the limits
([...]34 + if len(ids) > 1000:[...])
please use a modul-wide variable, or maybe eent set the limits in the config object.
Seif Lotfy (seif) wrote : | # |
Guys I added a plot of the current state of my "logic" for the cache
http://
- 1650. By Seif Lotfy
-
added ZEITGEIST_
CACHE_SIZE env variable and a check on payloads
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
Can you please add a code comment explaining why we don't cache events with payloads, it would seem arbitrary to the uninitiated.
Other than that it looks good to me. If Markus approves feel free to merge.
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
Oh, btw. I couldn't see from the diff and I don't have time to check the full code: Is the cache also used in FindEvents()? That would be where we'd get the biggest win I think.
Seif Lotfy (seif) wrote : | # |
It is not used in find_events yet but I have a hint of a solution to make use of it :)
- 1651. By Seif Lotfy
-
added comment on why we dont cache events with payloads
added to lrucache.py to Makefile.am - 1652. By Seif Lotfy
-
fix statement in get_events where looking if the event has a payload
- 1653. By Seif Lotfy
-
replaced lrucache
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
I see you replaced the LRUCache implementation? Is this your own implementation, it doesn't look like your code style. I'm worried about licensing issues because you removed the LGPL header.
That said it makes a lot of sense to leverage the native collections module - so in principle I'm +1 for this approach.
Seif Lotfy (seif) wrote : | # |
Hey,
Ofcourse its not my code :P
http://
<http://
not see it till now so this might be an issue.
http://
On Sun, Dec 12, 2010 at 8:04 AM, Mikkel Kamstrup Erlandsen <
<email address hidden>> wrote:
> Review: Needs Information
> I see you replaced the LRUCache implementation? Is this your own
> implementation, it doesn't look like your code style. I'm worried about
> licensing issues because you removed the LGPL header.
>
> That said it makes a lot of sense to leverage the native collections module
> - so in principle I'm +1 for this approach.
> --
>
> https:/
> You are the owner of lp:~seif/zeitgeist/add_cache_for_get_events.
>
--
This is me doing some advertisement for my blog http://
Markus Korn (thekorn) wrote : | # |
@Seif, what's the reason for using another cache implementation?
Seif Lotfy (seif) wrote : | # |
@Markus
1) The previous LRU has something broken in _move_item_to_end for some
reason. (*)
2) It's faster
(*) This is the Error I get. It can be replicated by opening and closing GAJ
several times.
Traceback (most recent call last):
File "/usr/lib/
msg_reply_handler
reply_
File "/home/
line 1303, in do_set
self.
File "/home/
line 1310, in set_items
box = CategoryBox(None, items, True, itemoff=4)
File "/home/
line 315, in __init__
+ " " +
str((time.
+ " " + str(category)
File "/home/
in __get__
value = self.method(
File "/home/
in event
events = INTERFACE.
File
"/home/
line 88, in _ProxyMethod
return self._disconnec
File
"/home/
line 68, in _disconnection_safe
return meth()
File
"/home/
line 89, in <lambda>
getattr(
File "/usr/lib/
**keywords)
File "/usr/lib/
call_blocking
message, timeout)
dbus.exceptions
Traceback (most recent call last):
File "/usr/lib/
_message_cb
retval = candidate_
File
"/home/
line 86, in GetEvents
sender=sender))
File
"/home/
line 202, in get_events
event = self._event_
File
"/home/
line 93, in __getitem__
self.
File
"/home/
line 115, in _move_item_to_end
if item == self._list_start:
File
"/home/
line 38, in __cmp__
return cmp(self.id, item.id)
AttributeError: 'NoneType' object has no attribute 'id'
Markus Korn (thekorn) wrote : | # |
Seif, 1) is not unfixable ;) and I also don't see the new cache implementation as a significat speed improvement, compared to the previous version.
markus@thekorn ~/devel/
;1;2;5;10;50
../trunk_
../trunk_
../add_
../add_
/tmp/new_
/tmp/new_
markus@thekorn ~/devel/
;50;100;
../trunk_
../trunk_
../add_
../add_
/tmp/new_
/tmp/new_
markus@thekorn ~/devel/
(the one in /tmp is this branch as it is right now, add_* is this branch at rev1649)
Seif Lotfy (seif) wrote : | # |
Well I tried to fix it and just checking if "None" does not seem like a
solution to me. I couldn't recreate it over test cases. So I tried over
practical usage.
Unless you can fix it I think it won't hurt to use the new LRU code
- 1654. By Seif Lotfy
-
revert cache implementation for fixing
Markus Korn (thekorn) wrote : | # |
[0] should fixes the issue reported by Seif in one of his comments, and adds a series of tests for it.
Seif Lotfy (seif) wrote : | # |
OK applied the patch. Thank you Markus
- 1655. By Seif Lotfy
-
fix problems encountered in LRU Cache and added test cases
- 1656. By Seif Lotfy
-
updated from trunk
Seif Lotfy (seif) wrote : | # |
@Markus:
the lrucache dumped out
Error from Zeitgeist engine: org.freedesktop
File "/usr/lib/
retval = candidate_
File "/usr/local/
event_
File "/usr/local/
return self._find_
File "/usr/local/
result = self.get_
File "/usr/local/
self.
File "/usr/local/
old = self.remove_
File "/usr/local/
del self._map[old.key]
KeyError: 20320
Trying to replicate it
Seif Lotfy (seif) wrote : | # |
And I just got it again
Error from Zeitgeist engine: org.freedesktop
File "/usr/lib/
retval = candidate_
File "/usr/local/
event_
File "/usr/local/
return self._find_
File "/usr/local/
result = self.get_
File "/usr/local/
self.
File "/usr/local/
old = self.remove_
File "/usr/local/
del self._map[old.key]
KeyError: 19187
- 1657. By Seif Lotfy
-
merge with trunk
- 1658. By Seif Lotfy
-
fix key errors
- 1659. By Seif Lotfy
-
remove prints
Seif Lotfy (seif) wrote : | # |
And again
Error from Zeitgeist engine: org.freedesktop
File "/usr/lib/
retval = candidate_
File "/usr/local/
event_
File "/usr/local/
return self._find_
File "/usr/local/
result = self.get_
File "/usr/local/
self.
File "/usr/local/
old = self.remove_
File "/usr/local/
del self[old.key]
File "/usr/local/
item = self._map[key]
KeyError: 23795
- 1660. By Seif Lotfy
-
replace our lru implementation with a more stable one
Seif Lotfy (seif) wrote : | # |
I think we should go with the online LRU implementation since it does not crash here and I cant find the issue with ours
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
Seif; I don't think you got the lrucache.py sources from that link because the contained link to the source is a dead and I know this package from history. It's based on a heap (not a double linked list or deque) and is dog slow.
Also the AFL is incompatible with LGPL... http://
Seif Lotfy (seif) wrote : | # |
scratch that this is the wrong link
Markus Korn (thekorn) wrote : | # |
Omg, not again, please. If you want to use this lru cache implementation, please create a debian package of this module, get it into debian/ubuntu and add this package as zg dependency. but please don't copy their code into ours (btw, I would consider it as rude if someone copies my code without leaving a comment pointing to the source of the code)
Seif, I'm sure we can fix our implmentation, *if* we know what's going wrong. And from what I see you where not able to provide this information. We know there is a KeyError in __delitem__ at some point. And we know that this is reproducable by you, that's all. Can you please give us more information on the state of the cache when it crashes? has the cache reached its max size? has the event with this id ever been in the cache?
Side note: please *never* merge this branch into lp:zeitgeist, it contains too much trash in the revision history, if you think you found the best solution for such cache please prepare a new clean branch and propose this branch for merge.
- 1661. By Seif Lotfy
-
repairt cache
Markus Korn (thekorn) wrote : | # |
Okidoki, seif. Can you please summarize and comment on the last change you did (against the last revision with this cache implementation). AFAICS you removed _current_id and reformated the docstrings, is this it?
Which bug in the old code were you actually fixing? is this fixing the KeyError you got several times?
Can you please write a test for the case this revision is fixing?
Thanks
Seif Lotfy (seif) wrote : | # |
On Mon, Jan 10, 2011 at 10:36 AM, Markus Korn <email address hidden> wrote:
> Okidoki, seif. Can you please summarize and comment on the last change you
> did (against the last revision with this cache implementation). AFAICS you
> removed _current_id and reformated the docstrings, is this it?
> Which bug in the old code were you actually fixing? is this fixing the
> KeyError you got several times?
> Can you please write a test for the case this revision is fixing?
> Thanks
> --
>
> https:/
> You are the owner of lp:~seif/zeitgeist/add_cache_for_get_events.
>
Yes, you used an incremental counter "current_id" that was redundant since
the keys for the cache could have been done over the "event id". I have been
running the branch now since I committed it in a terminal and noKeyError. I
will retrace the issue now again since it is in the current_id. But I think
this is it.
Markus Korn (thekorn) wrote : | # |
I've seen discussions on memory usage in irc today (seif complaining about this cache using >30 MB writable memory), and I think I found a bug:
20:39 < thekorn> seiflotfy: I think I know what's making the cache size bigger
20:39 < thekorn> seiflotfy: can you please remove old.next = None in line
20:40 < thekorn> and check writable memory again
20:41 < thekorn> if I get 4000 events with a cache size of 2000 I've 14MB
20:41 < thekorn> which is far away from your 30MB
- 1662. By Seif Lotfy
-
removed old.next to fix the memory leak
Seif Lotfy (seif) wrote : | # |
I am moving the work to a new clean branch lp:~seif/zeitgeist/fix-686732
Please lets continue there
Unmerged revisions
- 1662. By Seif Lotfy
-
removed old.next to fix the memory leak
- 1661. By Seif Lotfy
-
repairt cache
- 1660. By Seif Lotfy
-
replace our lru implementation with a more stable one
- 1659. By Seif Lotfy
-
remove prints
- 1658. By Seif Lotfy
-
fix key errors
- 1657. By Seif Lotfy
-
merge with trunk
- 1656. By Seif Lotfy
-
updated from trunk
- 1655. By Seif Lotfy
-
fix problems encountered in LRU Cache and added test cases
- 1654. By Seif Lotfy
-
revert cache implementation for fixing
- 1653. By Seif Lotfy
-
replaced lrucache
Preview Diff
1 | === modified file '_zeitgeist/Makefile.am' |
2 | --- _zeitgeist/Makefile.am 2010-10-13 15:33:17 +0000 |
3 | +++ _zeitgeist/Makefile.am 2011-01-11 08:49:14 +0000 |
4 | @@ -4,4 +4,5 @@ |
5 | |
6 | app_PYTHON = \ |
7 | __init__.py \ |
8 | - singleton.py |
9 | + singleton.py \ |
10 | + lrucache.py |
11 | |
12 | === modified file '_zeitgeist/engine/__init__.py' |
13 | --- _zeitgeist/engine/__init__.py 2010-12-04 17:49:33 +0000 |
14 | +++ _zeitgeist/engine/__init__.py 2011-01-11 08:49:14 +0000 |
15 | @@ -60,5 +60,12 @@ |
16 | CORE_SCHEMA_VERSION = 3 |
17 | |
18 | USER_EXTENSION_PATH = os.path.join(DATA_PATH, "extensions") |
19 | + |
20 | + __default_cache_size = 2000 |
21 | + if "ZEITGEIST_CACHE_SIZE" in os.environ: |
22 | + CACHE_SIZE = int(os.environ["ZEITGEIST_CACHE_SIZE"]) |
23 | + else: |
24 | + CACHE_SIZE = __default_cache_size |
25 | + log.debug("Cache size = %i" %CACHE_SIZE) |
26 | |
27 | constants = _Constants() |
28 | |
29 | === modified file '_zeitgeist/engine/main.py' |
30 | --- _zeitgeist/engine/main.py 2010-12-28 22:32:47 +0000 |
31 | +++ _zeitgeist/engine/main.py 2011-01-11 08:49:14 +0000 |
32 | @@ -34,6 +34,7 @@ |
33 | from _zeitgeist.engine import constants |
34 | from _zeitgeist.engine.sql import get_default_cursor, unset_cursor, \ |
35 | TableLookup, WhereClause |
36 | +from _zeitgeist.lrucache import LRUCache |
37 | |
38 | log = logging.getLogger("zeitgeist.engine") |
39 | |
40 | @@ -118,6 +119,7 @@ |
41 | self._manifestation = TableLookup(cursor, "manifestation") |
42 | self._mimetype = TableLookup(cursor, "mimetype") |
43 | self._actor = TableLookup(cursor, "actor") |
44 | + self._event_cache = LRUCache(constants.CACHE_SIZE) |
45 | |
46 | @property |
47 | def extensions(self): |
48 | @@ -166,10 +168,22 @@ |
49 | if not ids: |
50 | return [] |
51 | |
52 | - rows = self._cursor.execute(""" |
53 | - SELECT * FROM event_view |
54 | - WHERE id IN (%s) |
55 | - """ % ",".join("%d" % id for id in ids)).fetchall() |
56 | + # Split ids into cached and uncached |
57 | + uncached_ids = [] |
58 | + cached_ids = [] |
59 | + |
60 | + # Id ids batch greater than CACHE_SIZE ids ignore cache |
61 | + use_cache = True |
62 | + if len(ids) > constants.CACHE_SIZE/2: |
63 | + use_cache = False |
64 | + if not use_cache: |
65 | + uncached_ids = ids |
66 | + else: |
67 | + for id in ids: |
68 | + if id in self._event_cache: |
69 | + cached_ids.append(id) |
70 | + else: |
71 | + uncached_ids.append(id) |
72 | |
73 | id_hash = defaultdict(list) |
74 | for n, id in enumerate(ids): |
75 | @@ -183,10 +197,36 @@ |
76 | # deleted |
77 | events = {} |
78 | sorted_events = [None]*len(ids) |
79 | + |
80 | + for id in cached_ids: |
81 | + event = self._event_cache[id] |
82 | + if event: |
83 | + event = self.extensions.apply_get_hooks(event, sender) |
84 | + if event is not None: |
85 | + for n in id_hash[event.id]: |
86 | + # insert the event into all necessary spots (LP: #673916) |
87 | + sorted_events[n] = event |
88 | + |
89 | + # Get uncached events |
90 | + rows = self._cursor.execute(""" |
91 | + SELECT * FROM event_view |
92 | + WHERE id IN (%s) |
93 | + """ % ",".join("%d" % id for id in uncached_ids)).fetchall() |
94 | + |
95 | + log.debug("Got %d raw events in %fs" % (len(rows), time.time()-t)) |
96 | + t = time.time() |
97 | + |
98 | + t_get_event = 0 |
99 | + t_get_subject = 0 |
100 | + t_apply_get_hooks = 0 |
101 | + |
102 | for row in rows: |
103 | # Assumption: all rows of a same event for its different |
104 | # subjects are in consecutive order. |
105 | + t_get_event -= time.time() |
106 | event = self._get_event_from_row(row) |
107 | + t_get_event += time.time() |
108 | + |
109 | if event: |
110 | # Check for existing event.id in event to attach |
111 | # other subjects to it |
112 | @@ -194,14 +234,28 @@ |
113 | events[event.id] = event |
114 | else: |
115 | event = events[event.id] |
116 | + |
117 | + # Avoid caching events with payloads to have keep the cache MB size |
118 | + # at a decent level |
119 | + if use_cache and not event.payload: |
120 | + self._event_cache[event.id] = event |
121 | + |
122 | + t_get_subject -= time.time() |
123 | event.append_subject(self._get_subject_from_row(row)) |
124 | + t_get_subject += time.time() |
125 | + |
126 | + t_apply_get_hooks -= time.time() |
127 | event = self.extensions.apply_get_hooks(event, sender) |
128 | + t_apply_get_hooks += time.time() |
129 | if event is not None: |
130 | for n in id_hash[event.id]: |
131 | # insert the event into all necessary spots (LP: #673916) |
132 | sorted_events[n] = event |
133 | - |
134 | + |
135 | log.debug("Got %d events in %fs" % (len(sorted_events), time.time()-t)) |
136 | + log.debug(" Where time spent in _get_event_from_row in %fs" % (t_get_event)) |
137 | + log.debug(" Where time spent in _get_subject_from_row in %fs" % (t_get_subject)) |
138 | + log.debug(" Where time spent in apply_get_hooks in %fs" % (t_apply_get_hooks)) |
139 | return sorted_events |
140 | |
141 | @staticmethod |
142 | @@ -604,6 +658,11 @@ |
143 | """ % ",".join(str(int(_id)) for _id in ids)) |
144 | timestamps = self._cursor.fetchone() |
145 | |
146 | + # Remove events from cache |
147 | + for id in ids: |
148 | + if id in self._event_cache: |
149 | + del self._event_cache[id] |
150 | + |
151 | # Make sure that we actually found some events with these ids... |
152 | # We can't do all(timestamps) here because the timestamps may be 0 |
153 | if timestamps and timestamps[0] is not None and timestamps[1] is not None: |
154 | @@ -613,7 +672,6 @@ |
155 | log.debug("Deleted %s" % map(int, ids)) |
156 | |
157 | self.extensions.apply_post_delete(ids, sender) |
158 | - |
159 | return timestamps |
160 | else: |
161 | log.debug("Tried to delete non-existing event(s): %s" % map(int, ids)) |
162 | |
163 | === added file '_zeitgeist/lrucache.py' |
164 | --- _zeitgeist/lrucache.py 1970-01-01 00:00:00 +0000 |
165 | +++ _zeitgeist/lrucache.py 2011-01-11 08:49:14 +0000 |
166 | @@ -0,0 +1,125 @@ |
167 | +# -.- coding: utf-8 -.- |
168 | + |
169 | +# lrucache.py |
170 | +# |
171 | +# Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com> |
172 | +# Copyright © 2009 Markus Korn <thekorn@gmx.de> |
173 | +# Copyright © 2011 Seif Lotfy <seif@lotfy.com> |
174 | +# |
175 | +# This program is free software: you can redistribute it and/or modify |
176 | +# it under the terms of the GNU Lesser General Public License as published by |
177 | +# the Free Software Foundation, either version 3 of the License, or |
178 | +# (at your option) any later version. |
179 | +# |
180 | +# This program is distributed in the hope that it will be useful, |
181 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
182 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
183 | +# GNU Lesser General Public License for more details. |
184 | +# |
185 | +# You should have received a copy of the GNU Lesser General Public License |
186 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
187 | + |
188 | +class LRUCache: |
189 | + """ |
190 | + A simple LRUCache implementation backed by a linked list and a dict. |
191 | + It can be accessed and updated just like a dict. To check if an element |
192 | + exists in the cache the following type of statements can be used: |
193 | + if "foo" in cache |
194 | + """ |
195 | + |
196 | + class _Item: |
197 | + """ |
198 | + A container for each item in LRUCache which knows about the |
199 | + item's position and relations |
200 | + """ |
201 | + def __init__(self, item_key, item_value): |
202 | + self.value = item_value |
203 | + self.key = item_key |
204 | + self.next = None |
205 | + self.prev = None |
206 | + |
207 | + def __init__(self, max_size): |
208 | + """ |
209 | + The size of the cache (in number of cached items) is guaranteed to |
210 | + never exceed 'size' |
211 | + """ |
212 | + self._max_size = max_size |
213 | + self.clear() |
214 | + |
215 | + |
216 | + def clear(self): |
217 | + self._list_end = None # The newest item |
218 | + self._list_start = None # Oldest item |
219 | + self._map = {} |
220 | + |
221 | + def __len__(self): |
222 | + return len(self._map) |
223 | + |
224 | + def __contains__(self, key): |
225 | + return key in self._map |
226 | + |
227 | + def __delitem__(self, key): |
228 | + item = self._map[key] |
229 | + if item.prev: |
230 | + item.prev.next = item.next |
231 | + else: |
232 | + # we are deleting the first item, so we need a new first one |
233 | + self._list_start = item.next |
234 | + if item.next: |
235 | + item.next.prev = item.prev |
236 | + else: |
237 | + # we are deleting the last item, get a new last one |
238 | + self._list_end = item.prev |
239 | + del self._map[key], item |
240 | + |
241 | + def __setitem__(self, key, value): |
242 | + if key in self._map: |
243 | + item = self._map[key] |
244 | + item.value = value |
245 | + self._move_item_to_end(item) |
246 | + else: |
247 | + new = LRUCache._Item(key, value) |
248 | + self._append_to_list(new) |
249 | + |
250 | + if len(self._map) > self._max_size : |
251 | + # Remove eldest entry from list |
252 | + self.remove_eldest_item() |
253 | + |
254 | + def __getitem__(self, key): |
255 | + item = self._map[key] |
256 | + self._move_item_to_end(item) |
257 | + return item.value |
258 | + |
259 | + def __iter__(self): |
260 | + """ |
261 | + Iteration is in order from eldest to newest, |
262 | + and returns (key,value) tuples |
263 | + """ |
264 | + iter = self._list_start |
265 | + while iter != None: |
266 | + yield (iter.key, iter.value) |
267 | + iter = iter.next |
268 | + |
269 | + def _move_item_to_end(self, item): |
270 | + del self[item.key] |
271 | + self._append_to_list(item) |
272 | + |
273 | + def _append_to_list(self, item): |
274 | + self._map[item.key] = item |
275 | + if not self._list_start: |
276 | + self._list_start = item |
277 | + if self._list_end: |
278 | + self._list_end.next = item |
279 | + item.prev = self._list_end |
280 | + item.next = None |
281 | + self._list_end = item |
282 | + |
283 | + def remove_eldest_item(self): |
284 | + if self._list_start == self._list_end: |
285 | + self._list_start = None |
286 | + self._list_end = None |
287 | + return |
288 | + old = self._list_start |
289 | + old.next.prev = None |
290 | + self._list_start = old.next |
291 | + del self[old.key], old |
292 | \ No newline at end of file |
293 | |
294 | === added file 'test/lrucache_test.py' |
295 | --- test/lrucache_test.py 1970-01-01 00:00:00 +0000 |
296 | +++ test/lrucache_test.py 2011-01-11 08:49:14 +0000 |
297 | @@ -0,0 +1,98 @@ |
298 | +#!/usr/bin/python |
299 | + |
300 | +# Update python path to use local zeitgeist module |
301 | +import sys |
302 | +import os |
303 | +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) |
304 | + |
305 | +from _zeitgeist.lrucache import LRUCache |
306 | + |
307 | +import unittest |
308 | + |
309 | +class LRUCacheTest(unittest.TestCase): |
310 | + |
311 | + def testPutGetOne(self): |
312 | + """Test that we can cache and then retrieve one single item""" |
313 | + cache = LRUCache(10) |
314 | + cache["foo"] = "bar" |
315 | + self.assertEquals("bar", cache["foo"]) |
316 | + self.assertRaises(KeyError, lambda : cache["nosuchelement"]) |
317 | + |
318 | + def testPutGetTwo(self): |
319 | + """Test that we can cache and then retrieve two items""" |
320 | + cache = LRUCache(10) |
321 | + cache["foo1"] = "bar1" |
322 | + cache["foo2"] = "bar2" |
323 | + self.assertEquals("bar1", cache["foo1"]) |
324 | + self.assertEquals("bar2", cache["foo2"]) |
325 | + self.assertRaises(KeyError, lambda : cache["nosuchelement"]) |
326 | + |
327 | + def testExceedMaxSize(self): |
328 | + """Test that we can restrict the cache size to one element, and that |
329 | + this one element is the latest one we've added""" |
330 | + cache = LRUCache(1) |
331 | + cache["foo1"] = "bar1" |
332 | + cache["foo2"] = "bar2" |
333 | + self.assertRaises(KeyError, lambda : cache["foo1"]) |
334 | + self.assertEquals("bar2", cache["foo2"]) |
335 | + self.assertEquals(1, len(cache)) |
336 | + |
337 | + def testInKeyword(self): |
338 | + """Make sure we can do 'if "foo" in cache' type of statements""" |
339 | + cache = LRUCache(5) |
340 | + cache["foo1"] = "bar1" |
341 | + self.assertFalse("bork" in cache) |
342 | + self.assertTrue("foo1" in cache) |
343 | + |
344 | + def testIteration(self): |
345 | + """Make sure that iteration is in the correct order; oldest to newest""" |
346 | + cache = LRUCache(4) |
347 | + cache["foo1"] = "bar1" |
348 | + cache["foo2"] = "bar2" |
349 | + cache["foo3"] = "bar3" |
350 | + cache["foo4"] = "bar4" |
351 | + cache["foo1"] = "bar1" # "foo1" should now be newest |
352 | + |
353 | + l = [] |
354 | + for key_val in cache : l.append(key_val) |
355 | + self.assertEquals([("foo2", "bar2"), |
356 | + ("foo3", "bar3"), |
357 | + ("foo4", "bar4"), |
358 | + ("foo1", "bar1")], l) |
359 | + |
360 | + def testDeleteItem(self): |
361 | + cache = LRUCache(4) |
362 | + cache["foo1"] = "bar1" |
363 | + cache["foo2"] = "bar2" |
364 | + cache["foo3"] = "bar3" |
365 | + cache["foo4"] = "bar4" |
366 | + self.assertTrue("foo2" in cache) |
367 | + del cache["foo2"] # delete item in the middle of the cache |
368 | + self.assertTrue("foo2" not in cache) |
369 | + self.assertEquals( |
370 | + [("foo1", "bar1"), ("foo3", "bar3"), ("foo4", "bar4")], |
371 | + list(cache) |
372 | + ) |
373 | + self.assertTrue("foo1" in cache) |
374 | + del cache["foo1"] # delete first item |
375 | + self.assertTrue("foo1" not in cache) |
376 | + self.assertEquals( |
377 | + [("foo3", "bar3"), ("foo4", "bar4")], |
378 | + list(cache) |
379 | + ) |
380 | + self.assertTrue("foo4" in cache) |
381 | + del cache["foo4"] # delete last item |
382 | + self.assertTrue("foo4" not in cache) |
383 | + self.assertEquals( |
384 | + [("foo3", "bar3")], |
385 | + list(cache) |
386 | + ) |
387 | + del cache["foo3"] |
388 | + self.assertTrue("foo3" not in cache) |
389 | + self.assertEquals([], list(cache)) |
390 | + self.assertTrue(cache._list_start is None) |
391 | + self.assertTrue(cache._list_end is None) |
392 | + |
393 | + |
394 | +if __name__ == '__main__': |
395 | + unittest.main() |
396 | \ No newline at end of file |
397 | |
398 | === modified file 'zeitgeist/client.py' |
399 | --- zeitgeist/client.py 2010-12-26 09:18:11 +0000 |
400 | +++ zeitgeist/client.py 2011-01-11 08:49:14 +0000 |
401 | @@ -919,4 +919,4 @@ |
402 | normal_reply_handler(normal_reply_data) |
403 | |
404 | _FIND_EVENTS_FOR_TEMPLATES_ARGS = inspect.getargspec( |
405 | - ZeitgeistClient.find_events_for_templates)[0] |
406 | + ZeitgeistClient.find_events_for_templates)[0] |
407 | \ No newline at end of file |
So we go from 18ms to 2ms. That's a speedup of a factor of 9, quite nice :-)
As discussed on IRC we need to limit the cache somehow of course. The old LRU cache comes to mind. Also, we need to be relatively cautious here running the danger of ballooning our memory consumption.
I'd like to see some more elaborate speed tests (Markus' multicolor diagram ftw!) as well using a bounded cache implementation of some sort. Also an assessment of the memory impact of this.
And please remember to revive the unit tests for the LRU cache if we do choose to bring it back :-)
I really like where this is going :-)