NAME
    Data::Fenwick2D::Shared - shared-memory 2-D Fenwick tree (binary indexed
    tree) for Linux

SYNOPSIS
        use Data::Fenwick2D::Shared;

        # a rows x cols grid of signed 64-bit integers, all 0
        my $grid = Data::Fenwick2D::Shared->new(undef, 24, 7);

        $grid->update(9, 1, 12);       # add 12 at cell (row 9, col 1)
        $grid->update(22, 5, 30);      # add 30 at cell (22, 5)

        $grid->point(22, 5);           # 30  (value at a single cell)
        $grid->prefix(12, 5);          # sum over the rectangle [1..12] x [1..5]
        $grid->rect(8, 1, 12, 5);      # sum over [8..12] x [1..5]
        $grid->total;                  # sum over the whole grid

        $grid->set(9, 1, 100);         # set cell (9,1) to 100 (returns the old value)

        # share the grid across processes via a backing file
        my $shared = Data::Fenwick2D::Shared->new("/tmp/heatmap.f2d", 24, 7);

DESCRIPTION
    A 2-D Fenwick tree (binary indexed tree) in shared memory: a fixed
    "rows" x "cols" grid of signed 64-bit integers that supports point
    update and rectangle-sum query in "O(log rows * log cols)" each. It is
    the compact, update-friendly structure behind 2-D cumulative-frequency
    tables, running heatmaps, and image / grid area-sum queries -- the
    two-dimensional companion to Data::Fenwick::Shared.

    Cells are addressed by a 1-based "(row, col)" pair, "1..rows" by
    "1..cols". "update($x, $y, $delta)" adds a (possibly negative) delta at
    a cell; "prefix($x, $y)" returns the sum of the rectangle from the
    origin, "[1..$x] x [1..$y]"; "rect($x1, $y1, $x2, $y2)" the sum of any
    axis-aligned rectangle (via inclusion-exclusion of four prefix queries);
    "point($x, $y)" a single cell's value; and "total" the sum of the whole
    grid. "set" overwrites a cell with an absolute value.

    The grid lives in a shared mapping, so several processes update and
    query one grid: 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; queries take only the read lock.

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

METHODS
  Constructors
        my $grid = Data::Fenwick2D::Shared->new($path, $rows, $cols);
        my $grid = Data::Fenwick2D::Shared->new(undef, $rows, $cols);      # anonymous
        my $grid = Data::Fenwick2D::Shared->new_memfd($name, $rows, $cols);
        my $grid = Data::Fenwick2D::Shared->new_from_fd($fd);

    $rows and $cols are the grid dimensions (each at least 1, up to 2^24);
    cells are then addressed as "(1..$rows, 1..$cols)". Every cell starts at
    0. "new" and "new_memfd" croak if a dimension is below 1 or above the
    cap. When reopening an existing file or memfd the stored dimensions win
    and the caller's arguments are ignored. 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
        $grid->update($x, $y, $delta);       # add $delta at cell ($x, $y)
        my $old = $grid->set($x, $y, $value); # set cell ($x, $y) to $value; returns the old value
        $grid->clear;                         # reset every cell to 0

    "update" adds a signed delta at a single cell. "set" overwrites a cell
    with an absolute value and returns its previous value (it is "update($x,
    $y, $value - point($x, $y))" done atomically under one lock). Both croak
    if "($x, $y)" is outside "(1..rows, 1..cols)". "clear" zeroes the grid.

  Querying
        my $v = $grid->point($x, $y);            # value at a single cell
        my $s = $grid->prefix($x, $y);           # sum over [1..$x] x [1..$y] (origin rectangle)
        my $s = $grid->rect($x1, $y1, $x2, $y2);  # sum over [$x1..$x2] x [$y1..$y2]
        my $t = $grid->total;                    # sum over the whole grid

    "prefix" returns the cumulative sum of the rectangle anchored at the
    origin; either coordinate may be 0 (an empty rectangle, sum 0). "rect"
    returns the sum of any inclusive axis-aligned rectangle, computed from
    four prefix queries by inclusion-exclusion; it croaks unless "1 <= $x1
    <= $x2 <= rows" and "1 <= $y1 <= $y2 <= cols". "point" is the "1x1"
    rectangle at a cell. "total" is "prefix(rows, cols)". Every query takes
    only the read lock, so many run concurrently.

  Introspection and lifecycle
        $grid->rows; $grid->cols;    # the grid dimensions
        $grid->stats;                # { rows, cols, total, ops, mmap_size }
        $grid->path; $grid->memfd; $grid->sync; $grid->unlink;

    "stats" returns a hash reference with the dimensions, the current grand
    total, the running count of write-path operations, and the mapping size.
    "sync" flushes the mapping to its backing store (a no-op for anonymous
    and memfd grids); "unlink" removes the backing file (also callable as
    "Class->unlink($path)"); "path" returns the backing path ("undef" for
    anonymous, memfd, or fd-reopened grids) and "memfd" the backing
    descriptor.

SHARING ACROSS PROCESSES
    The grid lives in a shared mapping, shared the same three ways as the
    rest of the family: a backing file, an anonymous mapping inherited
    across "fork", or a memfd passed to an unrelated process and reopened
    with new_from_fd($fd). Every process's updates land in the one shared
    grid, and queries take only the read lock so many readers proceed
    concurrently.

SECURITY
    Backing files are created with mode 0600 (owner-only) by default; pass
    an explicit octal mode (e.g. 0660) as the last argument to "new" for
    cross-user sharing. The file is opened with "O_NOFOLLOW" and "O_EXCL",
    and the header is validated on attach. Any process granted write access
    is trusted not to corrupt the mapping.

CRASH SAFETY
    Mutation is guarded by a futex-based write-preferring rwlock with
    PID-encoded ownership and dead-owner recovery. Each update is a short
    bounded "O(log rows * log cols)" walk, so a crash leaves the grid
    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::Fenwick::Shared (the 1-D Fenwick tree: prefix sums, point/range
    update, weighted lookup), 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.

