Data-IntervalTree-Shared
view release on metacpan or search on metacpan
lib/Data/IntervalTree/Shared.pm view on Meta::CPAN
package Data::IntervalTree::Shared;
use strict;
use warnings;
our $VERSION = '0.02';
require XSLoader;
XSLoader::load('Data::IntervalTree::Shared', $VERSION);
sub CLONE_SKIP { 1 } # blessed C-pointer handle: never clone into ithreads (double-free)
1;
__END__
=encoding utf-8
=head1 NAME
Data::IntervalTree::Shared - shared-memory interval tree (overlap / stabbing queries)
=head1 SYNOPSIS
use Data::IntervalTree::Shared;
# up to 100_000 intervals
my $it = Data::IntervalTree::Shared->new(undef, 100_000);
$it->add($start, $end, $booking_id) for @bookings; # each interval carries an id
# which intervals contain a point?
my @at = $it->stab($t); # e.g. "what's booked at time $t"
# which intervals overlap a range?
my @ov = $it->overlaps($lo, $hi); # e.g. "any booking touching [$lo,$hi]"
printf "id %d: [%d, %d]\n", $_->{id}, $_->{lo}, $_->{hi} for @ov;
# share the index across processes via a backing file
my $shared = Data::IntervalTree::Shared->new("/tmp/bookings.it", 100_000);
=head1 DESCRIPTION
An B<interval tree> in shared memory: a set of integer intervals C<[lo, hi]> that
answers B<overlap> and B<stabbing> queries far faster than scanning every
interval -- "which stored intervals contain point C<p>?" and "which overlap the
range C<[lo, hi]>?". It complements L<Data::SegmentTree::Shared> (range-aggregate
over indexed positions) and L<Data::KDTree::Shared> (multi-dimensional points):
this one indexes a B<set of intervals> for containment/overlap. Classic uses:
scheduling and calendar conflict detection, IP-range to owner lookup, genomic
feature overlap, and "what is active at time C<t>".
Endpoints are B<signed 64-bit integers> (timestamps, IP addresses, genomic
coordinates -- exact, with no floating-point edge cases). Each interval carries a
user-supplied 64-bit B<id> (defaulting to its insertion index), returned with
every match. Internally it is an B<augmented balanced binary search tree> keyed
by the low endpoint, each node caching the maximum high endpoint of its subtree
so a query prunes whole subtrees that end before it.
Intervals are B<appended in O(1)> and the balanced tree is B<bulk-built on the
first query> after any insert, so query recursion is O(log n + k) deep (k =
matches) regardless of insertion order -- no risk of a degenerate, deep tree.
Because the intervals live in a shared mapping, B<several processes build and
query one index>: any process that opens the same backing file, inherits the
anonymous mapping across C<fork>, or reopens a passed memfd sees the same
intervals. A write-preferring futex rwlock with dead-process recovery guards
mutation; once the tree is built, queries take only the read lock. B<Linux-only>.
Requires 64-bit Perl.
The index has a fixed B<capacity>; adding beyond it croaks. Memory is
C<capacity * 40> bytes for the intervals plus a build scratch of C<capacity * 4>
bytes and a fixed header.
=head1 METHODS
=head2 Constructors
my $it = Data::IntervalTree::Shared->new($path, $capacity, $mode);
my $it = Data::IntervalTree::Shared->new(undef, $capacity);
my $it = Data::IntervalTree::Shared->new_memfd($name, $capacity);
my $it = Data::IntervalTree::Shared->new_from_fd($fd);
my $ro = Data::IntervalTree::Shared->new_readonly($path); # frozen file, read-only
C<$capacity> is the maximum number of intervals (1..2^24). C<new> and
C<new_memfd> croak on an out-of-range C<$capacity>. When reopening an existing
file or memfd the stored geometry wins and the caller's argument does not
resize it, though it is still range-checked and an out-of-range value croaks;
reopening a B<sealed> (frozen) file read-write is refused -- use
C<new_readonly> instead (see L</"FROZEN (READ-ONLY) MODE">). An optional file
B<mode> may be passed as the last argument to C<new> (e.g. C<0660>)
for cross-user sharing; it defaults to C<0600> (owner-only).
=head2 Adding intervals
my $i = $it->add($lo, $hi); # id defaults to the insertion index
my $i = $it->add($lo, $hi, $id); # attach an explicit 64-bit id
$it->build; # (optional) force a rebuild now
C<add> appends one interval with integer endpoints C<$lo E<lt>= $hi> (croaks
otherwise) and an optional integer C<$id>, returning its insertion index; it
croaks if the tree is full. Intervals are treated as B<closed> (both endpoints
inclusive). C<build> forces the balanced tree to be (re)built immediately; you
rarely need it, since queries build automatically after inserts.
=head2 Queries
my @at = $it->stab($point); # intervals containing $point (lo <= p <= hi)
my @ov = $it->overlaps($lo, $hi); # intervals overlapping [$lo, $hi]
C<stab> returns every stored interval that B<contains> the point C<$point>.
C<overlaps> returns every stored interval that B<intersects> the closed range
C<[$lo, $hi]> (i.e. C<interval.lo E<lt>= $hi> and C<interval.hi E<gt>= $lo>);
C<$lo E<gt> $hi> croaks. Both return a list of hash references
C<< { id => ..., lo => ..., hi => ... } >>, sorted by C<lo> ascending. A point
stab is exactly C<overlaps($p, $p)>.
=head2 Introspection and lifecycle
$it->count; # number of intervals added
$it->capacity; # maximum number of intervals
$it->clear; # remove all intervals
$it->stats; # { count, capacity, dirty, ops, mmap_size, frozen, readonly }
$it->frozen; # 1 if sealed by freeze, else 0
$it->readonly; # 1 if this handle is a read-only view, else 0
$it->path; $it->memfd; $it->sync; $it->unlink;
C<clear> empties the index. C<sync> flushes the mapping to its backing store (a
no-op for anonymous and memfd trees, and for any read-only view); C<unlink>
removes the backing file (also callable as C<< Class->unlink($path) >>);
C<path> returns the backing path (C<undef> for anonymous, memfd, or
fd-reopened trees) and C<memfd> the backing descriptor. C<frozen> and
C<readonly> report whether the tree has been sealed and whether this handle is
a read-only view, respectively (see L</"FROZEN (READ-ONLY) MODE">).
=head1 SHARING ACROSS PROCESSES
The index lives in a shared mapping, shared the same three ways as the rest of
the family: a B<backing file>, an B<anonymous mapping inherited across
C<fork>>, or a B<memfd> passed to an unrelated process and reopened with C<<
new_from_fd($fd) >>. The descriptor you pass is duplicated
(C<F_DUPFD_CLOEXEC>), so it stays yours to close and closing it does not
disturb the handle. Any process can add intervals; the first query after an
add rebuilds the shared tree once (under the write lock), and subsequent
queries run concurrently under the read lock.
=head1 FROZEN (READ-ONLY) MODE
A file-backed tree can be B<frozen> and then shipped to other machines, where
consumers open it B<read-only> and query it with B<no locking at all>.
# producer: build, freeze, ship the file
my $it = Data::IntervalTree::Shared->new("/tmp/bookings.it", 100_000);
$it->add($_->[0], $_->[1], $_->[2]) for @known;
$it->freeze; # seal: now immutable, and $it itself is read-only
# ... copy /tmp/bookings.it to another host ...
# consumer (any process, same architecture): read-only, lock-free
my $ro = Data::IntervalTree::Shared->new_readonly("/tmp/bookings.it");
$ro->stab($t); # or $ro->overlaps($lo, $hi)
C<freeze> takes the write lock, force-completes any balanced-tree build still
( run in 1.609 second using v1.01-cache-2.11-cpan-9789f410c06 )