Skip to content

Commit e7b0e1c

Browse files
committed
Implemented use_region. Let the testing begin. Especially the actual data handling will be interesting, which has to work exclusively through buffer objects. There are plenty of layers between the user and the data, which will always be copied when slicing it (as we have no memoryview). The latter one could be implemented on in case we use 2.7 at some point.
Now, let the testing begin !
1 parent 04b9ec1 commit e7b0e1c

4 files changed

Lines changed: 174 additions & 13 deletions

File tree

smmap/mman.py

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from exc import RegionCollectionError
1111
from weakref import proxy
12+
import sys
1213

1314
__all__ = ["MappedMemoryManager"]
1415
#{ Utilities
@@ -77,23 +78,138 @@ def assign(self, rhs):
7778
self._destroy()
7879
self._copy_from(rhs)
7980

80-
def use_region(self, offset, size):
81+
def use_region(self, offset, size, _is_recursive=False):
8182
"""Assure we point to a window which allows access to the given offset into the file
8283
:param offset: absolute offset in bytes into the file
8384
:param size: amount of bytes to map
8485
:return: this instance - it should be queried for whether it points to a valid memory region.
8586
This is not the case if the mapping failed becaues we reached the end of the file
8687
:note: The size actually mapped may be smaller than the given size. If that is the case,
8788
either the file has reached its end, or the map was created between two existing regions"""
89+
need_region = True
90+
man = self._manager
91+
size = min(size, man.window_size()) # clamp size to window size
8892

93+
if self._region is not None:
94+
if self._region.includes_ofs(offset):
95+
need_region = False
96+
else:
97+
self.unuse_region()
98+
# END handle existing region
99+
# END check existing region
100+
101+
if need_region:
102+
# abort on offsets beyond our mapped file's size - currently we are invalid
103+
if offset > self.file_size():
104+
return self
105+
# END handle offset too large
106+
107+
existing_region = None
108+
for region in self._rlist:
109+
if region.includes_ofs(offset):
110+
existing_region = region
111+
break
112+
#END handle existing region
113+
#END for each existing region
89114

115+
if existing_region is None:
116+
left = MemoryWindow(0, 0)
117+
mid = MemoryWindow(offset, size)
118+
right = MemoryWindow(self.file_size(), 0)
119+
120+
# we want to honor the max memory size, and assure we have anough
121+
# memory available
122+
man._collect_lru_region(man.window_size())
123+
124+
# we assume the list remains sorted by offset
125+
insert_pos = 0
126+
len_regions = len(self._rlist)
127+
if len_regions == 1:
128+
if self._rlist[0].ofs_begin() <= offset:
129+
insert_pos = 1
130+
#END maintain sort
131+
else:
132+
# find insert position
133+
insert_pos = len_regions
134+
for i, region in enumerate(self._rlist):
135+
if region.ofs_begin() > offset:
136+
insert_pos = i
137+
break
138+
#END if insert position is correct
139+
#END for each region
140+
# END obtain insert pos
141+
142+
# adjust the actual offset and size values to create the largest
143+
# possible mapping
144+
if insert_pos == 0:
145+
if len_regions:
146+
right = MemoryWindow.from_region(self._rlist[insert_pos])
147+
#END adjust right side
148+
else:
149+
if insert_pos != len_regions:
150+
right = MemoryWindow.from_region(self._rlist[insert_pos])
151+
# END adjust right window
152+
left = MemoryWindow.from_region(self._rlist[insert_pos - 1])
153+
#END adjust surrounding windows
154+
155+
mid.extend_left_to(left, man._window_size)
156+
mid.extend_right_to(right, man._window_size)
157+
mid.align()
158+
159+
# it can happen that we align beyond the end of the file
160+
if mid.ofs_end() > right.ofs:
161+
mid.size = right.ofs - mid.ofs
162+
#END readjust size
163+
164+
# insert new region at the right offset to keep the order
165+
try:
166+
if man._handle_count >= man._max_handle_count:
167+
raise Exception
168+
#END assert own imposed max file handles
169+
self._region = MappedRegion(self._rlist.path(), mid.ofs, mid.size)
170+
except Exception:
171+
# apparently we are out of system resources or hit a limit
172+
# As many more operations are likely to fail in that condition (
173+
# like reading a file from disk, etc) we free up as much as possible
174+
# As this invalidates our insert position, we have to recurse here
175+
# NOTE: The c++ version uses a linked list to curcumvent this, but
176+
# using that in python is probably too slow anyway
177+
if _is_recursive:
178+
# we already tried this, and still have no success in obtaining
179+
# a mapping. This is an exception, so we propagate it
180+
raise
181+
#END handle existing recursion
182+
man._collect_lru_region(0)
183+
return self.use_region(offset, size, True)
184+
#END handle exceptions
185+
186+
man._handle_count += 1
187+
man._memory_size += self._region.size()
188+
self._rlist.insert(insert_pos, self._region)
189+
else:
190+
self._region = existing_region
191+
#END need region handling
192+
#END handle acquire region
193+
194+
self._region.increment_usage_count()
195+
self._ofs = offset - self._region.ofs_begin()
196+
self._size = min(size, self._region.ofs_end() - offset)
197+
198+
return self
199+
90200
def unuse_region(self):
91201
"""Unuse the ucrrent region. Does nothing if we have no current region
92202
:note: the cursor unuses the region automatically upon destruction. It is recommended
93203
to unuse the region once you are done reading from it in persistent cursors as it
94204
helps to free up resource more quickly"""
95205
self._region = None
96-
206+
207+
def buffer(self):
208+
"""Return a buffer object which allows access to our memory region from our offset
209+
to the window size. Please note that it might be smaller than you requested
210+
:note: You can only obtain a buffer if this instance is_valid() !"""
211+
return buffer(self._region.buffer(), self._ofs, self._size)
212+
97213
def is_valid(self):
98214
""":return: True if we have a valid and usable region"""
99215
return self._region is not None
@@ -136,7 +252,6 @@ def path(self):
136252
#} END interface
137253

138254

139-
140255
class MappedMemoryManager(object):
141256
"""Maintains a list of ranges of mapped memory regions in one or more files and allows to easily
142257
obtain additional regions assuring there is no overlap.
@@ -161,13 +276,13 @@ class MappedMemoryManager(object):
161276

162277
_MB_in_bytes = 1024 * 1024
163278

164-
def __init__(self, window_size = 0, max_memory_size = 0, max_open_handles = ~0):
279+
def __init__(self, window_size = 0, max_memory_size = 0, max_open_handles = sys.maxint):
165280
"""initialize the manager with the given parameters.
166281
:param window_size: if 0, a default window size will be chosen depending on
167282
the operating system's architechture. It will internally be quantified to a multiple of the page size
168283
:param max_memory_size: maximum amount of memory we may map at once before releasing mapped regions.
169284
If 0, a viable default iwll be set dependning on the system's architecture.
170-
:param max_open_handles: if not ~0, lmit the amount of open file handles to the given number.
285+
:param max_open_handles: if not maxin, limit the amount of open file handles to the given number.
171286
Otherwise the amount is only limited by the system iteself. If a system or soft limit is hit,
172287
the manager will free as many handles as posisble"""
173288
self._fdict = dict()
@@ -193,7 +308,7 @@ def __init__(self, window_size = 0, max_memory_size = 0, max_open_handles = ~0):
193308
self._max_memory_size = coeff * self._MB_in_bytes
194309
#END handle max memory size
195310

196-
def _collect_one_lru_region(self, size):
311+
def _collect_lru_region(self, size):
197312
"""Unmap the region which was least-recently used and has no client
198313
:param size: size of the region we want to map next (assuming its not already mapped partially or full
199314
if 0, we try to free any available region
@@ -253,6 +368,10 @@ def mapped_memory_size(self):
253368
""":return: amount of bytes currently mapped in total"""
254369
return self._memory_size
255370

371+
def max_file_handles(self):
372+
""":return: maximium amount of handles we may have opened"""
373+
return self._max_handle_count
374+
256375
def max_mapped_memory_size(self):
257376
""":return: maximum amount of memory we may allocate"""
258377
return self._max_memory_size

smmap/test/test_mman.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,36 @@
1212
class TestMMan(TestBase):
1313

1414
def test_cursor(self):
15+
fc = FileCreator(self.k_window_test_size, "cursor_test")
16+
1517
man = MappedMemoryManager()
16-
c = MemoryCursor(man)
17-
assert not c.is_valid()
18-
assert not c.is_associated()
18+
ci = MemoryCursor(man) # invalid cursor
19+
assert not ci.is_valid()
20+
assert not ci.is_associated()
21+
assert ci.size() == 0 # this is cached, so we can query it in invalid state
22+
23+
cv = man.make_cursor(fc.path)
24+
assert not cv.is_valid() # no region mapped yet
25+
assert cv.is_associated()# but it know where to map it from
26+
assert cv.file_size() == fc.size
27+
assert cv.path() == fc.path
1928

2029
# copy module
30+
cio = copy(cv)
31+
assert not cio.is_valid() and cio.is_associated()
2132

2233
# assign method
34+
assert not ci.is_associated()
35+
ci.assign(cv)
36+
assert not ci.is_valid() and ci.is_associated()
2337

38+
# unuse non-existing region is fine
39+
cv.unuse_region()
40+
cv.unuse_region()
2441

42+
# destruction is fine (even multiple times)
43+
cv._destroy()
44+
MemoryCursor(man)._destroy()
2545

2646
def test_memory_manager(self):
2747
man = MappedMemoryManager()
@@ -33,8 +53,14 @@ def test_memory_manager(self):
3353
assert man.page_size() == PAGESIZE
3454

3555
# collection doesn't raise in 'any' mode
36-
man._collect_one_lru_region(0)
56+
man._collect_lru_region(0)
3757
# doesn't raise if we are within the limit
38-
man._collect_one_lru_region(10)
58+
man._collect_lru_region(10)
3959
# raises outside of limit
40-
self.failUnlessRaises(RegionCollectionError, man._collect_one_lru_region, sys.maxint)
60+
self.failUnlessRaises(RegionCollectionError, man._collect_lru_region, sys.maxint)
61+
62+
63+
# use a region, verify most basic functionality
64+
fc = FileCreator(self.k_window_test_size, "manager_test")
65+
c = man.make_cursor(fc.path)
66+
assert c.use_region(10, 10).is_valid()

smmap/test/test_util.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ def test_region_list(self):
8888
fc = FileCreator(100, "sample_file")
8989
ml = MappedRegionList(fc.path)
9090

91+
assert ml.client_count() == 1
92+
9193
assert len(ml) == 0
9294
assert ml.path() == fc.path
9395
assert ml.file_size() == fc.size

smmap/util.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ def ofs_end(self):
5454
return self.ofs + self.size
5555

5656
def align(self):
57-
self.ofs = align_to_page(self.ofs, 0)
57+
"""Assures the previous window area is contained in the new one"""
58+
nofs = align_to_page(self.ofs, 0)
59+
self.size += self.ofs - nofs # keep size constant
60+
self.ofs = nofs
5861
self.size = align_to_page(self.size, 1)
5962

6063
def extend_left_to(self, window, max_size):
@@ -123,6 +126,10 @@ def __init__(self, path, ofs, size):
123126
os.close(fd)
124127
#END close file handle
125128

129+
def buffer(self):
130+
""":return: a sliceable buffer which can be used to access the mapped memory"""
131+
return self._mf
132+
126133
def ofs_begin(self):
127134
""":return: absolute byte offset to the first byte of the mapping"""
128135
return self._b
@@ -159,6 +166,9 @@ def size(self):
159166

160167
def ofs_end(self):
161168
return len(self._mf)
169+
170+
def buffer(self):
171+
return self._mfb
162172
#END handle compat layer
163173

164174

@@ -176,6 +186,10 @@ def __init__(self, path):
176186
self._path = path
177187
self._file_size = None
178188

189+
def client_count(self):
190+
""":return: amount of clients which hold a reference to this instance"""
191+
return getrefcount(self)-3
192+
179193
def path(self):
180194
""":return: path to file whose regions we manage"""
181195
return self._path

0 commit comments

Comments
 (0)