NAME
    Data::Fenwick::Shared - shared-memory Fenwick tree (binary indexed tree;
    point or range update) for Linux

SYNOPSIS
        use Data::Fenwick::Shared;

        # a tree over positions 1..1_000_000, anonymous mapping
        my $fen = Data::Fenwick::Shared->new(undef, 1_000_000);

        $fen->update(5, 3);        # add 3 at position 5
        $fen->update(9, 7);        # add 7 at position 9

        $fen->prefix(9);           # 10  (sum of positions 1..9)
        $fen->range(5, 9);         # 10  (sum of positions 5..9)
        $fen->point(5);            # 3   (value at position 5)
        $fen->total;               # 10  (sum of all positions)

        $fen->set(5, 100);         # set position 5 to 100 (returns the old value)

        # rank / weighted lookup: smallest position whose prefix sum reaches a target
        $fen->find(50);            # first position i with prefix(i) >= 50

        # share across processes via a backing file
        my $shared = Data::Fenwick::Shared->new("/tmp/counts.fen", 1_000_000);

        # range-update mode: add to a whole range in O(log n), then query ranges
        my $rng = Data::Fenwick::Shared->new_range(undef, 1_000_000);
        $rng->range_add(10, 20, 5);   # add 5 to every position in [10, 20]
        $rng->range(10, 20);          # 55  (sum over the range)

DESCRIPTION
    A Fenwick tree (binary indexed tree) in shared memory: a fixed-size
    array of "n" signed 64-bit integer positions that supports point update
    and prefix-sum query in "O(log n)" each, plus an "O(log n)" binary
    search for the position at which a running total is reached. It is the
    compact, update-friendly structure behind cumulative-frequency tables,
    running rank/order statistics, and weighted random sampling.

    Positions are numbered 1 to n (1-indexed). "update($i, $delta)" adds a
    (possibly negative) delta at position $i; prefix($i) returns the sum of
    positions "1..$i"; "range($l, $r)" the sum of "$l..$r"; point($i) the
    current value at a single position; and "total" the sum of everything.
    "set" overwrites a position with an absolute value. find($target)
    returns the smallest position whose prefix sum is at least $target
    (meaningful when all stored values are non-negative) -- the operation
    that turns a Fenwick tree into a weighted sampler or a rank index.

    The tree lives in a shared mapping, so several processes update and
    query one structure: any process that opens the same backing file,
    inherits the anonymous mapping across "fork", or reopens a passed memfd
    sees the others' updates and contributes its own. A write-preferring
    futex rwlock with dead-process recovery guards mutation, so many
    processes may "update" and query concurrently. Two trees of equal size
    "n" can be "merge"d by element-wise addition (a Fenwick tree is linear,
    so the merge of tree(A) and tree(B) is tree(A+B)).

    Values are signed 64-bit integers; sums that overflow 64 bits wrap, as
    with any native integer arithmetic. Memory is "(n+1) * 8" bytes for the
    tree plus a fixed header. Linux-only. Requires 64-bit Perl.

  Range-update mode
    A tree created with "new_range" supports range update as well as range
    query: "range_add($l, $r, $delta)" adds a delta to every position in
    "[$l, $r]" in "O(log n)", and "prefix"/"range"/"point"/"total" report
    the resulting sums. It uses the classic two-BIT technique (a second
    binary indexed tree tracking the weighted difference), so a range-mode
    tree costs twice the memory ("2 * (n+1) * 8" bytes) but adds O(log n)
    range updates a plain Fenwick tree cannot do. "update($i, $delta)" and
    "set" still work (a point update is just "range_add($i, $i, $delta)").
    "find" and "merge" are not available in range mode (the two-BIT layout
    has no single-BIT binary lift); use a point tree for those. The mode is
    recorded in the header, so a reopened segment stays range mode. Note
    that the 0.02 on-disk format is incompatible with 0.01: a file created
    by 0.01 cannot be opened and must be recreated.

METHODS
  Constructors
        my $fen = Data::Fenwick::Shared->new($path, $n);
        my $fen = Data::Fenwick::Shared->new(undef, $n);            # anonymous
        my $fen = Data::Fenwick::Shared->new_memfd($name, $n);
        my $fen = Data::Fenwick::Shared->new_from_fd($fd);

        # range-update mode (two BITs) -- same arguments
        my $fen = Data::Fenwick::Shared->new_range($path, $n);
        my $fen = Data::Fenwick::Shared->new_range_memfd($name, $n);

    $path is the backing file ("undef" or omitted for an anonymous mapping).
    $n is the number of positions (at least 1); positions are then addressed
    as "1..$n". "new" and "new_memfd" croak if $n is less than 1 or exceeds
    the tree cap. When reopening an existing file or memfd, the stored "n"
    wins and the caller's $n argument is ignored -- but a positive $n
    placeholder is still required, since the constructor validates $n before
    the stored value wins. "new_memfd" creates a Linux memfd (transferable
    via its "memfd" descriptor); "new_from_fd" reopens one in another
    process. An optional file mode may be passed as the last argument to
    "new" (e.g. 0660) to opt a newly-created backing file into cross-user
    sharing; it defaults to 0600 (owner-only).

  Updating
        $fen->update($i, $delta);        # add $delta at position $i (1 <= $i <= n)
        $fen->range_add($l, $r, $delta);  # add $delta to every position in [$l, $r] (range mode)
        my $old = $fen->set($i, $value);  # set position $i to $value; returns the old value
        $fen->clear;                      # reset every position to 0

    "range_add" adds a delta to a whole inclusive range in "O(log n)" and
    requires a range-mode tree ("new_range"); it croaks on a point-mode
    tree. "update" adds a signed delta at a single position and returns
    nothing. "set" overwrites a position with an absolute value and returns
    its previous value (it is "update($i, $value - point($i))" done
    atomically under one lock). Both croak if $i is outside "1..n". "clear"
    zeroes the whole tree.

  Querying
        my $s = $fen->prefix($i);       # sum of positions 1..$i (0 <= $i <= n; prefix(0) == 0)
        my $s = $fen->range($l, $r);    # sum of positions $l..$r (1 <= $l <= $r <= n)
        my $v = $fen->point($i);        # value at position $i
        my $t = $fen->total;            # sum of all positions (== prefix(n))
        my $i = $fen->find($target);    # smallest position with prefix >= $target

    "prefix", "range", "point", and "total" are "O(log n)" reads returning
    signed integers. "find" binary-searches the tree for the smallest
    position whose prefix sum is at least $target, returning that position
    or "n+1" if no prefix reaches it. "find" is only meaningful when every
    stored value is non-negative (a cumulative distribution): it is the core
    of weighted sampling (draw $target uniformly in "[1, total]" and "find"
    the bucket) and of order-statistic / rank queries. Out-of-range
    positions croak. "find" requires a point-mode tree (it croaks in range
    mode).

  Merging, introspection, lifecycle
        $fen->merge($other);            # element-wise add (point mode; both must have equal n)
        $fen->size;                     # n, the number of positions
        $fen->is_range;                 # true for a range-mode (two-BIT) tree
        $fen->stats;                    # { size, total, ops, mmap_size, range }
        $fen->path; $fen->memfd; $fen->sync; $fen->unlink;

    "merge" adds another tree's contents into this one position by position;
    both trees must have the same "n" or it croaks, and both must be
    point-mode ("merge" croaks in range mode). The other tree is snapshotted
    under its own read lock, so two processes may merge concurrently without
    deadlock. "is_range" reports whether the tree is range mode. "size"
    (also "capacity") is "n". "sync" flushes the mapping to its backing
    store (a no-op for anonymous and memfd trees); "unlink" removes the
    backing file (also callable as "Class->unlink($path)"); "path" returns
    the backing path ("undef" for anonymous, memfd, or fd-reopened trees)
    and "memfd" the backing descriptor -- the memfd of a "new_memfd" tree or
    the dup'd fd of a "new_from_fd" tree, and -1 for file-backed or
    anonymous trees.

STATS
    stats() returns a hashref: "size" (the number of positions "n"), "total"
    (the current sum of all positions), "ops" (running count of write-path
    calls -- "update", "range_add", "set", "merge", "clear"), "mmap_size"
    (bytes of the shared mapping), and "range" (1 for a range-mode tree, 0
    for a point-mode tree).

SHARING ACROSS PROCESSES
    The tree lives in a shared mapping, shared the same three ways as the
    rest of the family: a backing file (every process calls "new($path, $n)"
    on the same path with a matching $n), an anonymous mapping inherited
    across "fork", or a memfd whose descriptor is passed to an unrelated
    process (over a UNIX socket via "SCM_RIGHTS", or via "/proc/$pid/fd/$n")
    and reopened with new_from_fd($fd). Because the mapping is shared, every
    process updates and queries the same tree.

        # producer and consumer share one running-sum tree with no coordination
        my $fen = Data::Fenwick::Shared->new(undef, 1000);   # before fork
        unless (fork) { $fen->update($_, 1) for 1 .. 500; exit }
        wait;
        print $fen->total, "\n";   # 500 -- the child's updates

SECURITY
    Backing files are created with mode 0600 (owner-only) by default. To
    share a backing file across users, pass an explicit octal file mode such
    as 0660 as the last argument to "new"; the mode is applied only when the
    file is created. The file is opened with "O_NOFOLLOW" (a symlink at the
    path is refused) and "O_EXCL"; the on-disk header is validated when the
    file is attached. Any process you grant write access to a shared mapping
    is trusted not to corrupt it while others are using it.

CRASH SAFETY
    Mutation is guarded by a futex-based write-preferring rwlock with
    PID-encoded ownership; if a holder dies, the next contender detects the
    dead owner and recovers. Each "update" is a short O(log n) sequence of
    int64 stores, so a crash leaves the tree consistent up to the last
    completed operation. Limitation: PID reuse is not detected (very
    unlikely in practice).

    Reader-slot exhaustion (slotless readers): dead-process recovery
    attributes a crashed lock holder's contribution through its reader-slot.
    The slot table holds 1024 entries (one per concurrent reader process).
    If more than that many reader processes share one mapping at once, a
    reader that cannot claim a slot proceeds "slotless" -- it still takes
    the read lock but leaves no per-process record. If such a slotless
    reader is then killed while holding the read lock, its share of the lock
    cannot be attributed to a dead process, so writer recovery cannot
    reclaim it and writers may block until the mapping is recreated.
    Reaching this needs more than 1024 concurrent reader processes on one
    mapping plus a crash in the brief read-lock window; the dead-process
    slot reclaim keeps the table from filling with stale entries, so in
    practice it is very unlikely.

SEE ALSO
    Data::SortedSet::Shared (order-statistics ZSET), Data::NDArray::Shared
    (dense numeric arrays), Data::Histogram::Shared (HdrHistogram), and the
    rest of the "Data::*::Shared" family.

AUTHOR
    vividsnow

LICENSE
    This is free software; you can redistribute it and/or modify it under
    the same terms as Perl itself.

