Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/contrib/lib9p/pytest/numalloc.py
39536 views
1
#! /usr/bin/env python
2
3
"""
4
Integer number allocator.
5
6
Basically, these keep track of a set of allocatable values in
7
some range (you provide min and max) and let you allocate out of
8
the range and return values into the range.
9
10
You may pick a value using "next since last time", or "next
11
available after provided value". Note that next-after will
12
wrap around as needed (modular arithmetic style).
13
14
The free lists are thread-locked so that this code can be used
15
with threads.
16
17
>>> a = NumAlloc(5, 10) # note closed interval: 5..10 inclusive
18
>>> a
19
NumAlloc(5, 10)
20
>>> a.avail
21
[[5, 10]]
22
>>> a.alloc()
23
5
24
>>> a.avail
25
[[6, 10]]
26
>>> a.alloc(8)
27
8
28
>>> a.avail
29
[[6, 7], [9, 10]]
30
>>> a.free(5)
31
>>> a.avail
32
[[5, 7], [9, 10]]
33
>>> a.free(8)
34
>>> a.avail
35
[[5, 10]]
36
37
Attempting to free a value that is already free is an error:
38
39
>>> a.free(5)
40
Traceback (most recent call last):
41
...
42
ValueError: free: 5 already available
43
44
You can, however, free a value that is outside the min/max
45
range. You can also free multiple values at once:
46
47
>>> a.free_multi([0, 1, 2, 4])
48
>>> a.avail
49
[[0, 2], [4, 10]]
50
>>> a.free_multi([3, 12])
51
>>> a.avail
52
[[0, 10], [12, 12]]
53
54
Note that this changes the min/max values:
55
56
>>> a
57
NumAlloc(0, 12)
58
59
To prevent adding values outside the min/max range, create the
60
NumArray with autoextend=False, or set .autoextend=False at any
61
time:
62
63
>>> a.autoextend = False
64
>>> a
65
NumAlloc(0, 12, autoextend=False)
66
>>> a.free(13)
67
Traceback (most recent call last):
68
...
69
ValueError: free: 13 is outside range limit
70
71
You can create an empty range, which is really only useful once
72
you free values into it:
73
74
>>> r = NumAlloc(0, -1)
75
>>> r
76
NumAlloc(0, -1)
77
>>> r.alloc() is None
78
True
79
>>> r.free_multi(range(50))
80
>>> r
81
NumAlloc(0, 49)
82
83
Note that r.alloc() starts from where you last left off, even if
84
you've freed a value:
85
86
>>> r.alloc()
87
0
88
>>> r.free(0)
89
>>> r.alloc()
90
1
91
92
Of course, in multithreaded code you can't really depend on this
93
since it will race other threads. Still, it generally makes for
94
efficient allocation. To force allocation to start from the
95
range's minimum, provide the minimum (e.g., r.min_val) as an
96
argument to r.alloc():
97
98
>>> r.alloc()
99
2
100
>>> r.alloc(r.min_val)
101
0
102
103
Providing a number to alloc() tries to allocate that number,
104
but wraps around to the next one if needed:
105
106
>>> r.alloc(49)
107
49
108
>>> r.alloc(49)
109
3
110
>>> r.alloc(99999)
111
4
112
>>> r.avail
113
[[5, 48]]
114
115
There is currently no way to find all allocated values, although
116
the obvious method (going through r.avail) will work. Any iterator
117
would not be thread-safe.
118
"""
119
120
import threading
121
122
class NumAlloc(object):
123
"""
124
Number allocator object.
125
"""
126
def __init__(self, min_val, max_val, autoextend=True):
127
self.min_val = min_val
128
self.max_val = max_val
129
if min_val <= max_val:
130
self.avail = [[min_val, max_val]]
131
else:
132
self.avail = []
133
self.autoextend = autoextend
134
self.last = None
135
self.lock = threading.Lock()
136
137
def __repr__(self):
138
myname = self.__class__.__name__
139
if self.autoextend:
140
ae = ''
141
else:
142
ae = ', autoextend=False'
143
return '{0}({1}, {2}{3})'.format(myname, self.min_val, self.max_val, ae)
144
145
def _find_block(self, val):
146
"""
147
Find the block that contains val, or that should contain val.
148
Remember that self.avail is a list of avaliable ranges of
149
the form [[min1, max1], [min2, max2], ..., [minN, maxN]]
150
where max1 < min2, max2 < min3, ..., < minN.
151
152
The input value either falls into one of the available
153
blocks, or falls into a gap between two available blocks.
154
We want to know which block it goes in, or if it goes
155
between two, which block it comes before.
156
157
We can do a binary search to find this block. When we
158
find it, return its index and its values.
159
160
If we find that val is not in a block, return the position
161
where the value should go, were it to be put into a new
162
block by itself. E.g., suppose val is 17, and there is a
163
block [14,16] and a block [18,20]. We would make this
164
[14,16],[17,17],[18,20] by inserting [17,17] between them.
165
(Afterward, we will want to fuse all three blocks to make
166
[14,18]. However, if we insert as block 0, e.g., if the
167
list starts with [18,20] and we insert to get
168
[17,17][18,20], we really end up just modifying block 0 to
169
[17,20]. Or, if we insert as the new final block, we
170
might end up modifying the last block.)
171
"""
172
low = 0
173
high = len(self.avail) - 1
174
while low <= high:
175
mid = low + ((high - low) // 2)
176
pair = self.avail[mid]
177
if val < pair[0]:
178
# must go before block mid
179
high = mid - 1
180
elif val > pair[1]:
181
# must go after block mid
182
low = mid + 1
183
else:
184
# val >= first and val <= last, so we found it
185
return mid, pair
186
# Low > high: no block actually contains val, or
187
# there are no blocks at all. If there are no blocks,
188
# return block #0 and None. Otherwise return the
189
return low, None
190
191
def alloc(self, val=None):
192
"""
193
Get new available value.
194
195
If val is None, we start from the most recently
196
allocated value, plus 1.
197
198
If val is a numeric value, we start from that value.
199
Hence, since the range is min_val..max_val, you can
200
provide min_val to take the first available value.
201
202
This may return None, if no values are still available.
203
"""
204
with self.lock:
205
if val is None:
206
val = self.last + 1 if self.last is not None else self.min_val
207
if val is None or val > self.max_val or val < self.min_val:
208
val = self.min_val
209
i, pair = self._find_block(val)
210
if pair is None:
211
# Value is is not available. The next
212
# available value that is greater than val
213
# is in the block right after block i.
214
# If there is no block after i, the next
215
# available value is in block 0. If there
216
# is no block 0, there are no available
217
# values.
218
nblocks = len(self.avail)
219
i += 1
220
if i >= nblocks:
221
if nblocks == 0:
222
return None
223
i = 0
224
pair = self.avail[i]
225
val = pair[0]
226
# Value val is available - take it.
227
#
228
# There are four special cases to handle.
229
#
230
# 1. pair[0] < val < pair[1]: split the pair.
231
# 2. pair[0] == val < pair[1]: increase pair[0].
232
# 3. pair[0] == val == pair[1]: delete the pair
233
# 4. pair[0] < val == pair[1]: decrease pair[1].
234
assert pair[0] <= val <= pair[1]
235
if pair[0] == val:
236
# case 2 or 3: Take the left edge or delete the pair.
237
if val == pair[1]:
238
del self.avail[i]
239
else:
240
pair[0] = val + 1
241
else:
242
# case 1 or 4: split the pair or take the right edge.
243
if val == pair[1]:
244
pair[1] = val - 1
245
else:
246
newpair = [val + 1, pair[1]]
247
pair[1] = val - 1
248
self.avail.insert(i + 1, newpair)
249
self.last = val
250
return val
251
252
def free(self, val):
253
"Free one value"
254
self._free_multi('free', [val])
255
256
def free_multi(self, values):
257
"Free many values (provide any iterable)"
258
values = list(values)
259
values.sort()
260
self._free_multi('free_multi', values)
261
262
def _free_multi(self, how, values):
263
"""
264
Free a (sorted) list of values.
265
"""
266
if len(values) == 0:
267
return
268
with self.lock:
269
while values:
270
# Take highest value, and any contiguous lower values.
271
# Note that it can be significantly faster this way
272
# since coalesced ranges make for shorter copies.
273
highval = values.pop()
274
val = highval
275
while len(values) and values[-1] == val - 1:
276
val = values.pop()
277
self._free_range(how, val, highval)
278
279
def _maybe_increase_max(self, how, val):
280
"""
281
If needed, widen our range to include new high val -- i.e.,
282
possibly increase self.max_val. Do nothing if this is not a
283
new all time high; fail if we have autoextend disabled.
284
"""
285
if val <= self.max_val:
286
return
287
if self.autoextend:
288
self.max_val = val
289
return
290
raise ValueError('{0}: {1} is outside range limit'.format(how, val))
291
292
def _maybe_decrease_min(self, how, val):
293
"""
294
If needed, widen our range to include new low val -- i.e.,
295
possibly decrease self.min_val. Do nothing if this is not a
296
new all time low; fail if we have autoextend disabled.
297
"""
298
if val >= self.min_val:
299
return
300
if self.autoextend:
301
self.min_val = val
302
return
303
raise ValueError('{0}: {1} is outside range limit'.format(how, val))
304
305
def _free_range(self, how, val, highval):
306
"""
307
Free the range [val..highval]. Note, val==highval it's just
308
a one-element range.
309
310
The lock is already held.
311
"""
312
# Find the place to store the lower value.
313
# We should never find an actual pair here.
314
i, pair = self._find_block(val)
315
if pair:
316
raise ValueError('{0}: {1} already available'.format(how, val))
317
# If we're freeing a range, check that the high val
318
# does not span into the *next* range, either.
319
if highval > val and i < len(self.avail):
320
if self.avail[i][0] <= highval:
321
raise ValueError('{0}: {2} (from {{1}..{2}) already '
322
'available'.format(how, val, highval))
323
324
# We'll need to insert a block and perhaps fuse it
325
# with blocks before and/or after. First, check
326
# whether there *is* a before and/or after, and find
327
# their corresponding edges and whether we abut them.
328
if i > 0:
329
abuts_below = self.avail[i - 1][1] + 1 == val
330
else:
331
abuts_below = False
332
if i < len(self.avail):
333
abuts_above = self.avail[i][0] - 1 == highval
334
else:
335
abuts_above = False
336
# Now there are these four cases:
337
# 1. abuts below and above: fuse the two blocks.
338
# 2. abuts below only: adjust previous (i-1'th) block
339
# 3. abuts above only: adjust next (i'th) block
340
# 4. doesn't abut: insert new block
341
if abuts_below:
342
if abuts_above:
343
# case 1
344
self.avail[i - 1][1] = self.avail[i][1]
345
del self.avail[i]
346
else:
347
# case 2
348
self._maybe_increase_max(how, highval)
349
self.avail[i - 1][1] = highval
350
else:
351
if abuts_above:
352
# case 3
353
self._maybe_decrease_min(how, val)
354
self.avail[i][0] = val
355
else:
356
# case 4
357
self._maybe_decrease_min(how, val)
358
self._maybe_increase_max(how, highval)
359
newblock = [val, highval]
360
self.avail.insert(i, newblock)
361
362
if __name__ == '__main__':
363
import doctest
364
import sys
365
366
doctest.testmod()
367
if sys.version_info[0] >= 3:
368
xrange = range
369
# run some worst case tests
370
# NB: coalesce is terribly slow when done bottom up
371
r = NumAlloc(0, 2**16 - 1)
372
for i in xrange(r.min_val, r.max_val, 2):
373
r.alloc(i)
374
print('worst case alloc: len(r.avail) = {0}'.format(len(r.avail)))
375
for i in xrange(r.max_val - 1, r.min_val, -2):
376
r.free(i)
377
print('free again; len(r.avail) should be 1; is {0}'.format(len(r.avail)))
378
if len(r.avail) != 1:
379
sys.exit('failure')
380
381