NAME

Catalyst::Seal::Accessors - XS accessors for the context hot set

DESCRIPTION

Moose's inlined readers are not slow. Measured on an immutable class, two million calls, best of five:

plain rw attribute (Moose inlined)       88.5 ns
rw with a default   (Moose inlined)      75.5 ns
lazy, after the build (Moose inlined)    99.9 ns
hand-written sub { $_[0]{x} }            76.7 ns
a delegator, sub { shift->plain(@_) }   133.8 ns
an XSUB                                  44.2 ns

Two things follow. Replacing a Moose reader with a hand-written Perl one buys nothing, so this has to be XS or not at all. And a reader is worth about 45 ns while a delegator is worth about 90, because a delegator pays for a whole extra frame before reaching the reader it stands in front of.

What is actually worth doing, in the order this module does it:

  • config, which is not a reader at all but a constant, and costs more than every reader in this phase put together.

  • The delegators, req and res, 25 calls a request between them.

config is a constant

$c->config costs 34.5 us per request inclusive under the profiler, about 11us real: an around trampoline, Catalyst::Component::config, Moose::Util::find_meta, Class::MOP::Package::get_or_add_package_symbol and Package::Stash::XS, nine calls of each.

It is a constant because Catalyst says so. The around config in Catalyst.pm croaks on any write once setup_finished is true, so after setup the value cannot change. The phase 1 constant accessor already handles this shape, and its slow path delegates to the original, which is what has to do the croaking.

The value is the same hash reference the stock chain returns, so MyApp->config->{foo} = 'bar' still works.

The delegators

req and res are

sub req { my $self = shift; return $self->request(@_) }

so every call is a frame that exists only to reach another frame. Sealing one means installing the target's own body under the delegator's name, which is only correct while the target is not itself replaced afterwards. They are sealed last for that reason.

A delegator is only sealed when its body is the exact two-statement shape above. Anything else, including an application that has overridden req, is left alone.

The attribute readers

Worth about 7 us per request over 160 calls, on the context class and the composed request and response classes. Small, but above the threshold this phase set itself.

Only readers, and only on immutable classes, where the reader is Moose's own generated one and has not been overridden or wrapped. Everything the XSUB cannot answer goes to that reader instead: a class method call, a write, an instance of a subclass, or a lazy attribute whose slot is not built yet.

Delegating the lazy miss is what keeps this small. Moose's reader does the build and the store, every call after it takes the fast path, and because nothing here ever writes a slot, a predicate keeps telling the truth.

AUTHOR

LNATION <email@lnation.org>

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)