File Coverage

blib/lib/POE/Queue/Array.pm
Criterion Covered Total %
statement 153 179 85.4
branch 52 66 78.7
condition 2 2 100.0
subroutine 19 20 95.0
pod 10 10 100.0
total 236 277 85.2


line stmt bran cond sub pod time code
1             # Copyrights and documentation are at the end.
2              
3             package POE::Queue::Array;
4              
5 178     178   37842 use strict;
  178         266  
  178         8291  
6              
7 178     178   855 use vars qw($VERSION @ISA);
  178         248  
  178         12090  
8             $VERSION = '1.367'; # NOTE - Should be #.### (three decimal places)
9             @ISA = qw(POE::Queue);
10              
11 178     178   1547 use Errno qw(ESRCH EPERM);
  178         2680  
  178         76572  
12 178     178   1035 use Carp qw(confess);
  178         265  
  178         28508  
13              
14             sub DEBUG () { 0 }
15              
16             ### Helpful offsets.
17              
18             sub ITEM_PRIORITY () { 0 }
19             sub ITEM_ID () { 1 }
20             sub ITEM_PAYLOAD () { 2 }
21              
22             sub import {
23 178     178   485 my $package = caller();
24 178     178   955 no strict 'refs';
  178         264  
  178         282545  
25 178         375 *{ $package . '::ITEM_PRIORITY' } = \&ITEM_PRIORITY;
  178         1174  
26 178         335 *{ $package . '::ITEM_ID' } = \&ITEM_ID;
  178         1016  
27 178         319 *{ $package . '::ITEM_PAYLOAD' } = \&ITEM_PAYLOAD;
  178         851  
28             }
29              
30             # Item IDs are unique across all queues.
31              
32             my $queue_seq = 0;
33             my %item_priority;
34              
35             ### A very simple constructor.
36              
37             sub new {
38 176     176 1 820 bless [], shift();
39             }
40              
41             ### Add an item to the queue. Returns the new item's ID.
42              
43             sub enqueue {
44 7477     7477 1 32839 my ($self, $priority, $payload) = @_;
45              
46             # Get the next item ID. This clever loop will hang indefinitely if
47             # you ever run out of integers to store things under. Map the ID to
48             # its due time for search-by-ID functions.
49              
50 7477         7854 my $item_id;
51 7477         22900 1 while exists $item_priority{$item_id = ++$queue_seq};
52 7477         22517 $item_priority{$item_id} = $priority;
53              
54 7477         18527 my $item_to_enqueue = [
55             $priority, # ITEM_PRIORITY
56             $item_id, # ITEM_ID
57             $payload, # ITEM_PAYLOAD
58             ];
59              
60             # Special case: No items in the queue. The queue IS the item.
61 7477 100       17938 unless (@$self) {
62 1317         2552 $self->[0] = $item_to_enqueue;
63 1317         1509 DEBUG and warn $self->_dump_splice(0);
64 1317         4157 return $item_id;
65             }
66              
67             # Special case: The new item belongs at the end of the queue.
68 6160 100       19101 if ($priority >= $self->[-1]->[ITEM_PRIORITY]) {
69 1090         2137 push @$self, $item_to_enqueue;
70 1090         1261 DEBUG and warn $self->_dump_splice(@$self-1);
71 1090         3675 return $item_id;
72             }
73              
74             # Special case: The new item belongs at the head of the queue.
75 5070 100       12956 if ($priority < $self->[0]->[ITEM_PRIORITY]) {
76 764         1708 unshift @$self, $item_to_enqueue;
77 764         879 DEBUG and warn $self->_dump_splice(0);
78 764         2696 return $item_id;
79             }
80              
81             # Special case: There are only two items in the queue. This item
82             # naturally belongs between them.
83 4306 100       8405 if (@$self == 2) {
84 103         350 splice @$self, 1, 0, $item_to_enqueue;
85 103         145 DEBUG and warn $self->_dump_splice(1);
86 103         422 return $item_id;
87             }
88              
89             # And finally we have a nontrivial queue. Insert the item using a
90             # binary seek.
91              
92 4203         11451 $self->_insert_item(0, $#$self, $priority, $item_to_enqueue);
93 4203         15641 return $item_id;
94             }
95              
96             ### Dequeue the next thing from the queue. Returns an empty list if
97             ### the queue is empty. There are different flavors of this
98             ### operation.
99              
100             sub dequeue_next {
101 5490     5490 1 693211 my $self = shift;
102              
103 5490 100       14024 return unless @$self;
104 5485         6647 my ($priority, $id, $stuff) = @{shift @$self};
  5485         15883  
105 5485         30145 delete $item_priority{$id};
106 5485         18572 return ($priority, $id, $stuff);
107             }
108              
109             ### Return the next item's priority, undef if the queue is empty.
110             # This is POE's most-called method. We could greatly benefit from
111             # finding ways to reduce the number of calls.
112              
113             sub get_next_priority {
114             # This is Ton Hospel's optimization.
115             # He measured a 4% improvement by avoiding $self.
116 14457   100 14457 1 85245 return (shift->[0] || return undef)->[ITEM_PRIORITY];
117             }
118              
119             ### Return the number of items currently in the queue.
120              
121             sub get_item_count {
122 5608     5608 1 14015 return scalar @{$_[0]};
  5608         33698  
123             }
124              
125             ### Internal method to insert an item using a binary seek and splice.
126             ### We accept the bounds as parameters because the alarm adjustment
127             ### functions may also use it.
128              
129             sub _insert_item {
130 6189     6189   10575 my ($self, $lower, $upper, $priority, $item) = @_;
131              
132 6189         5879 while (1) {
133 54938         50235 my $midpoint = ($upper + $lower) >> 1;
134              
135             # Upper and lower bounds crossed. Insert at the lower point.
136 54938 100       75382 if ($upper < $lower) {
137 6189         13891 splice @$self, $lower, 0, $item;
138 6189         5099 DEBUG and warn $self->_dump_splice($lower);
139 6189         9963 return;
140             }
141              
142             # We're looking for a priority lower than the one at the midpoint.
143             # Set the new upper point to just before the midpoint.
144 48749 100       86644 if ($priority < $self->[$midpoint]->[ITEM_PRIORITY]) {
145 22476         18524 $upper = $midpoint - 1;
146 22476         20175 next;
147             }
148              
149             # We're looking for a priority greater or equal to the one at the
150             # midpoint. The new lower bound is just after the midpoint.
151 26273         22690 $lower = $midpoint + 1;
152             }
153             }
154              
155             ### Internal method to find a queue item by its priority and ID. We
156             ### assume the priority and ID have been verified already, so the item
157             ### must exist. Returns the index of the item that matches the
158             ### priority/ID pair.
159              
160             sub _find_item {
161 3764     3764   3634 my ($self, $id, $priority) = @_;
162              
163             # Use a binary seek.
164              
165 3764         3908 my $upper = $#$self; # Last index of @$self.
166 3764         3124 my $lower = 0;
167 3764         2567 while (1) {
168 40108         31802 my $midpoint = ($upper + $lower) >> 1;
169              
170             # Upper and lower bounds crossed. The lower point is aimed at an
171             # element with a priority higher than our target.
172 40108 100       51937 last if $upper < $lower;
173              
174             # We're looking for a priority lower than the one at the midpoint.
175             # Set the new upper point to just before the midpoint.
176 36344 100       59602 if ($priority < $self->[$midpoint]->[ITEM_PRIORITY]) {
177 18986         14003 $upper = $midpoint - 1;
178 18986         15165 next;
179             }
180              
181             # We're looking for a priority greater or equal to the one at the
182             # midpoint. The new lower bound is just after the midpoint.
183 17358         13323 $lower = $midpoint + 1;
184             }
185              
186             # The lower index is pointing to an element with a priority higher
187             # than our target. Scan backwards until we find the item with the
188             # target ID.
189 3764         5803 while ($lower-- >= 0) {
190 3981 100       9462 return $lower if $self->[$lower]->[ITEM_ID] == $id;
191             }
192              
193 0         0 die "should never get here... maybe the queue is out of order";
194             }
195              
196             ### Remove an item by its ID. Takes a coderef filter, too, for
197             ### examining the payload to be sure it really wants to leave. Sets
198             ### $! and returns undef on failure.
199              
200             sub remove_item {
201 234     234 1 2615 my ($self, $id, $filter) = @_;
202              
203 234         478 my $priority = $item_priority{$id};
204 234 100       469 unless (defined $priority) {
205 2         7 $! = ESRCH;
206 2         11 return;
207             }
208              
209             # Find that darn item.
210 232         481 my $item_index = $self->_find_item($id, $priority);
211              
212             # Test the item against the filter.
213 232 100       806 unless ($filter->($self->[$item_index]->[ITEM_PAYLOAD])) {
214 1         6 $! = EPERM;
215 1         6 return;
216             }
217              
218             # Remove the item, and return it.
219 231         888 delete $item_priority{$id};
220 231         240 return @{splice @$self, $item_index, 1};
  231         1112  
221             }
222              
223             ### Remove items matching a filter. Regrettably, this must scan the
224             ### entire queue. An optional count limits the number of items to
225             ### remove, and it may shorten execution times. Returns a list of
226             ### references to priority/id/payload lists. This is intended to
227             ### return all the items matching the filter, and the function's
228             ### behavior is undefined when $count is less than the number of
229             ### matching items.
230              
231             sub remove_items {
232 4778     4778 1 8709 my ($self, $filter, $count) = @_;
233 4778 100       13702 $count = @$self unless $count;
234              
235 4778         5533 my @items;
236 4778         7145 my $i = @$self;
237 4778         14337 while ($i--) {
238 801032 100       1455848 if ($filter->($self->[$i]->[ITEM_PAYLOAD])) {
239 1629         5820 my $removed_item = splice(@$self, $i, 1);
240 1629         6907 delete $item_priority{$removed_item->[ITEM_ID]};
241 1629         4224 unshift @items, $removed_item;
242 1629 100       6726 last unless --$count;
243             }
244             }
245              
246 4778         32575 return @items;
247             }
248              
249             ### Adjust the priority of an item by a relative amount. Adds $delta
250             ### to the priority of the $id'd object (if it matches $filter), and
251             ### moves it in the queue.
252              
253             sub adjust_priority {
254 2032     2032 1 7799 my ($self, $id, $filter, $delta) = @_;
255              
256 2032         2621 my $old_priority = $item_priority{$id};
257 2032 50       2866 unless (defined $old_priority) {
258 0         0 $! = ESRCH;
259 0         0 return;
260             }
261              
262             # Find that darn item.
263 2032         2516 my $item_index = $self->_find_item($id, $old_priority);
264              
265             # Test the item against the filter.
266 2032 100       4131 unless ($filter->($self->[$item_index]->[ITEM_PAYLOAD])) {
267 1000         3561 $! = EPERM;
268 1000         1708 return;
269             }
270              
271             # Nothing to do if the delta is zero.
272             # TODO Actually we may need to ensure that the item is moved to the
273             # end of its current priority bucket, since it should have "moved".
274 1032 50       3735 return $self->[$item_index]->[ITEM_PRIORITY] unless $delta;
275              
276             # Remove the item, and adjust its priority.
277 1032         1840 my $item = splice(@$self, $item_index, 1);
278 1032         1011 my $new_priority = $item->[ITEM_PRIORITY] += $delta;
279 1032         1077 $item_priority{$id} = $new_priority;
280              
281 1032         1704 $self->_reinsert_item($new_priority, $delta, $item_index, $item);
282             }
283              
284             ### Set the priority to a specific amount. Replaces the item's
285             ### priority with $new_priority (if it matches $filter), and moves it
286             ### to the new location in the queue.
287              
288             sub set_priority {
289 2000     2000 1 9172 my ($self, $id, $filter, $new_priority) = @_;
290              
291 2000         3065 my $old_priority = $item_priority{$id};
292 2000 50       3089 unless (defined $old_priority) {
293 0         0 $! = ESRCH;
294 0         0 return;
295             }
296              
297             # Nothing to do if the old and new priorities match.
298             # TODO Actually we may need to ensure that the item is moved to the
299             # end of its current priority bucket, since it should have "moved".
300 2000 100       3512 return $new_priority if $new_priority == $old_priority;
301              
302             # Find that darn item.
303 1500         1979 my $item_index = $self->_find_item($id, $old_priority);
304              
305             # Test the item against the filter.
306 1500 100       3248 unless ($filter->($self->[$item_index]->[ITEM_PAYLOAD])) {
307 500         2012 $! = EPERM;
308 500         942 return;
309             }
310              
311             # Remove the item, and calculate the delta.
312 1000         4251 my $item = splice(@$self, $item_index, 1);
313 1000         951 my $delta = $new_priority - $old_priority;
314 1000         1246 $item->[ITEM_PRIORITY] = $item_priority{$id} = $new_priority;
315              
316 1000         1553 $self->_reinsert_item($new_priority, $delta, $item_index, $item);
317             }
318              
319             ### Sanity-check the results of an item insert. Verify that it
320             ### belongs where it was put. Only called during debugging.
321              
322             sub _dump_splice {
323 0     0   0 my ($self, $index) = @_;
324 0         0 my @return;
325 0         0 my $at = $self->[$index]->[ITEM_PRIORITY];
326 0 0       0 if ($index > 0) {
327 0         0 my $before = $self->[$index-1]->[ITEM_PRIORITY];
328 0         0 push @return, "before($before)";
329 0 0       0 confess "out of order: $before should be < $at" if $before > $at;
330             }
331 0         0 push @return, "at($at)";
332 0 0       0 if ($index < $#$self) {
333 0         0 my $after = $self->[$index+1]->[ITEM_PRIORITY];
334 0         0 push @return, "after($after)";
335 0         0 my @priorities = map {$_->[ITEM_PRIORITY]} @$self;
  0         0  
336 0 0       0 confess "out of order: $at should be < $after (@priorities)" if (
337             $at >= $after
338             );
339             }
340 0         0 return "@return";
341             }
342              
343             ### Reinsert an item into the queue. It has just been removed by
344             ### adjust_priority() or set_priority() and needs to be replaced.
345             ### This tries to be clever by not doing more work than necessary.
346              
347             sub _reinsert_item {
348 2032     2032   2223 my ($self, $new_priority, $delta, $item_index, $item) = @_;
349              
350             # Now insert it back.
351             # The special cases are duplicates from enqueue(). We use the delta
352             # (direction) of the move and the old item index to narrow down the
353             # subsequent nontrivial insert if none of the special cases apply.
354              
355             # Special case: No events in the queue. The queue IS the item.
356 2032 50       3172 unless (@$self) {
357 0         0 $self->[0] = $item;
358 0         0 DEBUG and warn $self->_dump_splice(0);
359 0         0 return $new_priority;
360             }
361              
362             # Special case: The item belongs at the end of the queue.
363 2032 100       3439 if ($new_priority >= $self->[-1]->[ITEM_PRIORITY]) {
364 24         28 push @$self, $item;
365 24         32 DEBUG and warn $self->_dump_splice(@$self-1);
366 24         52 return $new_priority;
367             }
368              
369             # Special case: The item belongs at the head of the queue.
370 2008 100       3536 if ($new_priority < $self->[0]->[ITEM_PRIORITY]) {
371 22         53 unshift @$self, $item;
372 22         21 DEBUG and warn $self->_dump_splice(0);
373 22         75 return $new_priority;
374             }
375              
376             # Special case: There are only two items in the queue. This item
377             # naturally belongs between them.
378              
379 1986 50       2813 if (@$self == 2) {
380 0         0 splice @$self, 1, 0, $item;
381 0         0 DEBUG and warn $self->_dump_splice(1);
382 0         0 return $new_priority;
383             }
384              
385             # The item has moved towards an end of the queue, but there are a
386             # lot of items into which it may be inserted. We'll binary seek.
387              
388 1986         1330 my ($upper, $lower);
389 1986 100       2674 if ($delta > 0) {
390 1491         1588 $upper = $#$self; # Last index in @$self.
391 1491         1502 $lower = $item_index;
392             }
393             else {
394 495         376 $upper = $item_index;
395 495         418 $lower = 0;
396             }
397              
398 1986         2829 $self->_insert_item($lower, $upper, $new_priority, $item);
399 1986         4346 return $new_priority;
400             }
401              
402             ### Peek at items that match a filter. Returns a list of payloads
403             ### that match the supplied coderef.
404              
405             sub peek_items {
406 1220     1220 1 2576 my ($self, $filter, $count) = @_;
407 1220 50       3929 $count = @$self unless $count;
408              
409 1220         1627 my @items;
410 1220         1891 my $i = @$self;
411 1220         3757 while ($i--) {
412 6129 100       12910 if ($filter->($self->[$i]->[ITEM_PAYLOAD])) {
413 5864         7452 unshift @items, $self->[$i];
414 5864 100       13723 last unless --$count;
415             }
416             }
417              
418 1220         5182 return @items;
419             }
420              
421             1;
422              
423             __END__