Skip to content

Commit 066ee2f

Browse files
committed
Merge branch 'device'
2 parents 04991e9 + 68fba82 commit 066ee2f

7 files changed

Lines changed: 246 additions & 26 deletions

File tree

smmap/buf.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Module with a simple buffer implementation using the memory manager"""
2+
from mman import MemoryCursor
3+
4+
import sys
5+
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+
assert c.is_valid()
48+
if not c.includes_ofs(i):
49+
c.use_region(i, 1)
50+
# END handle region usage
51+
return c.buffer()[i-c.ofs_begin()]
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+
assert c.is_valid()
58+
if (c.ofs_begin() <= i) and (j < c.ofs_end()):
59+
b = c.ofs_begin()
60+
return c.buffer()[i-b:j-b]
61+
else:
62+
l = j-i # total length
63+
ofs = i
64+
# Keeping tokens in a list could possible be faster, but the list
65+
# overhead outweighs the benefits (tested) !
66+
md = str()
67+
while l:
68+
c.use_region(ofs, l)
69+
d = c.buffer()[:l]
70+
ofs += len(d)
71+
l -= len(d)
72+
md += d
73+
#END while there are bytes to read
74+
return md
75+
# END fast or slow path
76+
#{ Interface
77+
78+
def begin_access(self, path = None, offset = 0, size = sys.maxint, flags = 0):
79+
"""Call this before the first use of this instance. The method was already
80+
called by the constructor in case sufficient information was provided.
81+
82+
For more information no the parameters, see the __init__ method
83+
:param path: if path is empty or None the existing path will be used if possible.
84+
:return: True if the buffer can be used"""
85+
if path and (not self._c.is_associated() or self._c.path() != path):
86+
self._c = self.manager.make_cursor(path)
87+
#END get associated cursor
88+
89+
# reuse existing cursors if possible
90+
if self._c.is_associated():
91+
return self._c.use_region(offset, size, flags).is_valid()
92+
return False
93+
94+
def end_access(self):
95+
"""Call this method once you are done using the instance. It is automatically
96+
called on destruction, and should be called just in time to allow system
97+
resources to be freed.
98+
99+
Once you called end_access, you must call begin access before reusing this instance!"""
100+
self._c.unuse_region()
101+
102+
def cursor(self):
103+
""":return: the currently set cursor which provides access to the data"""
104+
return self._c
105+
106+
#}END interface
107+
108+

smmap/mman.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,12 @@ def assign(self, rhs):
7878
self._destroy()
7979
self._copy_from(rhs)
8080

81-
def use_region(self, offset, size, _is_recursive=False):
81+
def use_region(self, offset, size, flags = 0, _is_recursive=False):
8282
"""Assure we point to a window which allows access to the given offset into the file
8383
:param offset: absolute offset in bytes into the file
8484
:param size: amount of bytes to map
85+
:param flags: additional flags to be given to os.open in case a file handle is initially opened
86+
for mapping. Has no effect if a region can actually be reused.
8587
:return: this instance - it should be queried for whether it points to a valid memory region.
8688
This is not the case if the mapping failed becaues we reached the end of the file
8789
:note: The size actually mapped may be smaller than the given size. If that is the case,
@@ -106,6 +108,8 @@ def use_region(self, offset, size, _is_recursive=False):
106108
return self
107109
# END handle offset too large
108110

111+
# bisect to find an existing region. The c++ implementation cannot
112+
# do that as it uses a linked list for regions.
109113
existing_region = None
110114
a = self._rlist
111115
lo = 0
@@ -181,7 +185,7 @@ def use_region(self, offset, size, _is_recursive=False):
181185
if man._handle_count >= man._max_handle_count:
182186
raise Exception
183187
#END assert own imposed max file handles
184-
self._region = MappedRegion(a.path(), mid.ofs, mid.size)
188+
self._region = MappedRegion(a.path(), mid.ofs, mid.size, flags)
185189
except Exception:
186190
# apparently we are out of system resources or hit a limit
187191
# As many more operations are likely to fail in that condition (
@@ -195,7 +199,7 @@ def use_region(self, offset, size, _is_recursive=False):
195199
raise
196200
#END handle existing recursion
197201
man._collect_lru_region(0)
198-
return self.use_region(offset, size, True)
202+
return self.use_region(offset, size, flags, True)
199203
#END handle exceptions
200204

201205
man._handle_count += 1
@@ -218,11 +222,15 @@ def unuse_region(self):
218222
to unuse the region once you are done reading from it in persistent cursors as it
219223
helps to free up resource more quickly"""
220224
self._region = None
225+
# note: should reset ofs and size, but we spare that for performance. Its not
226+
# allowed to query information if we are not valid !
221227

222228
def buffer(self):
223229
"""Return a buffer object which allows access to our memory region from our offset
224230
to the window size. Please note that it might be smaller than you requested
225-
:note: You can only obtain a buffer if this instance is_valid() !"""
231+
:note: You can only obtain a buffer if this instance is_valid() !
232+
:note: buffers should not be cached passed the duration of your access as it will
233+
prevent resources from being freed even though they might not be accounted for anymore !"""
226234
return buffer(self._region.buffer(), self._ofs, self._size)
227235

228236
def is_valid(self):
@@ -234,7 +242,8 @@ def is_associated(self):
234242
return self._rlist is not None
235243

236244
def ofs_begin(self):
237-
""":return: offset to the first byte pointed to by our cursor"""
245+
""":return: offset to the first byte pointed to by our cursor
246+
:note: only if is_valid() is True"""
238247
return self._region._b + self._ofs
239248

240249
def ofs_end(self):
@@ -332,6 +341,7 @@ def _collect_lru_region(self, size):
332341
:param size: size of the region we want to map next (assuming its not already mapped partially or full
333342
if 0, we try to free any available region
334343
:raise RegionCollectionError:
344+
:return: Amount of freed regions
335345
:todo: implement a case where all unusued regions are discarded efficiently. Currently its only brute force"""
336346
num_found = 0
337347
while (size == 0) or (self._memory_size + size > self._max_memory_size):
@@ -361,6 +371,8 @@ def _collect_lru_region(self, size):
361371
self._handle_count -= 1
362372
#END while there is more memory to free
363373

374+
return num_found
375+
364376
#{ Interface
365377
def make_cursor(self, path):
366378
""":return: a cursor pointing to the given path. It can be used to map new regions of the file into memory"""
@@ -371,6 +383,11 @@ def make_cursor(self, path):
371383
# END obtain region for path
372384
return MemoryCursor(self, regions)
373385

386+
def collect(self):
387+
"""Collect all available free-to-collect mapped regions
388+
:return: Amount of freed handles"""
389+
return self._collect_lru_region(0)
390+
374391
def num_file_handles(self):
375392
""":return: amount of file handles in use. Each mapped region uses one file handle"""
376393
return self._handle_count

smmap/stream.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

smmap/test/test_buf.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from lib import TestBase, FileCreator
2+
3+
from smmap.mman import MappedMemoryManager
4+
from smmap.buf import *
5+
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+
16+
class TestBuf(TestBase):
17+
18+
def test_basics(self):
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 = 1000
65+
for manager, man_id in ( (man, 'optimal'),
66+
(MappedMemoryManager(window_size=fc.size/100, max_memory_size=fc.size/3, max_open_handles=15), 'worst case')):
67+
TestBuffer.manager = manager
68+
buf = TestBuffer(fc.path)
69+
assert manager.num_file_handles() == 1
70+
for access_mode in range(2): # single, multi
71+
num_accesses_left = max_num_accesses
72+
num_bytes = 0
73+
fsize = fc.size
74+
75+
st = time()
76+
buf.begin_access()
77+
while num_accesses_left:
78+
num_accesses_left -= 1
79+
if access_mode: # multi
80+
ofs_start = randint(0, fsize)
81+
ofs_end = randint(ofs_start, fsize)
82+
d = buf[ofs_start:ofs_end]
83+
assert len(d) == ofs_end - ofs_start
84+
assert d == data[ofs_start:ofs_end]
85+
num_bytes += len(d)
86+
else:
87+
pos = randint(0, fsize)
88+
assert buf[pos] == data[pos]
89+
num_bytes += 1
90+
#END handle mode
91+
# END handle num accesses
92+
93+
buf.end_access()
94+
assert manager.num_file_handles()
95+
assert manager.collect()
96+
assert manager.num_file_handles() == 0
97+
elapsed = time() - st
98+
mb = float(1000*1000)
99+
mode_str = (access_mode and "slice") or "single byte"
100+
sys.stderr.write("%s: Made %i random %s accesses to buffer reading a total of %f mb in %f s (%f mb/s)\n" % (man_id, max_num_accesses, mode_str, num_bytes/mb, elapsed, (num_bytes/mb)/elapsed))
101+
# END handle access mode
102+
# END for each manager

smmap/test/test_mman.py

Lines changed: 11 additions & 5 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()
@@ -157,9 +158,14 @@ def test_memman_operation(self):
157158
assert not includes_ofs(base_offset+csize)
158159
# END while we should do an access
159160
elapsed = time() - st
160-
mb = 1000 * 1000
161+
mb = float(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

smmap/test/test_stream.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

smmap/util.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,19 @@ class MappedRegion(object):
9393
#END handle additional slot
9494

9595

96-
def __init__(self, path, ofs, size):
96+
def __init__(self, path, ofs, size, flags = 0):
9797
"""Initialize a region, allocate the memory map
9898
:param path: path to the file to map
9999
:param ofs: **aligned** offset into the file to be mapped
100100
:param size: if size is larger then the file on disk, the whole file will be
101101
allocated the the size automatically adjusted
102+
:param flags: additional flags to be given when opening the file.
102103
:raise Exception: if no memory can be allocated"""
103104
self._b = ofs
104105
self._size = 0
105106
self._uc = 0
106107

107-
fd = os.open(path, os.O_RDONLY|getattr(os, 'O_BINARY', 0))
108+
fd = os.open(path, os.O_RDONLY|getattr(os, 'O_BINARY', 0)|flags)
108109
try:
109110
kwargs = dict(access=ACCESS_READ, offset=ofs)
110111
corrected_size = size

0 commit comments

Comments
 (0)