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 179     179   32074 use strict;
  179         212  
  179         6071  
6              
7 179     179   645 use vars qw($VERSION @ISA);
  179         185  
  179         9743  
8             $VERSION = '1.366'; # NOTE - Should be #.### (three decimal places)
9             @ISA = qw(POE::Queue);
10              
11 179     179   1259 use Errno qw(ESRCH EPERM);
  179         2329  
  179         6939  
12 179     179   671 use Carp qw(confess);
  179         222  
  179         17413  
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 179     179   314 my $package = caller();
24 179     179   828 no strict 'refs';
  179         215  
  179         243900  
25 179         338 *{ $package . '::ITEM_PRIORITY' } = \&ITEM_PRIORITY;
  179         892  
26 179         269 *{ $package . '::ITEM_ID' } = \&ITEM_ID;
  179         788  
27 179         273 *{ $package . '::ITEM_PAYLOAD' } = \&ITEM_PAYLOAD;
  179         722  
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 177     177 1 675 bless [], shift();
39             }
40              
41             ### Add an item to the queue. Returns the new item's ID.
42              
43             sub enqueue {
44 7434     7434 1 26687 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 7434         6543 my $item_id;
51 7434         20119 1 while exists $item_priority{$item_id = ++$queue_seq};
52 7434         18734 $item_priority{$item_id} = $priority;
53              
54 7434         15279 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 7434 100       14733 unless (@$self) {
62 1322         2130 $self->[0] = $item_to_enqueue;
63 1322         1240 DEBUG and warn $self->_dump_splice(0);
64 1322         3444 return $item_id;
65             }
66              
67             # Special case: The new item belongs at the end of the queue.
68 6112 100       15979 if ($priority >= $self->[-1]->[ITEM_PRIORITY]) {
69 1125         1709 push @$self, $item_to_enqueue;
70 1125         1151 DEBUG and warn $self->_dump_splice(@$self-1);
71 1125         2996 return $item_id;
72             }
73              
74             # Special case: The new item belongs at the head of the queue.
75 4987 100       10179 if ($priority < $self->[0]->[ITEM_PRIORITY]) {
76 712         1559 unshift @$self, $item_to_enqueue;
77 712         1148 DEBUG and warn $self->_dump_splice(0);
78 712         2772 return $item_id;
79             }
80              
81             # Special case: There are only two items in the queue. This item
82             # naturally belongs between them.
83 4275 100       8175 if (@$self == 2) {
84 89         250 splice @$self, 1, 0, $item_to_enqueue;
85 89         103 DEBUG and warn $self->_dump_splice(1);
86 89         275 return $item_id;
87             }
88              
89             # And finally we have a nontrivial queue. Insert the item using a
90             # binary seek.
91              
92 4186         8980 $self->_insert_item(0, $#$self, $priority, $item_to_enqueue);
93 4186         12862 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 5432     5432 1 619323 my $self = shift;
102              
103 5432 100       11961 return unless @$self;
104 5427         5646 my ($priority, $id, $stuff) = @{shift @$self};
  5427         13177  
105 5427         16699 delete $item_priority{$id};
106 5427         17225 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 14190   100 14190 1 70752 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 5520     5520 1 9586 return scalar @{$_[0]};
  5520         26118  
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 6162     6162   9779 my ($self, $lower, $upper, $priority, $item) = @_;
131              
132 6162         5784 while (1) {
133 54817         48159 my $midpoint = ($upper + $lower) >> 1;
134              
135             # Upper and lower bounds crossed. Insert at the lower point.
136 54817 100       74203 if ($upper < $lower) {
137 6162         12729 splice @$self, $lower, 0, $item;
138 6162         4645 DEBUG and warn $self->_dump_splice($lower);
139 6162         8518 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 48655 100       84098 if ($priority < $self->[$midpoint]->[ITEM_PRIORITY]) {
145 22078         17487 $upper = $midpoint - 1;
146 22078         20078 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 26577         21803 $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   4258 my ($self, $id, $priority) = @_;
162              
163             # Use a binary seek.
164              
165 3764         4573 my $upper = $#$self; # Last index of @$self.
166 3764         3619 my $lower = 0;
167 3764         3227 while (1) {
168 40107         37943 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 40107 100       58616 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 36343 100       67984 if ($priority < $self->[$midpoint]->[ITEM_PRIORITY]) {
177 19286         16013 $upper = $midpoint - 1;
178 19286         18122 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 17057         15033 $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         6644 while ($lower-- >= 0) {
190 4079 100       11257 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 2139 my ($self, $id, $filter) = @_;
202              
203 234         314 my $priority = $item_priority{$id};
204 234 100       363 unless (defined $priority) {
205 2         4 $! = ESRCH;
206 2         8 return;
207             }
208              
209             # Find that darn item.
210 232         381 my $item_index = $self->_find_item($id, $priority);
211              
212             # Test the item against the filter.
213 232 100       516 unless ($filter->($self->[$item_index]->[ITEM_PAYLOAD])) {
214 1         5 $! = EPERM;
215 1         6 return;
216             }
217              
218             # Remove the item, and return it.
219 231         447 delete $item_priority{$id};
220 231         201 return @{splice @$self, $item_index, 1};
  231         776  
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 4723     4723 1 6771 my ($self, $filter, $count) = @_;
233 4723 100       9788 $count = @$self unless $count;
234              
235 4723         4817 my @items;
236 4723         5073 my $i = @$self;
237 4723         9760 while ($i--) {
238 801155 100       1182106 if ($filter->($self->[$i]->[ITEM_PAYLOAD])) {
239 1629         3489 my $removed_item = splice(@$self, $i, 1);
240 1629         4888 delete $item_priority{$removed_item->[ITEM_ID]};
241 1629         4287 unshift @items, $removed_item;
242 1629 100       4421 last unless --$count;
243             }
244             }
245              
246 4723         24479 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 8364 my ($self, $id, $filter, $delta) = @_;
255              
256 2032         2950 my $old_priority = $item_priority{$id};
257 2032 50       3501 unless (defined $old_priority) {
258 0         0 $! = ESRCH;
259 0         0 return;
260             }
261              
262             # Find that darn item.
263 2032         3049 my $item_index = $self->_find_item($id, $old_priority);
264              
265             # Test the item against the filter.
266 2032 100       5116 unless ($filter->($self->[$item_index]->[ITEM_PAYLOAD])) {
267 1000         4311 $! = EPERM;
268 1000         2061 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       4801 return $self->[$item_index]->[ITEM_PRIORITY] unless $delta;
275              
276             # Remove the item, and adjust its priority.
277 1032         2098 my $item = splice(@$self, $item_index, 1);
278 1032         1308 my $new_priority = $item->[ITEM_PRIORITY] += $delta;
279 1032         1221 $item_priority{$id} = $new_priority;
280              
281 1032         1788 $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 10413 my ($self, $id, $filter, $new_priority) = @_;
290              
291 2000         3049 my $old_priority = $item_priority{$id};
292 2000 50       3538 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       3909 return $new_priority if $new_priority == $old_priority;
301              
302             # Find that darn item.
303 1500         2462 my $item_index = $self->_find_item($id, $old_priority);
304              
305             # Test the item against the filter.
306 1500 100       3969 unless ($filter->($self->[$item_index]->[ITEM_PAYLOAD])) {
307 500         2780 $! = EPERM;
308 500         1292 return;
309             }
310              
311             # Remove the item, and calculate the delta.
312 1000         5404 my $item = splice(@$self, $item_index, 1);
313 1000         1174 my $delta = $new_priority - $old_priority;
314 1000         1457 $item->[ITEM_PRIORITY] = $item_priority{$id} = $new_priority;
315              
316 1000         1880 $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   2406 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       3474 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       4068 if ($new_priority >= $self->[-1]->[ITEM_PRIORITY]) {
364 35         55 push @$self, $item;
365 35         28 DEBUG and warn $self->_dump_splice(@$self-1);
366 35         93 return $new_priority;
367             }
368              
369             # Special case: The item belongs at the head of the queue.
370 1997 100       3721 if ($new_priority < $self->[0]->[ITEM_PRIORITY]) {
371 21         50 unshift @$self, $item;
372 21         18 DEBUG and warn $self->_dump_splice(0);
373 21         52 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 1976 50       3101 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 1976         1639 my ($upper, $lower);
389 1976 100       2949 if ($delta > 0) {
390 1480         1636 $upper = $#$self; # Last index in @$self.
391 1480         1710 $lower = $item_index;
392             }
393             else {
394 496         456 $upper = $item_index;
395 496         482 $lower = 0;
396             }
397              
398 1976         3188 $self->_insert_item($lower, $upper, $new_priority, $item);
399 1976         5245 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 1215     1215 1 1964 my ($self, $filter, $count) = @_;
407 1215 50       3363 $count = @$self unless $count;
408              
409 1215         1373 my @items;
410 1215         1633 my $i = @$self;
411 1215         3127 while ($i--) {
412 6146 100       10775 if ($filter->($self->[$i]->[ITEM_PAYLOAD])) {
413 5881         6226 unshift @items, $self->[$i];
414 5881 100       12363 last unless --$count;
415             }
416             }
417              
418 1215         4614 return @items;
419             }
420              
421             1;
422              
423             __END__