File Coverage

blib/lib/DiaColloDB/PackedFile/MMap.pm
Criterion Covered Total %
statement 21 170 12.3
branch 0 70 0.0
condition 0 18 0.0
subroutine 7 38 18.4
pod 30 31 96.7
total 58 327 17.7


line stmt bran cond sub pod time code
1             ## -*- Mode: CPerl -*-
2             ## File: DiaColloDB::PackedFile::MMap.pm
3             ## Author: Bryan Jurish <moocow@cpan.org>
4             ## Description: collocation db: flat fixed-length record-oriented files; mmap variant
5              
6             package DiaColloDB::PackedFile::MMap;
7 1     1   7 use DiaColloDB::PackedFile;
  1         53  
  1         31  
8 1     1   6 use DiaColloDB::Utils qw(:fcntl :file :pack);
  1         2  
  1         36  
9 1     1   284 use File::Map qw(map_handle);
  1         1  
  1         6  
10 1     1   128 use Fcntl qw(:DEFAULT :seek);
  1         2  
  1         48  
11 1     1   359 use Carp;
  1         2  
  1         17  
12 1     1   111 use strict;
  1         3  
  1         21  
13 1     1   4 no warnings 'portable';
  1         2  
  1         1922  
14              
15             ##==============================================================================
16             ## Globals & Constants
17              
18             our @ISA = qw(DiaColloDB::PackedFile);
19              
20             ##==============================================================================
21             ## Constructors etc.
22              
23             ## $pf = CLASS_OR_OBJECT->new(%opts)
24             ## + %opts, %$pf:
25             ## ##-- PackedFile: user options
26             ## file => $filename, ##-- default: undef (none)
27             ## flags => $flags, ##-- fcntl flags or open-mode (default='r')
28             ## perms => $perms, ##-- creation permissions (default=(0666 &~umask))
29             ## reclen => $reclen, ##-- record-length in bytes: (default: guess from pack format if available)
30             ## packas => $packas, ##-- pack-format or array; see DiaColloDB::Utils::packFilterStore();
31             ## temp => $bool, ##-- if true, data file(s) will be unlinked on DESTROY
32             ## ##
33             ## ##-- PackedFile: filters
34             ## filter_fetch => $filter, ##-- DB_File-style filter for fetch
35             ## filter_store => $filter, ##-- DB_File-style filter for store
36             ## ##
37             ## ##-- PackedFile: low-level data
38             ## fh => $fh, ##-- underlying filehandle
39             ## ##
40             ## ##-- PackedFile::MMap: buffers
41             ## bufr => \$buf, ##-- mmap $fh
42             ## bufp => $bufp, ##-- current buffer position (logical record number)
43             sub new {
44 0     0 1   my $that = shift;
45 0           return $that->SUPER::new(
46             #$bufr=>undef,
47             #bufp=>0,
48             @_,
49             );
50             }
51              
52              
53             ##==============================================================================
54             ## API: open/close
55              
56             ## $pf = $pf->open()
57             ## $pf = $pf->open($file)
58             ## $pf = $pf->open($file,$flags,%opts)
59             ## + %opts are as for new()
60             ## + $file defaults to $pf->{file}
61             sub open {
62 0     0 1   my ($pf,$file,$flags,%opts) = @_;
63 0 0         $pf->SUPER::open($file,$flags,%opts) or return undef;
64 0 0         return $pf if (!$pf->isa(__PACKAGE__)); ##-- superclass open() promoted us to another class
65 0           $pf->{bufp} = 0;
66 0           return $pf->remap();
67             }
68              
69             ## $bool = $pf->remap()
70             ## + re-maps $pf->{bufr} from $pf->{fh}
71             sub remap {
72 0     0 1   my $pf = shift;
73              
74             ##-- try to ensure filehandle is flushed to disk to handle recent writes
75 0 0 0       if (fcwrite($pf->{flags}//'r')) {
76 0 0         CORE::seek($pf->{fh},0,SEEK_END) or return undef;
77 0 0         CORE::truncate($pf->{fh}, $pf->{fh}->tell) or return undef;
78             }
79 0 0         CORE::seek($pf->{fh},0,SEEK_SET) or return undef;
80              
81             ##-- mmap handles
82 0           my ($buf);
83             ##-- BUGHUNT/birmingham.2016-07: "could not map errors" after 5 calls to remap() (xf.dba2, called from Unigrams::loadTextFile via flush())
84 0           map_handle($buf, $pf->{fh}, fcperl($pf->{flags}));
85 0           $pf->{bufr} = \$buf;
86              
87 0           return $pf;
88             }
89              
90             ## $bool = $pf->opened()
91             sub opened {
92 0     0 1   return defined($_[0]{bufr});
93             }
94              
95             ## $bool = $pf->reopen()
96             ## + re-opens datafile
97             sub reopen {
98 0     0 0   my $pf = shift;
99 0   0       return $pf->SUPER::reopen() && $pf->remap();
100             }
101              
102             ## $bool = $pf->close()
103             sub close {
104 0     0 1   my $pf = shift;
105 0           my $rc = $pf->SUPER::close();
106 0           delete $pf->{bufr};
107 0           return $rc;
108             }
109              
110             ## $bool = $pf->setsize($nrecords)
111             sub setsize {
112 0     0 1   my $pf = shift;
113 0 0         $pf->SUPER::setsize(@_) || return undef;
114 0           $pf->remap();
115             }
116              
117             ## $bool = $pf->truncate()
118             ## + truncates $pf->{fh} or $pf->{file}; otherwise a no-nop
119             sub truncate {
120 0     0 1   my $pf = shift;
121 0 0         $pf->SUPER::truncate(@_) || return undef;
122 0           $pf->remap();
123             }
124              
125             ## $bool = $pf->flush()
126             ## + attempt to flush underlying filehandle, may not work
127             ## + INHERITED
128             sub flush {
129 0     0 1   my $pf = shift;
130 0 0         $pf->SUPER::flush(@_) or return undef;
131 0           $pf->remap();
132             }
133              
134             ##==============================================================================
135             ## API: filters
136             ## + INHERITED from PackedFile
137              
138             ##==============================================================================
139             ## API: positioning
140              
141             ## $nrecords = $pf->size()
142             ## + returns number of records
143             sub size {
144 0 0   0 1   return undef if (!$_[0]{bufr});
145 0           return length(${$_[0]{bufr}})/$_[0]{reclen};
  0            
146             }
147              
148             ## $bool = $pf->seek($recno)
149             ## + seek to record-number $recno
150             sub seek {
151 0     0 1   $_[0]{bufp} = $_[1];
152 0           return 1;
153             }
154              
155             ## $recno = $pf->tell()
156             ## + report current record-number
157             sub tell {
158 0     0 1   return $_[0]{bufp};
159             }
160              
161             ## $bool = $pf->reset();
162             ## + reset position to beginning of file
163             ## + INHERITED from PackedFile
164             sub reset {
165 0     0 1   return $_[0]->seek(0);
166             }
167              
168             ## $bool = $pf->seekend()
169             ## + seek to end-of file
170             sub seekend {
171 0     0 1   return $_[0]->seek($_[0]->size);
172             }
173              
174             ## $bool = $pf->eof()
175             ## + returns true iff current position is end-of-file
176             sub eof {
177 0     0 1   return $_[0]{bufp} >= $_[0]->size;
178             }
179              
180             ##==============================================================================
181             ## API: record access
182              
183             ##--------------------------------------------------------------
184             ## API: record access: read
185              
186             ## $bool = $pf->read(\$buf)
187             ## + read a raw record into \$buf
188             sub read {
189 0     0 1   ${$_[1]} = substr(${$_[0]{bufr}}, $_[0]{bufp}*$_[0]{reclen}, $_[0]{reclen});
  0            
  0            
190 0           ++$_[0]{bufp};
191 0           return length(${$_[1]})==$_[0]{reclen};
  0            
192             }
193              
194             ## $bool = $pf->readraw(\$buf, $nrecords)
195             ## + batch-reads $nrecords into \$buf
196             sub readraw {
197 0     0 1   ${$_[1]} = substr(${$_[0]{bufr}}, $_[0]{bufp}*$_[0]{reclen}, $_[2]*$_[0]{reclen});
  0            
  0            
198 0           $_[0]{bufp} += $_[2];
199 0           return length(${$_[1]})==$_[2]*$_[0]{reclen};
  0            
200             }
201              
202             ## $value_or_undef = $pf->get()
203             ## + get (unpacked) value of current record, increments filehandle position to next record
204             sub get {
205 0     0 1   local $_ = substr(${$_[0]{bufr}}, $_[0]{bufp}*$_[0]{reclen}, $_[0]{reclen});
  0            
206 0 0         return undef if (length($_) != $_[0]{reclen});
207 0           ++$_[0]{bufp};
208 0 0         $_[0]{filter_fetch}->() if ($_[0]{filter_fetch});
209 0           return $_;
210             }
211              
212             ## \$buf_or_undef = $pf->getraw(\$buf)
213             ## + get (packed) value of current record, increments filehandle position to next record
214             sub getraw {
215 0     0 1   ${$_[1]} = substr(${$_[0]{bufr}}, $_[0]{bufp}*$_[0]{reclen}, $_[0]{reclen});
  0            
  0            
216 0           ++$_[0]{bufp};
217 0 0         return undef if (length(${$_[1]}) != $_[0]{reclen});
  0            
218 0           return $_[1];
219             }
220              
221             ## $value_or_undef = $pf->fetch($index)
222             ## + get (unpacked) value of record $index
223             sub fetch {
224 0     0 1   local $_ = substr(${$_[0]{bufr}}, $_[1]*$_[0]{reclen}, $_[0]{reclen});
  0            
225 0           ++$_[0]{bufp};
226 0 0         return undef if (length($_) != $_[0]{reclen});
227 0 0         $_[0]{filter_fetch}->() if ($_[0]{filter_fetch});
228 0           return $_;
229             }
230              
231             ## $buf_or_undef = $pf->fetchraw($index,\$buf)
232             ## + get (packed) value of record $index
233             sub fetchraw {
234 0     0 1   ${$_[2]} = substr(${$_[0]{bufr}}, $_[1]*$_[0]{reclen}, $_[0]{reclen});
  0            
  0            
235 0           ++$_[0]{bufp};
236 0 0         return undef if (length(${$_[2]}) != $_[0]{reclen});
  0            
237 0           return ${$_[2]};
  0            
238             }
239              
240             ##--------------------------------------------------------------
241             ## API: record access: write
242              
243             ## $bool = $pf->write($buf)
244             ## + write a raw record $buf to current position; increments position
245             sub write {
246 0     0 1   $_[0]->logconfess("write(): method not supported");
247             }
248              
249             ## $value_or_undef = $pf->set($value)
250             ## + set (packed) value of current record, increments filehandle position to next record
251             sub set {
252 0     0 1   $_[0]->logconfess("set(): method not supported");
253             }
254              
255             ## $value_or_undef = $pf->store($index,$value)
256             ## + store (packed) $value as record-number $index
257             sub store {
258 0     0 1   $_[0]->logconfess("store(): method not supported");
259             }
260              
261             ## $value_or_undef = $pf->push($value)
262             ## + store (packed) $value at end of record
263             sub push {
264 0     0 1   $_[0]->logconfess("push(): method not supported");
265             }
266              
267             ##==============================================================================
268             ## API: batch I/O
269              
270             ## \@data = $pf->toArray(%opts)
271             ## + read entire contents to an array
272             ## + %opts : override %$pf:
273             ## packas => $packas
274             sub toArray {
275 0     0 1   my ($pf,%opts) = @_;
276 0 0         $pf->setFilters($opts{packas}) if (exists($opts{packas}));
277 0           my ($bufr,$filter_fetch,$reclen) = @$pf{qw(bufr filter_fetch reclen)};
278 0           my @data = qw();
279 0           local $_;
280 0           my $off = 0;
281 0           my $end = length($$bufr);
282 0           for ($off=0; $off < $end; $off += $reclen) {
283 0           $_ = substr($$bufr, $off, $reclen);
284 0 0         $filter_fetch->() if ($filter_fetch);
285 0           CORE::push(@data,$_);
286             }
287 0           $pf->setFilters();
288 0           return \@data;
289             }
290              
291             ## $pf = $pf->fromArray(\@data,%opts)
292             ## + write file contents from an array
293             ## + %opts : override %$pf:
294             ## packas => $packas
295             sub fromArray {
296 0     0 1   my ($pf,$data,%opts) = @_;
297 0 0         $pf->setFilters($opts{packas}) if (exists($opts{packas}));
298 0           local $_;
299 0 0         $pf->setsize(scalar @$data)
300             or $pf->logconfess("fromArray(): failed to set file size = ", scalar(@$data), ": $!");
301 0           my ($bufr,$reclen,$filter_store) = @$pf{qw(bufr reclen filter_store)};
302 0           my $i = 0;
303 0           foreach (@$data) {
304 0 0         $filter_store->() if ($filter_store);
305 0           substr($bufr, $i*$reclen, $reclen) = $_;
306 0           ++$i;
307             }
308 0           $pf->setFilters();
309 0           return $pf;
310             }
311              
312             ## $pdl = $pf->toPdl(%options)
313             ## + returns a piddle for $pf
314             ## + %options:
315             ## type => $pdl_type, ##-- pdl type (default:'auto':guess)
316             ## swap => $bool_or_sub, ##-- byte-swap? (default:'auto':guess)
317             ## mmap => $bool, ##-- mmap data? (default: 0)
318             ## ... ##-- other options passed to DiaColloDB::Utils::readPdlFile()
319             ## + INHERITED from PackedFile
320              
321             ##==============================================================================
322             ## API: binary search
323              
324             ## $nbits_or_undef = $pf->vnbits()
325             ## + returns number of bits for using vec()-style search via Algorithm::BinarySearch::Vec, or undef if not supported
326             ## + currently UNUSED
327             sub vnbits {
328 0     0 1   my $pf = shift;
329 0           my $packas = $pf->{packas};
330 0           my $reclen = $pf->{reclen};
331 0 0         if ($reclen==1) {
    0          
    0          
    0          
332 0           return 8;
333             } elsif ($reclen==2) {
334 0 0         return 16 if (unpack('n',pack($packas,0xfedc)) == 0xfedc);
335             } elsif ($reclen==4) {
336 0 0         return 32 if (unpack('N',pack($packas,0xfedca987)) == 0xfedca987);
337             } elsif ($reclen==8) {
338 0 0         return 64 if (unpack('Q>',pack($packas,0xfedca9876543210f)) == 0xfedca9876543210f);
339             }
340 0           return undef;
341             }
342              
343             ## $index_or_undef = $pf->bsearch($key, %opts)
344             ## + %opts:
345             ## lo => $ilo, ##-- index lower-bound for search (default=0)
346             ## hi => $ihi, ##-- index upper-bound for search (default=size)
347             ## packas => $packas, ##-- key-pack template (default=$pf->{packas})
348             ## + returns the minimum index $i such that unpack($packas,$pf->[$i]) == $key and $ilo <= $j < $i,
349             ## or undef if no such $i exists.
350             ## + $key must be a numeric value, and records must be stored in ascending order
351             ## by numeric value of key (as unpacked by $packas) between $ilo and $ihi
352             ## + TODO: optimize this to use Algorithm::BinarySearch::Vec (only applicable for scalar pack-templates)
353             sub bsearch {
354 0     0 1   my ($pf,$key,%opts) = @_;
355 0   0       my $ilo = $opts{lo} // 0;
356 0   0       my $ihi = $opts{hi} // $pf->size;
357 0   0       my $packas = $opts{packas} // $pf->{packas};
358 0           my $reclen = $pf->{reclen};
359 0           my $bufr = $pf->{bufr};
360              
361             ##-- binary search guts
362 0           my ($imid,$keymid);
363 0           while ($ilo < $ihi) {
364 0           $imid = ($ihi+$ilo) >> 1;
365              
366             ##-- get item[$imid]
367 0           ($keymid) = unpack($packas, substr($$bufr, $imid*$reclen, $reclen));
368              
369 0 0         if ($keymid < $key) {
370 0           $ilo = $imid + 1;
371             } else {
372 0           $ihi = $imid;
373             }
374             }
375              
376 0 0         if ($ilo==$ihi) {
377             ##-- get item[$ilo]
378 0           ($keymid) = unpack($packas, substr($$bufr, $ilo*$reclen, $reclen));
379 0 0         return $ilo if ($keymid == $key);
380             }
381              
382 0           return undef;
383             }
384              
385             ##==============================================================================
386             ## disk usage, timestamp, etc
387             ## + see DiaColloDB::Persistent
388              
389             ## @files = $obj->diskFiles()
390             ## + returns disk storage files, used by du() and timestamp()
391             ## + default implementation returns $obj->{file} or glob("$obj->{base}*")
392             ## + INHERITED from PackedFile
393              
394              
395             ##==============================================================================
396             ## I/O
397             ## + largely INHERITED from DiaColloDB::Persistent, DiaColloDB::PackedFile
398              
399             ##--------------------------------------------------------------
400             ## I/O: header
401             ## + largely INHERITED from DiaColloDB::Persistent
402              
403             ## @keys = $coldb->headerKeys()
404             ## + keys to save as header
405             sub headerKeys {
406 0     0 1   my $pf = shift;
407 0   0       return grep {!ref($_[0]{$_}) && $_ !~ m{^(?:bufp)$}} $pf->SUPER::headerKeys(@_);
  0            
408             }
409              
410             ##--------------------------------------------------------------
411             ## I/O: text
412              
413             ## $bool = $obj->saveTextFile($filename_or_handle, %opts)
414             ## + wraps saveTextFh()
415             ## + INHERITED from DiaColloDB::Persistent
416              
417             ## $bool = $pf->saveTextFh($fh, %opts)
418             ## + save from text file with lines of the form "KEY? VALUE(s)..."
419             ## + %opts:
420             ## keys=>$bool, ##-- do/don't save keys (default=true)
421             ## key2s=>$key2s, ##-- code-ref for key formatting, called as $s=$key2s->($key)
422             sub saveTextFh {
423 0     0 1   my ($pf,$outfh,%opts) = @_;
424 0 0         $pf->logconfess("saveTextFh(): no packed-file opened!") if (!$pf->opened);
425              
426 0           my $key2s = $opts{key2s};
427 0   0       my $keys = $opts{keys} // 1;
428 0           my $bufr = $pf->{bufr};
429 0           my $size = $pf->size;
430 0           my ($i,$key,$val);
431 0           for ($i=0, $pf->reset; $i < $size; ++$i) {
432 0           $val = $pf->get();
433 0 0         $outfh->print(($keys
    0          
    0          
434             ? (($key2s ? $key2s->($i) : $i),"\t")
435             : qw()),
436             (UNIVERSAL::isa($val,'ARRAY') ? join(' ',@$val) : $val),
437             "\n");
438             }
439              
440 0           return $pf;
441             }
442              
443             ## $bool = $obj->loadTextFile($filename_or_handle, %opts)
444             ## + wraps loadTextFh()
445             ## + INHERITED from DiaColloDB::Persistent
446              
447             ## $bool = $pf->loadTextFh($fh, %opts)
448             ## + load from text file with lines of the form "KEY? VALUE(s)..."
449             ## + %opts:
450             ## keys=>$bool, ##-- expect keys in input? (default=true)
451             ## gaps=>$bool, ##-- expect gaps or out-of-order elements in input? (default=false; implies keys=>1)
452             ## + INHERITED from DiaColloDB::Persistent
453              
454              
455             ##==============================================================================
456             ## Footer
457             1;
458              
459             __END__