Acme-Parataxis

 view release on metacpan or  search on metacpan

CONTRIBUTING.md  view on Meta::CPAN


This project uses **native Perl `class` syntax** (`use v5.40`). Please keep that style and avoid adding comments unless they clarify non-obvious behavior.

We use [Test2::V0](https://metacpan.org/pod/Test2::V0) for tests. Follow the existing test structure when adding coverage.

Before submitting, please:

1. **Run the full suite** and confirm it passes.
2. Format your code so it matches the surrounding style (the repo has a
   `[Code::TidyAll](https://metacpan.org/pod/Code::TidyAll)` configuration in `.tidyallrc`).
3. Add a changelog entry under `## [Unreleased]` in `Changes.md`.

If you change the public API of a class, update the corresponding `.pod` and any examples under `eg/`.

## Submitting changes

1. Fork the repository on GitHub.
2. Create a feature branch: `git checkout -b my-change`
3. Commit your changes with a clear, concise message (see the existing `git log` for style).
4. Push your branch and open a Pull Request against `main` (previously `master`).

Please make sure your PR:

- Describes the problem being solved
- References any related issue
- Includes tests for any new behavior
- Does not push version-number bumps or changelog edits unless they are part of the change (maintainers handle release versions).

## Reporting bugs

Open a [GitHub issue](https://github.com/sanko/Acme-Parataxis.pm/issues) with:

- Perl version (`perl -V`, `$^O`)
- A minimal, reproducible example
- The output you saw (and what you expected)

Security issues should be reported privately. See [SECURITY.md](SECURITY.md).

Changes.md  view on Meta::CPAN

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.1.0] - 2026-09-21

This started as a silly little diversion in February but I'm using this in actual projects now. I've even used it to shake out bugs in Affix.

I might move it out of the Acme namespace...

Anyway, the major win is that fiber hot path has been moved from Perl into C and roughly tripled context swapping throughput with no change to the public API.

LICENSE  view on Meta::CPAN

modifying or distributing the Package, you accept this license. Do not
use, modify, or distribute the Package, if you do not accept this
license.

(11)  If your Modified Version has been derived from a Modified
Version made by someone other than you, you are nevertheless required
to ensure that your Modified Version complies with the requirements of
this license.

(12)  This license does not grant you the right to use any trademark,
service mark, tradename, or logo of the Copyright Holder.

(13)  This license includes the non-exclusive, worldwide,
free-of-charge patent license to make, have made, use, offer to sell,
sell, import and otherwise transfer the Package with respect to any
patent claims licensable by the Copyright Holder that are necessarily
infringed by the Package. If you institute patent litigation
(including a cross-claim or counterclaim) against any party alleging
that the Package constitutes direct or contributory patent
infringement, then this Artistic License to you shall terminate on the
date that such litigation is filed.

README.md  view on Meta::CPAN

fibers; it simply prevents the scheduler from starting new ones.

# Thread Pool Configuration

`Acme::Parataxis` uses a native thread pool to handle blocking tasks. While it manages itself automatically, you can
tune its behavior using these functions.

## `set_max_threads( $count )`

Sets the maximum number of worker threads the pool is allowed to spawn. By default, this is set to the number of
logical CPU cores detected on your system (up to a hard limit of 64).

```
# Limit the pool to 4 threads
set_max_threads(4);
```

## `max_threads()`

Returns the currently configured maximum thread pool size.

README.md  view on Meta::CPAN

## Signal Handling

Not to be confused with `Acme::Parataxis::Signal`, true OS-level signals (like `SIGINT`) are delivered to the main
process thread. Perl handles these at 'safe points,' which in this module typically occur during a context switch
(yield, transfer, or call). If you receive an OS signal while a fiber is suspended, it will generally be processed when
the fiber is resumed and hits its next internal Perl opcode.

## The 'Final Transfer' Requirement

In a symmetric coroutine model (using `transfer()`), fibers don't have a natural 'parent' to return to. I've added
fallback logic to return to the `last_sender` or the main thread on exit, but it's good practice to explicitly
`transfer()` back to a partner fiber or the `root()` context to ensure your application logic remains predictable.
Leaving a fiber to just 'fall off the end' is like walking out of a room without closing the door; eventually, the
draft will bother someone.

## `is_done()` vs. Destruction

A fiber being `is_done()` simply means its Perl code has finished executing. The underlying C-level memory (stacks,
context, etc.) is not immediately freed until the `Acme::Parataxis` object is destroyed or the runtime performs its
final `cleanup()`. This is why you might see memory usage stay flat even after a fiber finishes, until the garbage
collector finally catches up with the object.

lib/Acme/Parataxis.c  view on Meta::CPAN

#ifdef _WIN32
    return GetCurrentProcessorNumber();
#elif defined(__linux__)
    return sched_getcpu();
#else
    return -1;
#endif
}

/**
 * @brief Detects the number of logical cores available on the system.
 *
 * @return int CPU count (minimum 1).
 */
int get_cpu_count() {
#ifdef _WIN32
    SYSTEM_INFO sysinfo;
    GetSystemInfo(&sysinfo);
    int count = sysinfo.dwNumberOfProcessors;
    return (count > 0) ? count : 1;
#elif defined(__APPLE__) || defined(__FreeBSD__)

lib/Acme/Parataxis.pod  view on Meta::CPAN

fibers; it simply prevents the scheduler from starting new ones.

=head1 Thread Pool Configuration

C<Acme::Parataxis> uses a native thread pool to handle blocking tasks. While it manages itself automatically, you can
tune its behavior using these functions.

=head2 C<set_max_threads( $count )>

Sets the maximum number of worker threads the pool is allowed to spawn. By default, this is set to the number of
logical CPU cores detected on your system (up to a hard limit of 64).

    # Limit the pool to 4 threads
    set_max_threads(4);

=head2 C<max_threads()>

Returns the currently configured maximum thread pool size.

=head1 Manual Fiber Management

lib/Acme/Parataxis.pod  view on Meta::CPAN

=head2 Signal Handling

Not to be confused with C<Acme::Parataxis::Signal>, true OS-level signals (like C<SIGINT>) are delivered to the main
process thread. Perl handles these at 'safe points,' which in this module typically occur during a context switch
(yield, transfer, or call). If you receive an OS signal while a fiber is suspended, it will generally be processed when
the fiber is resumed and hits its next internal Perl opcode.

=head2 The 'Final Transfer' Requirement

In a symmetric coroutine model (using C<transfer()>), fibers don't have a natural 'parent' to return to. I've added
fallback logic to return to the C<last_sender> or the main thread on exit, but it's good practice to explicitly
C<transfer()> back to a partner fiber or the C<root()> context to ensure your application logic remains predictable.
Leaving a fiber to just 'fall off the end' is like walking out of a room without closing the door; eventually, the
draft will bother someone.

=head2 C<is_done()> vs. Destruction

A fiber being C<is_done()> simply means its Perl code has finished executing. The underlying C-level memory (stacks,
context, etc.) is not immediately freed until the C<Acme::Parataxis> object is destroyed or the runtime performs its
final C<cleanup()>. This is why you might see memory usage stay flat even after a fiber finishes, until the garbage
collector finally catches up with the object.

t/005_symmetric.t  view on Meta::CPAN

use v5.40;
use Test2::V1 -ipP;
use blib;
use Acme::Parataxis;
$|++;
#
subtest 'Direct transfer dance' => sub {
    my ( $p1, $p2 );
    my $log = '';
    $p1 = Acme::Parataxis->new(
        code => sub {
            $log .= '1';
            diag 'Fiber 1: Transferring to 2...';
            $p2->transfer();
            $log .= '3';
            diag 'Fiber 1: Transferring to 2 again...';
            $p2->transfer();
            $log .= '5';
            diag 'Fiber 1: Returning...';
            return 'Final 1';
        }
    );
    $p2 = Acme::Parataxis->new(
        code => sub {
            $log .= '2';
            diag 'Fiber 2: Transferring back to 1...';
            $p1->transfer();
            $log .= '4';
            diag 'Fiber 2: Transferring back to 1 again...';
            $p1->transfer();

            # In a symmetric model, we should transfer back to exit cleanly if we want someone to continue
            diag 'Fiber 2: Final transfer back to 1...';
            $p1->transfer();
        }
    );
    diag 'Starting the dance...';
    $p1->call();

    # Ensure both are recognized as done
    $p1->is_done;
    $p2->is_done;
    is $log, '12345', 'Control flowed symmetrically between fibers';
    ok $p1->is_done, 'Fiber 1 finished';
    ok $p2->is_done, 'Fiber 2 finished';
};
subtest 'Transfer with arguments' => sub {
    my ( $p1, $p2 );
    my @received;
    $p1 = Acme::Parataxis->new(
        code => sub {
            my $val = Acme::Parataxis->yield();
            push @received, $val;

t/008_preemption.t  view on Meta::CPAN

use v5.40;
use Test2::V1 -ipP;
use blib;
use Acme::Parataxis;
$|++;
#
my $log = '';

# Set threshold to 5
Acme::Parataxis::set_preempt_threshold(5);
my $c1 = Acme::Parataxis->new(
    code => sub {
        for ( 1 .. 10 ) {
            $log .= 'A';
            Acme::Parataxis->maybe_yield();
        }
    }
);
my $c2 = Acme::Parataxis->new(
    code => sub {
        for ( 1 .. 10 ) {
            $log .= 'B';
            Acme::Parataxis->maybe_yield();
        }
    }
);

# Call C1. It should run 5 times and then maybe_yield will switch back to main
# because the threshold is hit.
# WAIT: who is the parent? Main.
diag 'Calling C1...';
my $res1 = $c1->call();
is $log, 'AAAAA', 'C1 yielded after 5 iterations';
diag 'Calling C2...';
my $res2 = $c2->call();
is $log, 'AAAAABBBBB', 'C2 yielded after 5 iterations';
diag 'Resuming C1...';
$c1->call();
is $log, 'AAAAABBBBBAAAAA', 'C1 finished its remaining iterations';
diag 'Resuming C2...';
$c2->call();
is $log, 'AAAAABBBBBAAAAABBBBB', 'C2 finished its remaining iterations';
#
done_testing();

t/022_resume_nested.t  view on Meta::CPAN

    # failed" on threaded-debug perls (abort, exit 134).
    my $out = '';
    async {
        fiber {1};
        yield;
        $out = 'done';
    };
    is $out, 'done', 'fiber completed its body after yield following a nested spawn';
};
subtest 'Spawn and await in the same fiber, then yield' => sub {
    my @log;
    async {
        my $child = fiber {1};
        my $r     = await($child);
        push @log, "child=$r";
        yield;
        push @log, 'parent-done';
    };
    is "@log", 'child=1 parent-done', 'resumed fiber saw both its nested result and the post-yield continuation';
};
#
done_testing();

t/023_priority.t  view on Meta::CPAN

use Test2::V1 -ipP;
$|++;
#
subtest 'Default priority and accessor' => sub {
    my $f = fiber {1};
    is $f->priority,     0,  'newly spawned fiber defaults to priority 0';
    is $f->priority(10), 10, 'setter returns the new priority';
    is $f->priority,     10, 'getter reflects the set priority';
};
subtest 'Higher priority resumes before lower priority' => sub {
    my @log;
    async {
        my $low = fiber { push @log, 'l1'; yield; push @log, 'l2'; };
        $low->priority(1);
        my $high = fiber { push @log, 'h1'; yield; push @log, 'h2'; };
        $high->priority(10);
        yield;
        push @log, 'main-end';
    };
    is "@log", 'l1 h1 h2 l2 main-end', 'priority 10 fiber resumed before priority 1 fiber';
};
subtest 'Re-prioritizing a queued fiber moves it' => sub {
    my @log;
    async {
        my $a = fiber { push @log, 'a1'; yield; push @log, 'a2'; };
        my $b = fiber { push @log, 'b1'; yield; push @log, 'b2'; };
        $b->priority(5);    # jump ahead of $a
        $b->priority(0);    # and back behind it
        $b->priority(7);    # ahead again
        yield;
        push @log, 'main-end';
    };
    is "@log", 'a1 b1 b2 a2 main-end', 're-prioritized fiber resumed before the lower-priority tie';
};
subtest 'Equal priorities keep FIFO order' => sub {
    my @log;
    async {
        my $x = fiber { push @log, 'x1'; yield; push @log, 'x2'; };
        my $y = fiber { push @log, 'y1'; yield; push @log, 'y2'; };
        yield;
        push @log, 'main-end';
    };
    is "@log", 'x1 y1 x2 y2 main-end', 'same-priority fibers resume in enqueue order';
};
subtest 'Changing priority of a running fiber is allowed' => sub {
    my @log;
    async {
        fiber {
            my $me = Acme::Parataxis->by_id( Acme::Parataxis::current_fid() );
            is $me->priority(-3), -3, 'running fiber reads/writes its own priority';
            push @log, 'start';
        };
        push @log, 'main-end';
    };
    is "@log", 'start main-end', 'both fibers completed';
};
#
done_testing();

t/024_stack_growth.t  view on Meta::CPAN

                my @out = sort { my $x = $recur->( $n - 1 ) } ( 0, 1 );
                return $out[0];
            };
            $recur->(500);
        };
        my $r = $f->await();
        is $r, 0, 'depth-500 sort-comparator recursion completed';
    };
};
subtest 'Recursion interleaved with yields' => sub {
    my @log;
    async {
        my $f = fiber {
            my $recur;
            $recur = sub {
                my ($n) = @_;
                return 0 if $n <= 1;
                push @log, "d$n";
                yield;
                return 1 + $recur->( $n - 1 );
            };
            $recur->(50);
        };
        yield;
        my $r = $f->await();
        is $r, 49, 'recursing fiber that yields still completes';
    };
    ok @log >= 49, 'fiber yielded during recursion';
};
done_testing();

t/028_stress_fuzz.t  view on Meta::CPAN

        elsif ( $op == 6 )                          {yield}
        else                                        {maybe_yield}
    }
    $sem->up   for 1 .. $need_ups;
    $sig->send for 1 .. $need_sig;
    $fut->set_result('v') if $plan{A} && !$fut_set;
    maybe_yield for 1 .. 3;
    return { plan => \%plan, inv => \%inv };
}

# Prologue: abort a run mid-flight so a parked fiber and its job are left over; every subsequent iteration has to drain
# and reclaim that leftover while running its own population. This is the cross-run contamination case from the
# hardening pass.
async {
    my $leaker = fiber { await_sleep(4) };
    yield for 1 .. 2;
    stop;
};
my ( $pass, $fails ) = ( 0, 0 );
my @problems_out;
for my $iter ( 1 .. $ITERS ) {

t/030_soak.t  view on Meta::CPAN

        elsif ( $op == 6 )                          {yield}
        else                                        {maybe_yield}
    }
    $sem->up   for 1 .. $need_ups;
    $sig->send for 1 .. $need_sig;
    $fut->set_result('v') if $plan{A} && !$fut_set;
    maybe_yield for 1 .. 3;
    return { plan => \%plan, inv => \%inv };
}

# Prologue: abandon a run mid-flight so a parked fiber and its job are left over; the
# first wave has to reclaim them before it can reach a clean boundary.
async {
    my $leaker = fiber { await_sleep(4) };
    yield for 1 .. 2;
    stop;
};
my $t0 = time;
my ( $waves, $fails ) = ( 0, 0 );
my ( $peak_live, $peak_jobs, $max_scale ) = ( 0, 0, 1 );
my @problems_out;



( run in 2.559 seconds using v1.01-cache-2.11-cpan-eca53f54451 )