NAME

Catalyst::Seal::Execute - the nine executions a request makes

DESCRIPTION

A request that reaches one action executes nine times: the private _BEGIN, _AUTO, _ACTION and _END steps, the action itself, and the forwards between them. Each of those goes through Catalyst::execute, Catalyst::Action::dispatch and Catalyst::Action::execute, and between them those three spend most of their time reaching values through accessors.

execute

The stock body calls depth, use_stats twice, stack twice, req, args, and writes and reads state - nine accessor calls, nine times a request, before any of them has done anything. They are all reads of a plain hash slot on an immutable class, so this reads the slot.

Worth about 5 us a request. Nothing else about the body changes: the recursion guard, the detach and go rethrows, the _handle_http_exception path and the exact wording of a caught exception are the stock ones.

The two use_stats calls are gone from the common body rather than answered more cheaply, because setup_stats has already decided the answer by the time this runs. An application with stats on gets a body that keeps them.

A slot is only read directly where the metaclass says it is a plain one: stored in a hash under its own name, not lazy, and with no type constraint, coercion, trigger, weak reference or initializer for the accessor to apply and a slot read to miss. Anything else and the stock execute stays.

use_stats and depth

use_stats is sub use_stats { 0 }, or the sub { 1 } that setup_stats installs when stats are on. Twenty calls a request, to something setup decided; it becomes the same XS constant the class data uses.

depth is scalar @{ shift-stack || [] }> - a method call to reach a method call to count an array - nine times a request. It becomes an XSUB that counts the array in the slot. An absent or undefined slot is 0, which is what the stock body's || [] says; anything that is not an array reference delegates.

Catalyst::Action::execute and ::dispatch

sub execute  { my $self = shift; $self->code->(@_) }
sub dispatch { $_[0]->has_instance ? $_[1]->execute($_[0]->instance, $_[0])
                                   : $_[1]->execute($_[0]->class,    $_[0]) }

Five accessor frames between them, nine times each a request, to reach two calls that are a hash fetch. Both become XSUBs that fetch the slot and make the call.

dispatch asks for the instance slot the way Moose's has_instance predicate does, by presence and not by definedness, and calls execute on the context as a method rather than as a resolved body, so a plugin's execute is found exactly as the stock code would find it.

Neither XSUB checks the invocant's class. Every action class is a Catalyst::Action subclass and one that did not override the method is precisely who should arrive here; an invocant with no such slot delegates to the body that was there before, so a wrong caller gets the stock error rather than a wrong answer. A role that wrapped either method leaves a Class::MOP::Method::Wrapped in the stash, and neither is sealed then.

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)