view release on metacpan or search on metacpan
- MAJOR: Hypersonic compile() now derives the JIT module id
from a Digest::MD5 hash of the generated C source instead
of int(rand(100000)).
- t/1007-future-pool-chaining.t should no longer flake on slow
smokers
- t/2012..t/2017 (UA async future / run-poll / callback /
start-request / state-machine / helpers) all converted to
HypersonicTest::spawn_server + wait_for_port.
0.17 2026-05-26
- Fix CPAN-tester bailouts on t/0035-e2e-streaming.t (perl
5.12..5.42 on the k93msid smoker): bumped wait_for_port
default from 5s to 60s and t/0035 to 120s so slow debugging
perls have time to JIT-compile the largest test server
(regular + streaming + 2 SSE + 2 WebSocket routes)
- HypersonicTest spawn_server now sets HYPERSONIC_COMPILE_DIAG
so the child prints a 'compiling JIT module ...' breadcrumb
before the slow gcc invocation; wait_for_port diag is no
longer empty when the timeout hits mid-compile
- Hypersonic compile() wraps XS::JIT->compile() in
'no warnings "redefine"' to silence the
'Subroutine Hypersonic::Stream::* redefined at
Hypersonic.pm line 881' noise when the boot xsub installs
its xsubs into the same package as Hypersonic/Stream.pm
- Fix Hypersonic::SSE call_method() return handling: SvTRUE(POPs)
- Bump XS::JIT to 0.22
0.11 2026-01-31
- Fix can_link() to properly pass headers to Devel::CheckLib
- Fix can_link() test to work reliably with compiler builtins (libm sin)
- Add missing C includes to UA.pm for socket/networking types
- Attempt a portable strcasestr implementation for non-GNU systems
- Fix compilation failures on Linux (missing headers, GNU extensions)
- Fix WebSocket g_websocket_handlers not declared when no dynamic routes
- Fix WebSocket requires Stream module (set needs_streaming for WebSocket)
- Fix WebSocket test to handle frame arriving with HTTP handshake
0.10 2026-01-29
- RequestID Middleware
- More C89 compliance refactoring
0.09 2026-01-29
- Fix io_uring backend to check for liburing library (not just headers)
- Add event backend ldflags to XS compile options
- Fix test race conditions with active port probing instead of sleep()
t/0009-security.t
t/0010-tls.t
t/0011-request-features.t
t/0012-response.t
t/0013-request.t
t/0014-middleware.t
t/0015-static.t
t/0016-session.t
t/0017-compression.t
t/0018-http2.t
t/0019-streaming.t
t/0020-event.t
t/0021-event-role.t
t/0022-event-kqueue.t
t/0023-event-epoll.t
t/0024-event-poll.t
t/0025-event-select.t
t/0026-event-iouring.t
t/0027-chunked-encoding.t
t/0027-event-iocp.t
t/0028-event-eventports.t
t/0028-http2-streaming.t
t/0029-sse.t
t/0030-websocket-handshake.t
t/0031-websocket-framing.t
t/0032-websocket-api.t
t/0035-e2e-streaming.t
t/0040-server-reliability.t
t/1001-future-basic.t
t/1002-future-convergent.t
t/1003-future-pool-basic.t
t/1004-future-pool-submit.t
t/1005-future-pool-concurrent.t
t/1006-future-pool-errors.t
t/1007-future-pool-chaining.t
t/1008-future-pool-shutdown.t
t/1009-future-data-structures.t
t/2001-ua-foundation.t
t/2002-ua-http11.t
t/2003-ua-response.t
t/2004-ua-streaming.t
t/2005-ua-tls.t
t/2006-ua-pool.t
t/2007-ua-http2.t
t/2008-ua-sse.t
t/2009-ua-websocket.t
t/2010-ua-async-api.t
t/2011-ua-blocking-methods.t
t/2012-ua-async-future.t
t/2013-ua-run-poll.t
t/2014-ua-callback.t
lib/Hypersonic.pm view on Meta::CPAN
# Check for dynamic option and feature flags
my $dynamic = 0;
my %features = (
parse_query => 0, # Parse ?key=value query strings
parse_headers => 0, # Parse HTTP headers
parse_cookies => 0, # Parse Cookie header
parse_json => 0, # Parse JSON body (requires Cpanel::JSON::XS)
parse_form => 0, # Parse form-urlencoded body
response_helpers => 0, # JIT compile response helper methods
streaming => 0, # Streaming response handler
need_xs_builder => 0, # Handler receives XS::JIT::Builder
);
if (ref($opts) eq 'HASH') {
$dynamic = $opts->{dynamic} ? 1 : 0;
# Copy feature flags from options
for my $feat (keys %features) {
$features{$feat} = $opts->{$feat} ? 1 : 0 if exists $opts->{$feat};
}
}
lib/Hypersonic.pm view on Meta::CPAN
shift @segments; # Remove leading empty string
for my $i (0 .. $#segments) {
if ($segments[$i] =~ /^:(\w+)$/) {
push @params, { name => $1, position => $i };
$dynamic = 1; # Path params imply dynamic
}
}
# Streaming handlers are always dynamic
if ($features{streaming}) {
$dynamic = 1;
}
# need_xs_builder handlers are always dynamic (but handled specially at compile time)
if ($features{need_xs_builder}) {
$dynamic = 1;
}
push @{$self->{routes}}, {
method => $method,
path => $path,
handler => $handler,
dynamic => $dynamic,
streaming => $features{streaming},
need_xs_builder => $features{need_xs_builder},
params => \@params,
segments => \@segments,
features => \%features,
# Per-route middleware (optional)
before => $opts->{before} // [],
after => $opts->{after} // [],
};
return $self;
lib/Hypersonic.pm view on Meta::CPAN
route_count => scalar(@{$self->{routes}}),
all_same_prefix => undef, # Common prefix like /api/*
single_method => undef, # Only one HTTP method used?
# JIT feature flags - only generate code for features actually used
needs_query => 0, # Any route needs query string parsing?
needs_headers => 0, # Any route needs header access?
needs_cookies => 0, # Any route needs cookie parsing?
needs_json => 0, # Any route needs JSON body parsing?
needs_form => 0, # Any route needs form data parsing?
needs_response_helpers => 0, # Any route needs response helper methods?
needs_streaming => 0, # Any route uses streaming responses?
needs_xs_builder => 0, # Any route uses need_xs_builder?
# Middleware flags - JIT: only generate middleware code if actually used
has_global_before => scalar(@{$self->{before_middleware}}) > 0,
has_global_after => scalar(@{$self->{after_middleware}}) > 0,
has_route_middleware => 0, # Any route has before/after hooks?
has_any_middleware => 0, # Global OR per-route middleware?
);
# First pass: collect method usage and route characteristics
for my $route (@{$self->{routes}}) {
lib/Hypersonic.pm view on Meta::CPAN
# features it actually uses. This avoids generating unused parsing code.
my $f = $route->{features} // {};
# Explicit flags take precedence
$analysis{needs_query} = 1 if $f->{parse_query};
$analysis{needs_headers} = 1 if $f->{parse_headers};
$analysis{needs_cookies} = 1 if $f->{parse_cookies};
$analysis{needs_json} = 1 if $f->{parse_json};
$analysis{needs_form} = 1 if $f->{parse_form};
$analysis{needs_response_helpers} = 1 if $f->{response_helpers};
$analysis{needs_streaming} = 1 if $f->{streaming};
$analysis{needs_xs_builder} = 1 if $f->{need_xs_builder};
# Auto-detect by analyzing handler code
my $handler_code = _deparse_handler($route->{handler});
if ($handler_code) {
# Look for $req->{query} or ->{query} access patterns
$analysis{needs_query} = 1 if $handler_code =~ /\{['"]*query['"]*\}/;
$analysis{needs_headers} = 1 if $handler_code =~ /\{['"]*headers['"]*\}/;
$analysis{needs_cookies} = 1 if $handler_code =~ /\{['"]*cookies['"]*\}/;
$analysis{needs_json} = 1 if $handler_code =~ /\{['"]*json['"]*\}/;
lib/Hypersonic.pm view on Meta::CPAN
push @dynamic_handlers, $route->{handler};
push @route_param_info, $route->{params};
$route->{handler_idx} = $#dynamic_handlers;
}
}
# Store dynamic handlers and param info for runtime access
$self->{dynamic_handlers} = \@dynamic_handlers;
$self->{route_param_info} = \@route_param_info;
# JIT: Build streaming handlers lookup table (only if streaming is enabled)
if ($self->{route_analysis}{needs_streaming}) {
my @streaming_flags;
for my $route (@{$self->{routes}}) {
next unless $route->{dynamic};
push @streaming_flags, $route->{streaming} ? 1 : 0;
}
$self->{_streaming_flags} = \@streaming_flags;
}
# JIT: Build WebSocket handlers lookup table
if ($self->_has_websocket_routes()) {
my @ws_handlers;
my @ws_paths;
for my $route (@{$self->{websocket_routes}}) {
push @ws_handlers, $route->{handler};
push @ws_paths, $route->{path};
# Check for Room usage in route options
if ($route->{opts}{rooms}) {
$self->{route_analysis}{needs_websocket_rooms} = 1;
}
}
$self->{_websocket_handlers} = \@ws_handlers;
$self->{_websocket_paths} = \@ws_paths;
$self->{route_analysis}{needs_websocket} = 1;
# Handler is needed if we have websocket routes
$self->{route_analysis}{needs_websocket_handler} = 1;
# WebSocket uses Stream for connection handling
$self->{route_analysis}{needs_streaming} = 1;
}
# JIT: Explicit opt-in for Room support (can also be set in new())
if ($self->{websocket_rooms}) {
$self->{route_analysis}{needs_websocket_rooms} = 1;
}
# JIT: Build per-route middleware arrays (only if route middleware is present)
my $analysis = $self->{route_analysis};
if ($analysis->{has_route_middleware}) {
lib/Hypersonic.pm view on Meta::CPAN
#
# Pre-0.18 we used 'Hypersonic::_Server_' . int(rand(100000)) which
# gave a different module name on every fresh perl process; because
# XS::JIT's on-disk cache lives at
# <cache_dir>/lib/auto/<safe_name>/<safe_name>.<dlext>
# (see XS-JIT/lib/XS/JIT/xs_jit.c xs_jit_cache_path()), a different
# $name on every run forced a full gcc/cc re-invocation EVERY time
# the test suite or a user re-ran their server. On slow CPAN smoker
# boxes that gcc invocation takes 30-60+ seconds per server, which
# is what caused the SIGKILL cascade in CPAN tester reports for
# 0.17 (t/0035-e2e-streaming.t, t/2012..t/2017, t/2102).
#
# Using a content hash means: identical route+option configurations
# produce the same module name -> warm cache hit -> dlopen() of a
# 100ms .so instead of a 30s gcc rebuild. The random fallback id is
# retained for the degenerate case where Digest::MD5 isn't available.
my $module_id;
{
my $hash_input = join("\0",
$c_code,
$VERSION,
lib/Hypersonic.pm view on Meta::CPAN
"${module_name}::run_event_loop" => {
source => 'hypersonic_run_event_loop',
is_xs_native => 1,
},
"${module_name}::dispatch" => {
source => 'hypersonic_dispatch',
is_xs_native => 1,
},
);
# Add Stream and SSE XS functions if streaming is enabled
if ($self->{route_analysis}{needs_streaming}) {
%functions = (%functions, %{Hypersonic::Stream->get_xs_functions()});
%functions = (%functions, %{Hypersonic::SSE->get_xs_functions()});
}
# Add WebSocket XS functions if WebSocket routes are registered
if ($self->{route_analysis}{needs_websocket}) {
require Hypersonic::WebSocket;
%functions = (%functions, %{Hypersonic::WebSocket->get_xs_functions()});
}
lib/Hypersonic.pm view on Meta::CPAN
# something upstream disabled autoflush on STDERR.
if ($ENV{HYPERSONIC_COMPILE_DIAG} || $ENV{AUTOMATED_TESTING}) {
local $| = 1;
print STDERR "# Hypersonic: compiling JIT module $module_name ...\n";
eval { STDERR->flush; };
}
# The JIT boot xsub installs Hypersonic::Stream::* xsubs into
# the same package that Hypersonic/Stream.pm already lives in,
# which Perl reports as "Subroutine ... redefined". This is
# expected (the .pm defines is_streaming_handler and the .so
# provides the rest at compile time, or - on a second compile()
# in the same process - it reinstalls them). Silence the noise.
my $ok;
{
no warnings 'redefine';
$ok = XS::JIT->compile(%compile_opts);
}
die "XS::JIT->compile failed for $module_name (check liburing/zlib/openssl "
. "are installed and linkable; extra_ldflags='"
. ($compile_opts{extra_ldflags} // '') . "')"
lib/Hypersonic.pm view on Meta::CPAN
Hypersonic::Protocol::HTTP2->gen_connection_preface_check($builder);
Hypersonic::Protocol::HTTP2->gen_response_sender($builder);
Hypersonic::Protocol::HTTP2->gen_404_response($builder);
Hypersonic::Protocol::HTTP2->gen_callbacks($builder);
Hypersonic::Protocol::HTTP2->gen_session_init($builder);
Hypersonic::Protocol::HTTP2->gen_dispatcher($builder);
Hypersonic::Protocol::HTTP2->gen_input_processor($builder);
$builder->blank;
}
# Streaming support - JIT: only generate when streaming handlers detected
my $analysis = $self->{route_analysis};
if ($analysis->{needs_streaming}) {
require Hypersonic::Stream;
Hypersonic::Stream->generate_c_code($builder, {
max_streams => $self->{max_connections},
});
# SSE support - compile SSE methods when streaming is enabled
require Hypersonic::SSE;
Hypersonic::SSE->generate_c_code($builder, {
max_sse_instances => $self->{max_connections},
});
}
# WebSocket support - JIT: only generate when WebSocket routes exist
if ($analysis->{needs_websocket}) {
require Hypersonic::WebSocket;
require Hypersonic::Protocol::WebSocket;
lib/Hypersonic.pm view on Meta::CPAN
my $padding = 8 - scalar(@param_strs);
for (1 .. $padding) {
push @param_strs, '{NULL, 0}';
}
my $params_str = join(', ', @param_strs);
$builder->line(" { $count, { $params_str } },");
}
$builder->line('};')
->blank;
# JIT: Streaming handler flags array (only if streaming is enabled)
if ($analysis->{needs_streaming} && $self->{_streaming_flags}) {
my @flags = @{$self->{_streaming_flags}};
my $flags_str = join(', ', @flags);
$builder->comment('Streaming handler flags - 1 = streaming, 0 = normal')
->line("static int g_streaming_handlers[$handler_count] = { $flags_str };")
->blank;
}
}
# JIT: WebSocket route paths array (only if WebSocket routes exist)
if ($analysis->{needs_websocket} && $self->{_websocket_paths}) {
my @paths = @{$self->{_websocket_paths}};
my $ws_count = scalar @paths;
$builder->comment('WebSocket route paths');
for my $i (0 .. $#paths) {
lib/Hypersonic.pm view on Meta::CPAN
$builder->comment('Dynamic route - call Perl handler');
# Body parsing - delegates to Protocol module
$PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access);
$builder->blank
->line('char* dyn_resp;')
->line('int dyn_resp_len;')
->line('call_dynamic_handler(aTHX_ handler_idx, fd, method, method_len, path, full_path_len, body, body_len, recv_buf, len, &dyn_resp, &dyn_resp_len);')
->comment('dyn_resp_len == -1 means streaming handler (response already sent)')
->line('if (dyn_resp_len >= 0) {')
->line(' HYPERSONIC_SEND(fd, dyn_resp, dyn_resp_len);')
->line('}')
->else
->line('HYPERSONIC_SEND(fd, resp, resp_len);')
->endif;
} else {
$builder->line('HYPERSONIC_SEND(fd, resp, resp_len);');
}
$builder->blank;
lib/Hypersonic.pm view on Meta::CPAN
->line(' int seg_lens[16];')
->line(' int seg_count;')
->line(' AV* seg_av;')
->line(' int i;')
->line(' HV* params_hv;')
->line(' RouteParamInfo* param_info;')
->line(' SV* req_ref;')
->line(' SV* mw_result = NULL;')
->line(' int short_circuit = 0;')
->line(' HV* headers_hv = NULL;')
->line(' int is_streaming = 0;')
->line(' SV* stream_sv = NULL;')
->blank
->line(' if (!g_handler_array) {')
->line(' *resp_out = (char*)RESP_404;')
->line(' *resp_len_out = RESP_404_LEN;')
->line(' return;')
->line(' }')
->blank
->line(' handlers = (AV*)SvRV(g_handler_array);')
->line(' handler_sv = av_fetch(handlers, handler_idx, 0);')
lib/Hypersonic.pm view on Meta::CPAN
->line(' if (mw_result) {')
->line(' result = mw_result;')
->line(' short_circuit = 1;')
->line(' }')
->line(' }')
->line(' }')
->line(' }');
}
# JIT: Streaming handler support
if ($analysis->{needs_streaming}) {
$builder->blank
->comment('JIT: Check if this is a streaming handler')
->line(' is_streaming = g_streaming_handlers[handler_idx];')
->if('is_streaming')
->comment('Create Hypersonic::Stream object for streaming handler')
->line('dSP;')
->line('PUSHMARK(SP);')
->line('XPUSHs(sv_2mortal(newSVpv("Hypersonic::Stream", 0)));')
->line('XPUSHs(sv_2mortal(newSVpv("fd", 0)));')
->line('XPUSHs(sv_2mortal(newSViv(client_fd)));')
->line('PUTBACK;')
->line('int stream_count = call_method("new", G_SCALAR);')
->line('SPAGAIN;')
->if('stream_count > 0')
->line('stream_sv = POPs;')
lib/Hypersonic.pm view on Meta::CPAN
}
# Call the main handler (conditionally if middleware present)
if ($analysis->{has_any_middleware}) {
$builder->blank
->comment('Call main handler (unless middleware short-circuited)')
->line(' if (!short_circuit) {')
->line(' PUSHMARK(SP);')
->line(' XPUSHs(req_ref);');
# For streaming handlers with middleware
if ($analysis->{needs_streaming}) {
$builder->line(' if (is_streaming && stream_sv) XPUSHs(stream_sv);');
}
$builder->line(' PUTBACK;')
->line(' count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
->line(' SPAGAIN;')
->line(' if (count == 1) result = POPs;')
->line(' PUTBACK;')
->line(' }');
} else {
$builder->line(' PUSHMARK(SP);')
->line(' XPUSHs(sv_2mortal(req_ref));');
# For streaming handlers without middleware
if ($analysis->{needs_streaming}) {
$builder->line(' if (is_streaming && stream_sv) XPUSHs(stream_sv);');
}
$builder->line(' PUTBACK;')
->line(' count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
->line(' SPAGAIN;');
}
# JIT: Call per-route after middleware
if ($analysis->{has_route_middleware}) {
$builder->blank
lib/Hypersonic.pm view on Meta::CPAN
$builder->blank
->comment('JIT: Builder after middleware (inline C - zero Perl overhead)');
for my $mw (@{$analysis->{builder_after}}) {
if ($mw->can('build_after')) {
$mw->build_after($builder, $ctx);
}
}
}
# JIT: Streaming handlers - early return (response already sent via Stream)
if ($analysis->{needs_streaming}) {
$builder->blank
->comment('JIT: Streaming handlers return early - response sent via Stream object')
->if('is_streaming')
->line('if (stream_sv) SvREFCNT_dec(stream_sv);')
->line('FREETMPS;')
->line('LEAVE;')
->line('*resp_out = NULL;')
->line('*resp_len_out = -1;')
->comment('Signal streaming response - caller should not send')
->line('return;')
->endif;
}
$builder->blank
->line(' if (SvTRUE(ERRSV)) {')
->line(' static char error_resp[512];')
->line(' int err_len = snprintf(error_resp, sizeof(error_resp),')
->line(' "HTTP/1.1 500 Internal Server Error\\r\\n"')
->line(' "Content-Type: text/plain\\r\\n"')
lib/Hypersonic.pm view on Meta::CPAN
my ($msg) = @_;
$global->broadcast($msg, $ws); # Send to all except sender
});
$ws->on(close => sub {
$global->leave($ws);
$global->broadcast("A user left");
});
});
=head2 streaming
$server->get('/events' => sub {
my ($stream) = @_;
# Send SSE events
my $sse = $stream->sse;
$sse->event(type => 'update', data => 'Hello');
$sse->keepalive;
$sse->close;
}, { streaming => 1 });
Enable streaming responses for a route. The handler receives a
L<Hypersonic::Stream> object instead of returning a static response.
B<Stream Object Methods:>
=over 4
=item $stream->write($data)
Write data to the response (chunked encoding).
lib/Hypersonic.pm view on Meta::CPAN
$sse->retry(3000); # Reconnect after 3s
$sse->event(
type => 'notification',
data => '{"message":"New update!"}',
id => '12345',
);
# Keep connection alive...
$sse->keepalive;
}, { streaming => 1 });
=head2 Route Handler Options
All route methods accept an optional hashref as the third argument:
$server->get('/path' => sub { ... }, {
dynamic => 1, # Force dynamic handler
parse_query => 1, # Parse query string
parse_headers => 1, # Parse HTTP headers
parse_cookies => 1, # Parse Cookie header
lib/Hypersonic.pm view on Meta::CPAN
Must be called after all routes are registered, before C<run()>.
=head2 JIT Feature Detection
Hypersonic uses a "JIT philosophy" - only code that's actually needed gets
compiled. The C<compile()> method analyzes your routes and sets these flags:
=over 4
=item needs_streaming
Set when any route has C<streaming =E<gt> 1>. Compiles L<Hypersonic::Stream>
and L<Hypersonic::SSE> XS code.
=item needs_websocket
Set when any C<websocket()> routes are registered. Compiles
L<Hypersonic::WebSocket> and L<Hypersonic::Protocol::WebSocket::Frame> XS code.
=item needs_websocket_handler
Automatically set when C<needs_websocket> is true. Compiles
lib/Hypersonic.pm view on Meta::CPAN
Set when C<async_pool()> is called. Compiles L<Hypersonic::Future> and
L<Hypersonic::Future::Pool> for async thread pool operations.
=back
You can inspect these flags after compile:
$server->compile();
my $analysis = $server->{route_analysis};
say "Has streaming: ", $analysis->{needs_streaming} ? "yes" : "no";
say "Has WebSocket: ", $analysis->{needs_websocket} ? "yes" : "no";
say "Has Rooms: ", $analysis->{needs_websocket_rooms} ? "yes" : "no";
=head2 dispatch
my $response = $server->dispatch($request_arrayref);
Dispatch a request and return the response. Primarily for testing.
Request is an arrayref: C<[method, path, body, keep_alive, fd]>
lib/Hypersonic/Protocol/HTTP1.pm view on Meta::CPAN
return $text{$code} // 'Unknown';
}
# Get status text (class method for external use)
sub status_text {
my ($class, $code) = @_;
return _status_text($code);
}
# ============================================================
# Chunked Transfer Encoding (HTTP/1.1 streaming)
# ============================================================
# Generate C code for chunked response headers
sub gen_chunked_start {
my ($class, $builder) = @_;
$builder->comment('Send HTTP/1.1 headers with chunked encoding')
->line('static void send_chunked_headers(int fd, int status, const char* content_type) {')
->line(' char headers[2048];')
->line(' const char* status_str = "OK";')
lib/Hypersonic/Protocol/HTTP2.pm view on Meta::CPAN
->line('}')
->blank;
return $builder;
}
# ============================================================
# HTTP/2 Streaming Support (Phase 3)
# ============================================================
# Generate streaming headers without END_STREAM
sub gen_stream_headers {
my ($class, $builder) = @_;
$builder->comment('HTTP/2 Streaming: Send HEADERS without END_STREAM (allows more DATA)')
->line('static int h2_stream_headers(nghttp2_session* session, int32_t stream_id,')
->line(' int status, const char* content_type) {')
->line(' char status_str[4];')
->line(' snprintf(status_str, sizeof(status_str), "%d", status);')
->line(' ')
->line(' nghttp2_nv hdrs[] = {')
lib/Hypersonic/Protocol/HTTP2.pm view on Meta::CPAN
->line(' stream_id, NULL, hdrs, 2, NULL);')
->line(' if (rv < 0) return rv;')
->line(' ')
->line(' return nghttp2_session_send(session);')
->line('}')
->blank;
return $builder;
}
# Generate streaming data chunk sender
sub gen_stream_data {
my ($class, $builder) = @_;
$builder->comment('HTTP/2 Streaming: Chunk provider for streaming DATA frames')
->line('typedef struct {')
->line(' const uint8_t* data;')
->line(' size_t length;')
->line(' size_t pos;')
->line('} H2ChunkProvider;')
->blank
->line('static ssize_t h2_chunk_read_cb(nghttp2_session* session,')
->line(' int32_t stream_id,')
->line(' uint8_t* buf, size_t length,')
->line(' uint32_t* data_flags,')
lib/Hypersonic/Protocol/HTTP2.pm view on Meta::CPAN
->line(' int32_t conn_window = nghttp2_session_get_remote_window_size(session);')
->line(' int32_t stream_window = nghttp2_session_get_stream_remote_window_size(')
->line(' session, stream_id);')
->line(' return conn_window < stream_window ? conn_window : stream_window;')
->line('}')
->blank;
return $builder;
}
# Generate XS wrappers for HTTP/2 streaming from Perl
sub gen_stream_xs_wrappers {
my ($class, $builder) = @_;
$builder->comment('XS wrappers for HTTP/2 streaming from Perl');
# h2_stream_start(session_ptr, stream_id, status, content_type)
$builder->xs_function('hypersonic_h2_stream_start')
->xs_preamble
->check_items(4, 4, 'session_ptr, stream_id, status, content_type')
->line('nghttp2_session* session = (nghttp2_session*)SvUV(ST(0));')
->line('int32_t stream_id = (int32_t)SvIV(ST(1));')
->line('int status = (int)SvIV(ST(2));')
->line('STRLEN ct_len;')
->line('const char* content_type = SvPV(ST(3), ct_len);')
lib/Hypersonic/Protocol/HTTP2.pm view on Meta::CPAN
->line('nghttp2_session* session = (nghttp2_session*)SvUV(ST(0));')
->line('int32_t stream_id = (int32_t)SvIV(ST(1));')
->line('int rv = h2_stream_end(session, stream_id);')
->line('XSRETURN_IV(rv);')
->xs_end
->blank;
return $builder;
}
# Generate all HTTP/2 streaming code
sub generate_streaming {
my ($class, $builder, $opts) = @_;
$class->gen_stream_headers($builder);
$class->gen_stream_data($builder);
$class->gen_stream_end($builder);
$class->gen_flow_control($builder);
$class->gen_stream_xs_wrappers($builder);
return $builder;
}
lib/Hypersonic/SSE.pm view on Meta::CPAN
package Hypersonic::SSE;
use strict;
use warnings;
use 5.010;
# Hypersonic::SSE - High-level Server-Sent Events API
#
# Wraps the streaming infrastructure to provide a clean SSE interface.
# Automatically handles headers, event formatting, and keepalives.
# Uses JIT-compiled XS for performance.
our $VERSION = '0.19';
use constant {
STATE_INIT => 0,
STATE_STARTED => 1,
STATE_FINISHED => 2,
};
use constant MAX_SSE_INSTANCES => 65536;
use constant DEFAULT_KEEPALIVE => 30;
use Hypersonic::Protocol::SSE;
=head1 NAME
Hypersonic::SSE - Server-Sent Events streaming interface
=head1 SYNOPSIS
$app->get('/events' => sub {
my ($req, $stream) = @_;
my $sse = Hypersonic::SSE->new($stream);
$sse->event(
type => 'message',
data => 'Hello World!',
);
$sse->event(
type => 'update',
data => '{"count": 42}',
id => '123',
);
$sse->close();
}, { streaming => 1 });
=head1 DESCRIPTION
Hypersonic::SSE provides a high-level API for sending Server-Sent Events.
It wraps a Hypersonic::Stream object and handles SSE-specific formatting,
headers, and keepalives.
=cut
# ============================================================
lib/Hypersonic/SSE.pm view on Meta::CPAN
for my $id (($last_id + 1) .. 100) {
$sse->event(
type => 'update',
data => "Event $id",
id => $id,
);
}
$sse->close();
}, { streaming => 1 });
=head1 SEE ALSO
L<Hypersonic::Stream>, L<Hypersonic::Protocol::SSE>
=head1 AUTHOR
Hypersonic Contributors
=cut
lib/Hypersonic/Stream.pm view on Meta::CPAN
our $VERSION = '0.19';
use constant {
STATE_INIT => 0,
STATE_STARTED => 1,
STATE_FINISHED => 2,
STATE_ABORTED => 3,
};
use constant MAX_STREAMS => 65536;
# Class method for streaming handler detection (only Perl code needed)
sub is_streaming_handler {
my ($class, $handler, $opts) = @_;
return 1 if $opts && $opts->{streaming};
my $proto = prototype($handler);
return 1 if defined $proto && $proto =~ /stream/i;
eval {
require B::Deparse;
my $deparser = B::Deparse->new('-p', '-sC');
my $code = $deparser->coderef2text($handler);
return 1 if $code =~ /\$stream\s*->/;
};
return 0;
}
lib/Hypersonic/Stream.pm view on Meta::CPAN
sub gen_xs_headers {
my ($class, $builder) = @_;
$builder->xs_function('xs_stream_headers')
->xs_preamble
->line('int fd = SvIV(SvRV(ST(0)));')
->line('StreamState* s = &stream_registry[fd];')
->blank
->if('s->state != STREAM_STATE_INIT')
->line('croak("Cannot set headers after streaming started");')
->endif
->blank
->if('items >= 2')
->line('s->status = SvIV(ST(1));')
->endif
->blank
->line('s->extra_headers[0] = \'\\0\';')
->if('items >= 3 && SvROK(ST(2)) && SvTYPE(SvRV(ST(2))) == SVt_PVHV')
->line('HV* hv = (HV*)SvRV(ST(2));')
->line('int extra_pos = 0;')
lib/Hypersonic/Stream.pm view on Meta::CPAN
$builder->xs_function('xs_stream_content_type')
->xs_preamble
->if('items != 2')
->line('croak("Usage: $stream->content_type(type)");')
->endif
->line('int fd = SvIV(SvRV(ST(0)));')
->line('StreamState* s = &stream_registry[fd];')
->blank
->if('s->state != STREAM_STATE_INIT')
->line('croak("Cannot set content_type after streaming started");')
->endif
->blank
->line('STRLEN len;')
->line('const char* ct = SvPV(ST(1), len);')
->if('len < sizeof(s->content_type)')
->line('memcpy(s->content_type, ct, len);')
->line('s->content_type[len] = \'\\0\';')
->endif
->blank
->line('ST(0) = ST(0);')
t/0019-streaming.t view on Meta::CPAN
my $stream = Hypersonic::Stream->new(fd => 200);
is($stream->state, 0, 'state starts at INIT');
ok(!$stream->is_finished, 'not finished initially');
};
# ============================================================
# Test 8-10: Streaming handler detection
# ============================================================
subtest 'is_streaming_handler with explicit flag' => sub {
plan tests => 2;
my $handler = sub { };
ok(
Hypersonic::Stream->is_streaming_handler($handler, { streaming => 1 }),
'explicit streaming => 1 detected'
);
ok(
!Hypersonic::Stream->is_streaming_handler($handler, { streaming => 0 }),
'explicit streaming => 0 not detected'
);
};
subtest 'is_streaming_handler with no options' => sub {
plan tests => 1;
my $handler = sub { my ($req) = @_; return { status => 200 }; };
ok(
!Hypersonic::Stream->is_streaming_handler($handler, {}),
'regular handler not detected as streaming'
);
};
subtest 'is_streaming_handler with code analysis' => sub {
plan tests => 1;
# Handler that uses $stream->
my $handler = sub {
my ($req, $stream) = @_;
$stream->write("test");
};
# Note: code analysis may or may not detect this depending on B::Deparse
ok(1, 'code analysis attempted');
};
# ============================================================
# Test 11-14: Route registration with streaming flag
# ============================================================
subtest 'Route with streaming flag' => sub {
plan tests => 4;
my $app = Hypersonic->new();
$app->get('/stream' => sub {
my ($req, $stream) = @_;
$stream->write("test");
$stream->end();
}, { streaming => 1 });
my $route = $app->{routes}[0];
ok($route->{streaming}, 'route has streaming flag');
ok($route->{dynamic}, 'streaming route is dynamic');
is($route->{features}{streaming}, 1, 'features has streaming');
is($route->{path}, '/stream', 'path correct');
};
subtest 'Route without streaming flag' => sub {
plan tests => 2;
my $app = Hypersonic->new();
$app->get('/normal' => sub {
return { status => 200, body => 'hello' };
});
my $route = $app->{routes}[0];
ok(!$route->{streaming}, 'regular route has no streaming flag');
is($route->{features}{streaming}, 0, 'features has streaming = 0');
};
subtest 'Route analysis detects streaming' => sub {
plan tests => 2;
my $app = Hypersonic->new();
$app->get('/stream' => sub {
my ($req, $stream) = @_;
$stream->write("data");
$stream->end();
}, { streaming => 1 });
my %analysis = (needs_streaming => 0);
for my $route (@{$app->{routes}}) {
if ($route->{features}{streaming}) {
$analysis{needs_streaming} = 1;
}
}
ok($analysis{needs_streaming}, 'analysis detects streaming routes');
# Non-streaming app
my $app2 = Hypersonic->new();
$app2->get('/normal' => sub { return 'hello' });
%analysis = (needs_streaming => 0);
for my $route (@{$app2->{routes}}) {
if ($route->{features}{streaming}) {
$analysis{needs_streaming} = 1;
}
}
ok(!$analysis{needs_streaming}, 'analysis does not detect streaming for normal routes');
};
subtest 'Multiple routes mixed streaming' => sub {
plan tests => 3;
my $app = Hypersonic->new();
$app->get('/normal' => sub { return 'hello' });
$app->get('/stream' => sub { }, { streaming => 1 });
$app->post('/data' => sub { }, { dynamic => 1 });
my @streaming = grep { $_->{streaming} } @{$app->{routes}};
my @dynamic = grep { $_->{dynamic} } @{$app->{routes}};
is(scalar(@streaming), 1, 'one streaming route');
is(scalar(@dynamic), 2, 'two dynamic routes (streaming is dynamic)');
is($app->{routes}[1]{streaming}, 1, 'correct route is streaming');
};
# ============================================================
# Test 15: Code generation produces valid C
# ============================================================
subtest 'Stream code generation' => sub {
plan tests => 6;
my $builder = XS::JIT::Builder->new;
t/0019-streaming.t view on Meta::CPAN
like($code, qr/STREAM_STATE_STARTED/, 'defines STREAM_STATE_STARTED');
like($code, qr/STREAM_STATE_FINISHED/, 'defines STREAM_STATE_FINISHED');
like($code, qr/StreamState/, 'defines StreamState struct');
like($code, qr/stream_registry/, 'has stream registry');
like($code, qr/xs_stream_new/, 'has xs_stream_new');
};
# ============================================================
# Test 16-18: Hypersonic integration
# ============================================================
subtest 'Hypersonic compile with streaming route' => sub {
plan tests => 2;
my $app = Hypersonic->new();
$app->get('/events' => sub {
my ($req, $stream) = @_;
$stream->write("event: test\n");
$stream->end();
}, { streaming => 1 });
$app->get('/' => sub { return 'hello' });
eval { $app->compile() };
ok(!$@, 'compile succeeds with streaming route') or diag($@);
ok($app->{route_analysis}{needs_streaming}, 'analysis has needs_streaming flag');
};
subtest 'Hypersonic compile without streaming route' => sub {
plan tests => 2;
my $app = Hypersonic->new();
$app->get('/' => sub { return 'hello' });
$app->get('/about' => sub { return 'about' });
eval { $app->compile() };
ok(!$@, 'compile succeeds without streaming route') or diag($@);
ok(!$app->{route_analysis}{needs_streaming}, 'analysis has no needs_streaming flag');
};
subtest 'Feature flags in analysis' => sub {
plan tests => 3;
my $app = Hypersonic->new();
$app->get('/stream' => sub { }, { streaming => 1 });
$app->get('/json' => sub { }, { dynamic => 1, parse_json => 1 });
my %analysis = (
needs_streaming => 0,
needs_json => 0,
);
for my $route (@{$app->{routes}}) {
my $f = $route->{features} // {};
$analysis{needs_streaming} = 1 if $f->{streaming};
$analysis{needs_json} = 1 if $f->{parse_json};
}
ok($analysis{needs_streaming}, 'streaming detected');
ok($analysis{needs_json}, 'json detected');
# Verify they're independent
my $app2 = Hypersonic->new();
$app2->get('/stream' => sub { }, { streaming => 1 });
%analysis = (needs_streaming => 0, needs_json => 0);
for my $route (@{$app2->{routes}}) {
my $f = $route->{features} // {};
$analysis{needs_streaming} = 1 if $f->{streaming};
$analysis{needs_json} = 1 if $f->{parse_json};
}
ok(!$analysis{needs_json}, 'json not detected when not used');
};
done_testing();
t/0027-chunked-encoding.t view on Meta::CPAN
require XS::JIT::Builder;
my $builder = XS::JIT::Builder->new;
Hypersonic::Protocol::HTTP1->gen_chunked_start($builder);
my $code = $builder->code;
like($code, qr/void send_chunked_headers\(/, 'function defined');
like($code, qr/Transfer-Encoding: chunked/, 'chunked header included');
like($code, qr/Content-Type: %s/, 'content-type placeholder');
like($code, qr/Connection: keep-alive/, 'keep-alive for streaming');
like($code, qr/send\(fd, headers/, 'sends headers');
};
subtest 'gen_chunked_write uses writev' => sub {
plan tests => 4;
require XS::JIT::Builder;
my $builder = XS::JIT::Builder->new;
Hypersonic::Protocol::HTTP1->gen_chunked_write($builder);
t/0027-chunked-encoding.t view on Meta::CPAN
my $final = Hypersonic::Protocol::HTTP1->build_final_chunk();
# "0" CRLF CRLF (we don't send trailers)
is($final, "0\r\n\r\n", 'final chunk correct');
is(length($final), 5, 'final chunk is 5 bytes');
};
# ============================================================
# Test 14-15: Integration with Hypersonic
# ============================================================
subtest 'Hypersonic compiles streaming routes' => sub {
plan tests => 3;
my $app = Hypersonic->new();
$app->get('/events' => sub {
my ($req, $stream) = @_;
$stream->content_type('text/event-stream');
$stream->write("data: hello\n\n");
$stream->end();
}, { streaming => 1 });
$app->get('/' => sub { return 'hello' });
eval { $app->compile() };
ok(!$@, 'compile succeeds') or diag($@);
ok($app->{route_analysis}{needs_streaming}, 'streaming detected');
ok($app->{compiled}, 'app is compiled');
};
subtest 'Generated code includes chunked functions' => sub {
plan tests => 3;
require XS::JIT::Builder;
my $builder = XS::JIT::Builder->new;
# Generate all HTTP1 chunked code
t/0028-http2-streaming.t view on Meta::CPAN
}
# ============================================================
# Test 4: Check nghttp2 availability
# ============================================================
my $has_nghttp2 = Hypersonic::Protocol::HTTP2->check_nghttp2();
ok(defined $has_nghttp2, 'nghttp2 detection works');
diag($has_nghttp2 ? 'nghttp2 found' : 'nghttp2 not available');
# ============================================================
# Test 5-9: JIT code generation for HTTP/2 streaming
# ============================================================
SKIP: {
skip 'nghttp2 not available', 5 unless $has_nghttp2;
require XS::JIT::Builder;
subtest 'gen_stream_headers generates correct code' => sub {
plan tests => 4;
my $builder = XS::JIT::Builder->new;
t/0028-http2-streaming.t view on Meta::CPAN
};
subtest 'Stream defaults to http1' => sub {
plan tests => 1;
my $stream = Hypersonic::Stream->new(fd => 1);
is($stream->protocol, 'http1', 'default protocol is http1');
};
# ============================================================
# Test 13-15: generate_streaming method
# ============================================================
SKIP: {
skip 'nghttp2 not available', 3 unless $has_nghttp2;
subtest 'generate_streaming generates all functions' => sub {
plan tests => 5;
require XS::JIT::Builder;
my $builder = XS::JIT::Builder->new;
Hypersonic::Protocol::HTTP2->generate_streaming($builder);
my $code = $builder->code;
like($code, qr/h2_stream_headers/, 'has h2_stream_headers');
like($code, qr/h2_stream_data/, 'has h2_stream_data');
like($code, qr/h2_stream_end/, 'has h2_stream_end');
like($code, qr/h2_can_send/, 'has h2_can_send');
like($code, qr/hypersonic_h2_stream/, 'has XS wrappers');
};
subtest 'Stream.generate_c_code with http2 option' => sub {
t/0028-http2-streaming.t view on Meta::CPAN
require XS::JIT::Builder;
my $builder = XS::JIT::Builder->new;
Hypersonic::Stream->generate_c_code($builder, {
max_connections => 100,
http2 => 1,
});
my $code = $builder->code;
# HTTP/1.1 streaming code (always present)
like($code, qr/stream_start|stream_write_chunk/, 'has HTTP/1.1 streaming code');
# Stream registry tracks http2 flag for future HTTP/2 support
like($code, qr/int http2/, 'has http2 flag in StreamState');
};
subtest 'Stream.generate_c_code without http2 option' => sub {
plan tests => 2;
require XS::JIT::Builder;
my $builder = XS::JIT::Builder->new;
Hypersonic::Stream->generate_c_code($builder, {
max_connections => 100,
http2 => 0,
});
my $code = $builder->code;
# HTTP/1.1 streaming code
like($code, qr/stream_start|stream_write_chunk/, 'has HTTP/1.1 streaming code');
# No HTTP/2 streaming code
unlike($code, qr/h2_stream_headers/, 'no HTTP/2 streaming code');
};
}
# ============================================================
# Test 16-18: Integration with Hypersonic
# ============================================================
SKIP: {
skip 'nghttp2 not available', 3 unless $has_nghttp2;
# Check for TLS test certificates
my $cert_file = 't/certs/server.crt';
my $key_file = 't/certs/server.key';
my $has_certs = (-f $cert_file && -f $key_file);
SKIP: {
skip 'TLS certificates not found', 1 unless $has_certs;
subtest 'Hypersonic compile with http2 and streaming' => sub {
plan tests => 3;
my $app = Hypersonic->new(
http2 => 1,
tls => 1,
cert => $cert_file,
key => $key_file,
);
$app->get('/events' => sub {
my ($req, $stream) = @_;
$stream->write("data: test\n\n");
$stream->end();
}, { streaming => 1 });
$app->get('/' => sub { return { status => 200, body => 'hello' } });
eval { $app->compile() };
ok(!$@, 'compile succeeds') or diag($@);
ok($app->{http2}, 'http2 enabled');
ok($app->{route_analysis}{needs_streaming}, 'streaming detected');
};
}
SKIP: {
skip 'TLS certificates not found', 1 unless $has_certs;
subtest 'Hypersonic compile with http2, no streaming' => sub {
plan tests => 3;
my $app = Hypersonic->new(
http2 => 1,
tls => 1,
cert => $cert_file,
key => $key_file,
);
$app->get('/' => sub { return { status => 200, body => 'hello' } });
eval { $app->compile() };
ok(!$@, 'compile succeeds') or diag($@);
ok($app->{http2}, 'http2 enabled');
ok(!$app->{route_analysis}{needs_streaming}, 'no streaming detected');
};
}
subtest 'Hypersonic without http2 still compiles streaming' => sub {
plan tests => 3;
my $app = Hypersonic->new(http2 => 0);
$app->get('/stream' => sub {
my ($req, $stream) = @_;
$stream->write("chunk");
$stream->end();
}, { streaming => 1 });
$app->get('/' => sub { return 'hello' });
eval { $app->compile() };
ok(!$@, 'compile succeeds') or diag($@);
ok(!$app->{http2}, 'http2 disabled');
ok($app->{route_analysis}{needs_streaming}, 'streaming detected');
};
}
done_testing();
t/0035-e2e-streaming.t view on Meta::CPAN
$server->get('/' => sub { 'OK' });
# Streaming route - sends multiple chunks
$server->get('/stream' => sub {
my ($req, $stream) = @_;
$stream->headers(200, { 'Content-Type' => 'text/plain' });
$stream->write("chunk1\n");
$stream->write("chunk2\n");
$stream->write("chunk3\n");
$stream->end();
}, { streaming => 1 });
# SSE route - sends server-sent events
$server->get('/sse' => sub {
my ($req, $stream) = @_;
require Hypersonic::SSE;
my $sse = Hypersonic::SSE->new($stream);
$sse->event(type => 'greeting', data => 'Hello SSE!');
$sse->event(type => 'update', data => 'First update', id => '1');
$sse->event(type => 'update', data => "Multi\nLine\nData", id => '2');
$sse->data('simple data');
$sse->comment('test comment');
$sse->retry(5000);
$sse->close();
}, { streaming => 1 });
# SSE with keepalive test
$server->get('/sse-keepalive' => sub {
my ($req, $stream) = @_;
require Hypersonic::SSE;
my $sse = Hypersonic::SSE->new($stream, keepalive => 1);
$sse->event(data => 'start');
$sse->keepalive();
$sse->event(data => 'end');
$sse->close();
}, { streaming => 1 });
# WebSocket echo route
$server->websocket('/ws-echo' => sub {
my ($ws) = @_;
$ws->on(message => sub {
my ($data) = @_;
$ws->send("echo: $data");
});
$ws->on(close => sub {
# Connection closed
t/0035-e2e-streaming.t view on Meta::CPAN
$ws->on(open => sub {
$ws->send("Welcome!");
});
});
$server->compile();
$server->run(port => $port, workers => 1);
});
# Parent - wait for server to start. This test compiles the largest
# JIT module of the suite (regular + streaming + 2 SSE routes + 2
# websocket routes); on smokers with -O0 -g debugging perls the gcc
# invocation alone can take 30-60s, which is why earlier 5s/10s
# timeouts produced the "child wrote no output" bailouts on CPAN
# testers (host k93msid, perl 5.12 .. 5.42).
wait_for_port($port, { pid => $pid, log => $log, tries => 600, sleep => 0.2 })
or BAIL_OUT("server child failed to bind port $port (see diag above)");
# ============================================================
# Test helpers
# ============================================================
t/0035-e2e-streaming.t view on Meta::CPAN
my $buf;
my $bytes = sysread($sock, $buf, 4096);
last unless $bytes;
$response .= $buf;
}
close($sock);
return $response;
}
# Read streaming response incrementally
sub http_streaming_request {
my ($method, $path, %opts) = @_;
my $timeout = $opts{timeout} // 5;
my $headers = $opts{headers} // {};
my $sock = IO::Socket::INET->new(
PeerAddr => '127.0.0.1',
PeerPort => $port,
Proto => 'tcp',
Timeout => $timeout,
);
t/0035-e2e-streaming.t view on Meta::CPAN
like($resp, qr/200 OK/, 'Returns 200');
};
# ============================================================
# Test 2: Streaming response with multiple chunks
# ============================================================
subtest 'Streaming response (chunked)' => sub {
plan tests => 6;
my $resp = http_request('GET', '/stream');
ok($resp, 'Got streaming response');
like($resp, qr/HTTP\/1\.1 200 OK/, 'Status 200');
like($resp, qr/Transfer-Encoding: chunked/i, 'Chunked encoding header');
like($resp, qr/chunk1/, 'Contains chunk1');
like($resp, qr/chunk2/, 'Contains chunk2');
like($resp, qr/chunk3/, 'Contains chunk3');
};
# ============================================================
# Test 3: SSE response format
# ============================================================
t/2103-ua-minimal-compile.t view on Meta::CPAN
$server->compile();
$server->run(port => $PORT, workers => 1);
});
# tries => 50 was 10s, which timed out on the ANDK k93msid
# DEBUGGING perl 5.24.0 smoker (Hypersonic 0.18 report
# 404e86a8) with `Child server still alive but not listening on
# port 34861`. Raise to 600 (120s) and let HypersonicTest's
# 60s floor + PERL_TEST_TIME_OUT_FACTOR scaling handle the rest.
# This is the same budget t/0035-e2e-streaming.t uses.
wait_for_port($PORT, { pid => $server_pid, log => $server_log, tries => 600, sleep => 0.2 })
or die "Server failed to start";
return 1;
}
sub stop_test_server {
if ($server_pid) {
kill('TERM', $server_pid);
waitpid($server_pid, 0);
}
t/lib/HypersonicTest.pm view on Meta::CPAN
# wait_for_port($port [, $opts]) -> 1 / 0
#
# Probes 127.0.0.1:$port until something accepts or we give up. On
# timeout, if $opts->{log} is given, diag()s the child's captured
# output; if $opts->{pid} is given, diag()s the child's exit status.
sub wait_for_port {
my ($port, $opts) = @_;
$opts //= {};
# Default: 60 seconds (300 tries x 0.2s). The JIT compile of a
# full Hypersonic server (all event backends + TLS + HTTP/2 +
# WebSocket + SSE + streaming) can take >30s on a debugging-perl
# smoke host (gcc -O0 -g), and on older perls/hosts even longer.
# The previous 5s default caused t/0035-e2e-streaming.t bailouts
# in CPAN tester reports on the k93msid host for perl 5.12..5.42.
my $max_tries = $opts->{tries} // 300;
my $sleep = $opts->{sleep} // 0.2;
# Enforce a MINIMUM total wait of 60 wallclock seconds regardless
# of what the caller asked for. Most test files in this dist pass
# `tries => 50` which (at 0.2s/try) is only 10s; that's nowhere
# near enough for the JIT compile on a slow/DEBUGGING smoker.
# Also scale by PERL_TEST_TIME_OUT_FACTOR (some smokers set this
# to 3 to indicate "I'm a slow box, give tests 3x the budget").