Skip to content

Commit 8670443

Browse files
committed
Implemented buffer interface and test to fully proove it. Its not yet fully properly implemented
1 parent 011343f commit 8670443

4 files changed

Lines changed: 196 additions & 10 deletions

File tree

smmap/buf.py

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,92 @@
1-
"""Module with a simple stream implementation using the memory manager"""
1+
"""Module with a simple buffer implementation using the memory manager"""
2+
from mman import MemoryCursor
23

3-
from mman import *
4+
import sys
45

5-
__all__ = []
6+
__all__ = ["MappedMemoryBuffer"]
7+
8+
class MappedMemoryBuffer(object):
9+
"""A buffer like object which allows direct byte-wise object and slicing into
10+
memory of a mapped file. The mapping is controlled by an underlying memory manager.
11+
12+
A buffer, once initialized, stays put on providing access to eactly one path.
13+
A custom interface allows you to change paths mid way, and to optimize
14+
the resource usage.
15+
16+
Please note that this type is only fully usable if you configure it with the
17+
MappedMemoryManager to use.
18+
19+
The buffer is relative, that is if you map an offset, index 0 will map to the
20+
first byte at your given offset."""
21+
__slots__ = '_c' # our cursor
22+
23+
#{ Configuration
24+
# A subclass must provide an instance of a (usually global) MappedMemoryManager
25+
manager = None
26+
#}END configuration
27+
28+
def __init__(self, path = None, offset = 0, size = sys.maxint, flags = 0):
29+
"""Initalize the instance to operate on the given path if given.
30+
:param path: if not None, the path to the file you want to access
31+
If None, you have call begin_access before using the buffer
32+
:param offset: absolute offset in bytes
33+
:param size: the total size of the mapping. Defaults to the maximum possible size
34+
:param flags: Additional flags to be passed to os.open
35+
:raise ValueError: if the buffer could not achieve a valid state"""
36+
self._c = MemoryCursor(self.manager)
37+
assert self.manager is not None, "Require the cls.manager variable to be set in subclass"
38+
if path and not self.begin_access(path, offset, size, flags):
39+
raise ValueError("Failed to allocate the buffer - probably the given offset is out of bounds")
40+
# END handle offset
41+
42+
def __del__(self):
43+
self.end_access()
44+
45+
def __getitem__(self, i):
46+
c = self._c
47+
if not c.includes_ofs(i):
48+
c.use_region(i, 1)
49+
# END handle region usage
50+
assert c.is_valid() # TODO: remove for performance
51+
return c.buffer()[i]
52+
53+
def __getslice__(self, i, j):
54+
c = self._c
55+
# fast path, slice fully included - safes a concatenate operation and
56+
# should be the default
57+
if c.ofs_begin() >= i and j < c.ofs_end():
58+
return c.buffer()[i:j]
59+
raise NotImplementedError()
60+
#{ Interface
61+
62+
def begin_access(self, path = None, offset = 0, size = sys.maxint, flags = 0):
63+
"""Call this before the first use of this instance. The method was already
64+
called by the constructor in case sufficient information was provided.
65+
66+
For more information no the parameters, see the __init__ method
67+
:param path: if path is empty or None the existing path will be used if possible.
68+
:return: True if the buffer can be used"""
69+
if path and (not self._c.is_associated() or self._c.path() != path):
70+
self._c = self.manager.make_cursor(path)
71+
#END get associated cursor
72+
73+
# reuse existing cursors if possible
74+
if self._c.is_associated():
75+
return self._c.use_region(offset, size, flags).is_valid()
76+
return False
77+
78+
def end_access(self):
79+
"""Call this method once you are done using the instance. It is automatically
80+
called on destruction, and should be called just in time to allow system
81+
resources to be freed.
82+
83+
Once you called end_access, you must call begin access before reusing this instance!"""
84+
self._c.unuse_region()
85+
86+
def cursor(self):
87+
""":return: the currently set cursor which provides access to the data"""
88+
return self._c
89+
90+
#}END interface
691

792

smmap/mman.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,9 @@ def unuse_region(self):
226226
def buffer(self):
227227
"""Return a buffer object which allows access to our memory region from our offset
228228
to the window size. Please note that it might be smaller than you requested
229-
:note: You can only obtain a buffer if this instance is_valid() !"""
229+
:note: You can only obtain a buffer if this instance is_valid() !
230+
:note: buffers should not be cached passed the duration of your access as it will
231+
prevent resources from being freed even though they might not be accounted for anymore !"""
230232
return buffer(self._region.buffer(), self._ofs, self._size)
231233

232234
def is_valid(self):
@@ -336,6 +338,7 @@ def _collect_lru_region(self, size):
336338
:param size: size of the region we want to map next (assuming its not already mapped partially or full
337339
if 0, we try to free any available region
338340
:raise RegionCollectionError:
341+
:return: Amount of freed regions
339342
:todo: implement a case where all unusued regions are discarded efficiently. Currently its only brute force"""
340343
num_found = 0
341344
while (size == 0) or (self._memory_size + size > self._max_memory_size):
@@ -365,6 +368,8 @@ def _collect_lru_region(self, size):
365368
self._handle_count -= 1
366369
#END while there is more memory to free
367370

371+
return num_found
372+
368373
#{ Interface
369374
def make_cursor(self, path):
370375
""":return: a cursor pointing to the given path. It can be used to map new regions of the file into memory"""
@@ -375,6 +380,11 @@ def make_cursor(self, path):
375380
# END obtain region for path
376381
return MemoryCursor(self, regions)
377382

383+
def collect(self):
384+
"""Collect all available free-to-collect mapped regions
385+
:return: Amount of freed handles"""
386+
return self._collect_lru_region(0)
387+
378388
def num_file_handles(self):
379389
""":return: amount of file handles in use. Each mapped region uses one file handle"""
380390
return self._handle_count

smmap/test/test_buf.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,92 @@
1-
from lib import TestBase
1+
from lib import TestBase, FileCreator
22

3+
from smmap.mman import MappedMemoryManager
34
from smmap.buf import *
45

6+
from random import randint
7+
from time import time
8+
import sys
9+
10+
class TestBuffer(MappedMemoryBuffer):
11+
#{ Configuration
12+
manager = MappedMemoryManager()
13+
#} END configuration
14+
15+
516
class TestBuf(TestBase):
17+
618
def test_basics(self):
7-
assert False
19+
self.failUnlessRaises(AssertionError, MappedMemoryBuffer) # needs subclass
20+
fc = FileCreator(self.k_window_test_size, "buffer_test")
21+
22+
# invalid paths fail upon construction
23+
self.failUnlessRaises(OSError, TestBuffer, "somefile") # invalid file
24+
self.failUnlessRaises(ValueError, TestBuffer, fc.path, fc.size) # offset too large
25+
26+
buf = TestBuffer() # can create uninitailized buffers
27+
assert not buf.cursor().is_valid() and not buf.cursor().is_associated()
28+
29+
# can call end access any time
30+
buf.end_access()
31+
buf.end_access()
32+
33+
# begin access can revive it, if the offset is suitable
34+
offset = 100
35+
assert buf.begin_access(fc.path, fc.size) == False
36+
assert buf.begin_access(fc.path, offset) == True
37+
38+
# empty begin access keeps it valid on the same path, but alters the offset
39+
assert buf.begin_access() == True
40+
assert buf.cursor().is_valid()
41+
42+
# simple access
43+
data = open(fc.path, 'rb').read()
44+
assert data[offset] == buf[0]
45+
assert data[offset:offset*2] == buf[0:offset]
46+
47+
# end access makes its cursor invalid
48+
buf.end_access()
49+
assert not buf.cursor().is_valid()
50+
assert buf.cursor().is_associated() # but it remains associated
51+
52+
# an empty begin access fixes it up again
53+
assert buf.begin_access() == True and buf.cursor().is_valid()
54+
del(buf) # ends access automatically
55+
56+
man = TestBuffer.manager
57+
assert man.num_file_handles() == 1
58+
59+
# PERFORMANCE
60+
# blast away with rnadom access and a full mapping - we don't want to
61+
# exagerate the manager's overhead, but measure the buffer overhead
62+
# We do it once with an optimal setting, and with a worse manager which
63+
# will produce small mappings only !
64+
max_num_accesses = 5000
65+
num_accesses_left = max_num_accesses
66+
67+
for manager in (MappedMemoryManager(window_size=fc.size/100, max_memory_size=fc.size/3, max_open_handles=15), man):
68+
TestBuffer.manager = manager
69+
st = time()
70+
buf = TestBuffer(fc.path)
71+
assert manager.num_file_handles() == 1
72+
num_bytes = 0
73+
fsize = fc.size
74+
while num_accesses_left:
75+
num_accesses_left -= 1
76+
ofs_start = randint(0, fsize)
77+
ofs_end = randint(ofs_start, fsize)
78+
d = buf[ofs_start:ofs_end]
79+
assert len(d) == ofs_end - ofs_start
80+
assert d == data[ofs_start:ofs_end]
81+
num_bytes += len(d)
82+
pos = randint(0, fsize)
83+
assert buf[pos] == data[pos]
84+
# END handle num accesses
85+
buf.end_access()
86+
assert manager.num_file_handles() == 1
87+
assert manager.collect() == 1
88+
assert manager.num_file_handles() == 0
89+
elapsed = time() - st
90+
mb = 1000*1000
91+
sys.stderr.write("Made %i random slices to buffer reading a total of %f mb in %f s (%f mb/s)\n" % (max_num_accesses, num_bytes/mb, elapsed, (num_bytes/mb)/elapsed))
92+
# END for each manager

smmap/test/test_mman.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ def test_memman_operation(self):
7575
assert len(data) == fc.size
7676

7777
# small windows, a reasonable max memory. Not too many regions at once
78-
man = MappedMemoryManager(fc.size / 100, fc.size / 3, 15)
78+
max_num_handles = 15
79+
man = MappedMemoryManager(window_size=fc.size / 100, max_memory_size=fc.size / 3, max_open_handles=max_num_handles)
7980
c = man.make_cursor(fc.path)
8081

8182
# still empty (more about that is tested in test_memory_manager()
@@ -129,7 +130,7 @@ def test_memman_operation(self):
129130

130131
# iterate through the windows, verify data contents
131132
# this will trigger map collection after a while
132-
max_random_accesses = 15000
133+
max_random_accesses = 5000
133134
num_random_accesses = max_random_accesses
134135
memory_read = 0
135136
st = time()
@@ -160,6 +161,11 @@ def test_memman_operation(self):
160161
mb = 1000 * 1000
161162
sys.stderr.write("Read %i mb of memory with %i random accesses in %fs (%f mb/s)\n"
162163
% (memory_read/mb, max_random_accesses, elapsed, (memory_read/mb)/elapsed))
163-
164+
164165
# an offset as large as the size doesn't work !
165-
assert not c.use_region(fc.size, size).is_valid()
166+
assert not c.use_region(fc.size, size).is_valid()
167+
168+
# collection - it should be able to collect all
169+
assert man.num_file_handles()
170+
assert man.collect()
171+
assert man.num_file_handles() == 0

0 commit comments

Comments
 (0)