Api4 Services - Lazy-load subscriber-objects
authorTim Otten <totten@civicrm.org>
Tue, 4 May 2021 20:36:48 +0000 (13:36 -0700)
committerTim Otten <totten@civicrm.org>
Wed, 5 May 2021 01:24:48 +0000 (18:24 -0700)
commita9d469f980abf4fb797a0b2d26868ce3ad442b34
treefd17c9cae473cdb815df56f3c402cdd00d096c63
parentfc9b680f4d7d7359b7c7b9ca1aeafdaec08be1d9
Api4 Services - Lazy-load subscriber-objects

This refines the way in which `Civi/Api4/Event/Subscriber/**.php` are loaded.
This makes it safer (from a performance POV) to continue adding more
subscribers/listeners without worrying that it will impact the quantity of
files/classes/opcodes/SLOC loaded in a typical page-view.

A good way to visualize this change is to skim `getDispatcherService()`
(`[civicrm.compile]/CachedCiviContainer.*.php`) before and after the patch.
(Examples included below.)

Before
------

During every page-load, you need to register event-listeners.  For
subscriber-objects (like `Civi/Api4/Event/Subscriber/**.php`), you get the list
of subscriptions by calling `getSubscribedEvents()`.  Therefore, on every
page-load, you must load/process the subscriber (regardless of whether it will
actually be used) on the chance it that may be needed.

In `CachedCiviContainer.*.php`, you will see snippets like:

```php
    protected function getDispatcherService()
    {
        ...
        $instance->addSubscriber(${($_ = isset($this->services['Civi_Api4_Event_Subscriber_ActivityPreCreationSubscriber']) ? $this->services['Civi_Api4_Event_Subscriber_ActivityPreCreationSubscriber'] : ($this->services['Civi_Api4_Event_Subscriber_ActivityPreCreationSubscriber'] = new \Civi\Api4\Event\Subscriber\ActivityPreCreationSubscriber())) && false ?: '_'});
        $instance->addSubscriber(${($_ = isset($this->services['Civi_Api4_Event_Subscriber_ActivitySchemaMapSubscriber']) ? $this->services['Civi_Api4_Event_Subscriber_ActivitySchemaMapSubscriber'] : ($this->services['Civi_Api4_Event_Subscriber_ActivitySchemaMapSubscriber'] = new \Civi\Api4\Event\Subscriber\ActivitySchemaMapSubscriber())) && false ?: '_'});
        ...
```

Observe that it instantiates `ActivitySchemaMapSubscriber` then passes the instance to `addSubscriber()`.

After
-----

You only need to instantiate service-objects if (a) you are building a fresh container or (b) actually running an event.

This works by calling `getSubscribedEvents()` when building the container.  The
list of events is cached in the container.

In `CachedCiviContainer.*.php`, you will see snippets like:

```php
    protected function getDispatcherService()
    {
        ...

        $instance->addSubscriberServiceMap('Civi_Api4_Event_Subscriber_ActivityPreCreationSubscriber', ['civi.api.prepare' => 'onApiPrepare']);
        $instance->addSubscriberServiceMap('Civi_Api4_Event_Subscriber_ActivitySchemaMapSubscriber', ['api.schema_map.build' => 'onSchemaBuild']);
        ...
```

Observe that it alludes to `ActivityPreCreationSubscriber` symbolically but
does not need an actual instance.

Comments
-----------------

1.  To see that this is equivalent, I used `cv debug:event-dispatcher` before
and after the patch.  This requires an updated version of `cv`, and the
formatting is a little a different, but it does show the same list of
listeners.

2.  There could be some concern like, "What happens if you're upgrading and
have a cached list of subscription events?" Well, note that
`CRM_Api4_Services::hook_container` already puts a cached list of subscribers
in the container.  It also registers the `FileResource`.  Thus, if a file
`Civi/Api4/Event/Subscriber/**.php` changes, it already makes the decision to
recompile based on `filemtime()`.

3. There should already be a lot of test-coverage which hits code-paths for these listeners.
   (If this were generally non-functional, you'd see massive failures.)

4. For `r-run`, I picked an arbitary subscriber (`ActivitySchemaMapSubscriber`), then:

   * At the start of the file, add a statement to log whenever the file is read.
     ```php
     file_put_contents('/tmp/parselog.txt', sprintf("%s: %s: %s\n\n",date('Y-m-d H:i:s'), __FILE__, \CRM_Core_Error::formatBacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 15))), FILE_APPEND);
     ```
   * In a separate window, do a `tail -f /tmp/parselog.txt`.
   * Edit the file to add/remove listeners (like `hook_civicrm_alterContent`)
   * Request some Civi page (`curl 'http://dmaster.127.0.0.1.nip.io:8001/civicrm/admin?reset=1'`). It's not important that it actually runs the full page...
     just that we boot up Civi to look for the page.
   * Alternately repeat the past few steps. Observe thta it only parses the file if there has been a change or if the relevant event(s) actually fire.
CRM/Api4/Services.php
Civi/Core/CiviEventDispatcher.php