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   23996 use strict;
  178         223  
  178         6233  
6              
7 178     178   701 use vars qw($VERSION @ISA);
  178         204  
  178         10414  
8             $VERSION = '1.365'; # NOTE - Should be #.### (three decimal places)
9             @ISA = qw(POE::Queue);
10              
11 178     178   1278 use Errno qw(ESRCH EPERM);
  178         1865  
  178         8044  
12 178     178   767 use Carp qw(confess);
  178         232  
  178         18706  
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   377 my $package = caller();
24 178     178   879 no strict 'refs';
  178         277  
  178         264592  
25 178         283 *{ $package . '::ITEM_PRIORITY' } = \&ITEM_PRIORITY;
  178         950  
26 178         273 *{ $package . '::ITEM_ID' } = \&ITEM_ID;
  178         830  
27 178         270 *{ $package . '::ITEM_PAYLOAD' } = \&ITEM_PAYLOAD;
  178         822  
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 700 bless [], shift();
39             }
40              
41             ### Add an item to the queue. Returns the new item's ID.
42              
43             sub enqueue {
44 7418     7418 1 24906 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 7418         6577 my $item_id;
51 7418         19679 1 while exists $item_priority{$item_id = ++$queue_seq};
52 7418         18120 $item_priority{$item_id} = $priority;
53              
54 7418         15012 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 7418 100       14355 unless (@$self) {
62 1316         2095 $self->[0] = $item_to_enqueue;
63 1316         1150 DEBUG and warn $self->_dump_splice(0);
64 1316         3513 return $item_id;
65             }
66              
67             # Special case: The new item belongs at the end of the queue.
68 6102 100       16100 if ($priority >= $self->[-1]->[ITEM_PRIORITY]) {
69 1119         1998 push @$self, $item_to_enqueue;
70 1119         1328 DEBUG and warn $self->_dump_splice(@$self-1);
71 1119         3229 return $item_id;
72             }
73              
74             # Special case: The new item belongs at the head of the queue.
75 4983 100       10701 if ($priority < $self->[0]->[ITEM_PRIORITY]) {
76 736         1531 unshift @$self, $item_to_enqueue;
77 736         843 DEBUG and warn $self->_dump_splice(0);
78 736         2602 return $item_id;
79             }
80              
81             # Special case: There are only two items in the queue. This item
82             # naturally belongs between them.
83 4247 100       8274 if (@$self == 2) {
84 78         218 splice @$self, 1, 0, $item_to_enqueue;
85 78         91 DEBUG and warn $self->_dump_splice(1);
86 78         243 return $item_id;
87             }
88              
89             # And finally we have a nontrivial queue. Insert the item using a
90             # binary seek.
91              
92 4169         8318 $self->_insert_item(0, $#$self, $priority, $item_to_enqueue);
93 4169         11630 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 5419     5419 1 758837 my $self = shift;
102              
103 5419 100       12383 return unless @$self;
104 5414         5428 my ($priority, $id, $stuff) = @{shift @$self};
  5414         12919  
105 5414         16594 delete $item_priority{$id};
106 5414         16565 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 14213   100 14213 1 72828 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 5490     5490 1 9808 return scalar @{$_[0]};
  5490         27371  
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 6145     6145   8435 my ($self, $lower, $upper, $priority, $item) = @_;
131              
132 6145         5266 while (1) {
133 54830         43122 my $midpoint = ($upper + $lower) >> 1;
134              
135             # Upper and lower bounds crossed. Insert at the lower point.
136 54830 100       68063 if ($upper < $lower) {
137 6145         10435 splice @$self, $lower, 0, $item;
138 6145         4544 DEBUG and warn $self->_dump_splice($lower);
139 6145         8079 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 48685 100       75583 if ($priority < $self->[$midpoint]->[ITEM_PRIORITY]) {
145 21893         16077 $upper = $midpoint - 1;
146 21893         18883 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 26792         20216 $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   3394 my ($self, $id, $priority) = @_;
162              
163             # Use a binary seek.
164              
165 3764         3979 my $upper = $#$self; # Last index of @$self.
166 3764         3010 my $lower = 0;
167 3764         2647 while (1) {
168 40119         29503 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 40119 100       47277 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 36355 100       54010 if ($priority < $self->[$midpoint]->[ITEM_PRIORITY]) {
177 19234         13151 $upper = $midpoint - 1;
178 19234         14644 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 17121         12564 $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         5249 while ($lower-- >= 0) {
190 3979 100       8504 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 2252 my ($self, $id, $filter) = @_;
202              
203 234         348 my $priority = $item_priority{$id};
204 234 100       444 unless (defined $priority) {
205 2         5 $! = ESRCH;
206 2         8 return;
207             }
208              
209             # Find that darn item.
210 232         372 my $item_index = $self->_find_item($id, $priority);
211              
212             # Test the item against the filter.
213 232 100       605 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         485 delete $item_priority{$id};
220 231         198 return @{splice @$self, $item_index, 1};
  231         809  
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 4725     4725 1 6838 my ($self, $filter, $count) = @_;
233 4725 100       10846 $count = @$self unless $count;
234              
235 4725         4633 my @items;
236 4725         5022 my $i = @$self;
237 4725         10461 while ($i--) {
238 801047 100       1313191 if ($filter->($self->[$i]->[ITEM_PAYLOAD])) {
239 1626         3821 my $removed_item = splice(@$self, $i, 1);
240 1626         5301 delete $item_priority{$removed_item->[ITEM_ID]};
241 1626         2542 unshift @items, $removed_item;
242 1626 100       5884 last unless --$count;
243             }
244             }
245              
246 4725         25162 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 6120 my ($self, $id, $filter, $delta) = @_;
255              
256 2032         2379 my $old_priority = $item_priority{$id};
257 2032 50       2837 unless (defined $old_priority) {
258 0         0 $! = ESRCH;
259 0         0 return;
260             }
261              
262             # Find that darn item.
263 2032         2177 my $item_index = $self->_find_item($id, $old_priority);
264              
265             # Test the item against the filter.
266 2032 100       3436 unless ($filter->($self->[$item_index]->[ITEM_PAYLOAD])) {
267 1000         2987 $! = EPERM;
268 1000         1451 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       3416 return $self->[$item_index]->[ITEM_PRIORITY] unless $delta;
275              
276             # Remove the item, and adjust its priority.
277 1032         1644 my $item = splice(@$self, $item_index, 1);
278 1032         947 my $new_priority = $item->[ITEM_PRIORITY] += $delta;
279 1032         943 $item_priority{$id} = $new_priority;
280              
281 1032         1305 $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 7635 my ($self, $id, $filter, $new_priority) = @_;
290              
291 2000         2380 my $old_priority = $item_priority{$id};
292 2000 50       2734 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       3044 return $new_priority if $new_priority == $old_priority;
301              
302             # Find that darn item.
303 1500         1670 my $item_index = $self->_find_item($id, $old_priority);
304              
305             # Test the item against the filter.
306 1500 100       2792 unless ($filter->($self->[$item_index]->[ITEM_PAYLOAD])) {
307 500         1835 $! = EPERM;
308 500         918 return;
309             }
310              
311             # Remove the item, and calculate the delta.
312 1000         3868 my $item = splice(@$self, $item_index, 1);
313 1000         917 my $delta = $new_priority - $old_priority;
314 1000         1138 $item->[ITEM_PRIORITY] = $item_priority{$id} = $new_priority;
315              
316 1000         1356 $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   2009 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       2864 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       3049 if ($new_priority >= $self->[-1]->[ITEM_PRIORITY]) {
364 33         42 push @$self, $item;
365 33         25 DEBUG and warn $self->_dump_splice(@$self-1);
366 33         64 return $new_priority;
367             }
368              
369             # Special case: The item belongs at the head of the queue.
370 1999 100       3072 if ($new_priority < $self->[0]->[ITEM_PRIORITY]) {
371 23         32 unshift @$self, $item;
372 23         17 DEBUG and warn $self->_dump_splice(0);
373 23         49 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       2637 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         1355 my ($upper, $lower);
389 1976 100       2471 if ($delta > 0) {
390 1482         1374 $upper = $#$self; # Last index in @$self.
391 1482         1240 $lower = $item_index;
392             }
393             else {
394 494         349 $upper = $item_index;
395 494         401 $lower = 0;
396             }
397              
398 1976         2412 $self->_insert_item($lower, $upper, $new_priority, $item);
399 1976         3670 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 1162     1162 1 2069 my ($self, $filter, $count) = @_;
407 1162 50       3228 $count = @$self unless $count;
408              
409 1162         1277 my @items;
410 1162         1480 my $i = @$self;
411 1162         2962 while ($i--) {
412 6079 100       11771 if ($filter->($self->[$i]->[ITEM_PAYLOAD])) {
413 5814         6848 unshift @items, $self->[$i];
414 5814 100       13322 last unless --$count;
415             }
416             }
417              
418 1162         4283 return @items;
419             }
420              
421             1;
422              
423             __END__