Problem/Motivation

I was profiling the decoupled router module and the biggest overhead in there was calculating the list of resources. The same overhead will be triggered on the next request to fetch the node by ID that the router will provide. On top of that this data seems mostly static but we are generating it on every request and preserve in a static cache that gets lost.

Proposed resolution

Task 1:
Introduce a persistent cache on top of the static one, so we follow the flow static cache -> persistent cache -> generation.

Task 2:
After research (can be found in the discussion below) implementation of the ::all() method was found to be non-optimal and even the cold cache scenario can be optimized.

Both tasks are already done in the latest patch #69.
Final benchmarks can be found in #73

History / Others

During development it was found that there are cases that need to clear the static cache mid-request. This were initially raised in #44 and #49 and this is already included in the patch from #69.

RESOLVED There was a blocker that was preventing commit in jsonapi_extras - #3019729: Invalidate JSON:API Extras' cache tags when needed

Remaining tasks

JSON:API team review and commit.

User interface changes

None - this are cache internals.

API changes

None - change of implementation detail.

Data model changes

None.

Release notes snippet

Maybe note that there is a persistent cache around the data...

CommentFileSizeAuthor
#111 interdiff-110-111.txt540 bytesndobromirov
#111 3018287-111--reroll-of-89-for-8.7.11--do-not-test.patch9.38 KBndobromirov
#110 3018287-110--reroll-of-89-for-8.7.11--do-not-test.patch9.35 KBndobromirov
#89 3018287-89.patch9.4 KBWim Leers
#89 interdiff.txt2.37 KBWim Leers
#88 3018287-87.patch9.06 KBWim Leers
#88 3018287-86.patch9.1 KBWim Leers
#88 3018287-85.patch7.66 KBWim Leers
#87 3018287-87.patch8.99 KBWim Leers
#87 interdiff.txt636 bytesWim Leers
#86 3018287-86.patch9.04 KBWim Leers
#86 interdiff.txt2.23 KBWim Leers
#85 3018287-85.patch7.6 KBWim Leers
#85 interdiff.txt6.68 KBWim Leers
#84 3018287-84.patch7.53 KBndobromirov
#83 Workspace 1_338.png26.47 KBndobromirov
#83 Workspace 1_337.png26.26 KBndobromirov
#83 Workspace 1_336.png62.81 KBndobromirov
#83 Workspace 1_335.png65.89 KBndobromirov
#73 Workspace 1_296.png133.2 KBndobromirov
#73 Workspace 1_295.png26.15 KBndobromirov
#73 Workspace 1_294.png25.61 KBndobromirov
#73 Workspace 1_293.png26.4 KBndobromirov
#73 Workspace 1_292.png61.68 KBndobromirov
#73 Workspace 1_291.png129.38 KBndobromirov
#69 interdiff-3018287-66-69.txt385 bytesndobromirov
#69 jsonapi-3018287-69.patch8.19 KBndobromirov
#67 jsonapi-3018287-66.patch8.25 KBndobromirov
#63 3018287-63.patch7.42 KBWim Leers
#62 3018287-62.patch8.58 KBWim Leers
#62 interdiff.txt1.29 KBWim Leers
#61 Workspace 1_259.png93.92 KBndobromirov
#61 Workspace 1_258.png117.38 KBndobromirov
#60 3018287-60.patch8.9 KBndobromirov
#60 interdiff-3018287-59-60.txt1.06 KBndobromirov
#59 3018287-59.patch8.07 KBgabesullice
#59 interdiff.txt2.42 KBgabesullice
#56 interdiff-3018287-54-56.txt1.19 KBndobromirov
#56 3018287-56.patch7.42 KBndobromirov
#54 interdiff-3018287-52-54.txt788 bytesndobromirov
#54 3018287-54.patch7.03 KBndobromirov
#52 interdiff-3018287-49-52.txt2.42 KBndobromirov
#52 3018287-52.patch7.03 KBndobromirov
#49 3018287-49.patch5.11 KBgabesullice
#49 interdiff.txt1.54 KBgabesullice
#46 interdiff-3018287-43-46.txt2.42 KBndobromirov
#46 3018287-46.patch3.58 KBndobromirov
#43 interdiff-35-42.txt779 bytesndobromirov
#43 3018287-42.patch2.94 KBndobromirov
#42 3018287-40.patch2.58 KBWim Leers
#42 interdiff.txt1.12 KBWim Leers
#35 interdiff-31-35.txt2.02 KBndobromirov
#35 3018287-35.patch2.18 KBndobromirov
#31 interdiff-23-31.txt1.68 KBndobromirov
#31 3018287-persistent-cache-only-31.patch2.48 KBndobromirov
#23 3018287-persistent-cache-only-23.patch825 bytesndobromirov
#21 jsonapi-3018287-persistent-cache-only-21.patch616 bytesndobromirov
#20 jsonapi-3018287-20-persistent-cache-only.patch0 bytesndobromirov
#15 interdiff-13-15.txt616 bytesndobromirov
#15 3018287-15.patch3.89 KBndobromirov
#14 Workspace 1_255.png143.57 KBndobromirov
#14 Workspace 1_256.png25.89 KBndobromirov
#13 interdiff-12-13.txt2.46 KBndobromirov
#13 3018287-13.patch3.29 KBndobromirov
#12 3018287-12.patch1.06 KBndobromirov
#11 Workspace 1_254.png114.44 KBndobromirov
#6 Workspace 1_253.png25.2 KBndobromirov
#6 Workspace 1_252.png25.97 KBndobromirov
#5 Workspace 1_251.png99.13 KBndobromirov
#5 Workspace 1_250.png82.91 KBndobromirov
#5 Workspace 1_249.png103.95 KBndobromirov
Workspace 1_248.png124.57 KBndobromirov
Workspace 1_247.png81.6 KBndobromirov
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

ndobromirov created an issue. See original summary.

ndobromirov’s picture

Issue summary: View changes
ndobromirov’s picture

Title: ResourceTypeRepository::all() introduce persistent cache » ResourceTypeRepository::all - introduce persistent cache
Issue summary: View changes
e0ipso’s picture

160ms to calculate the relatable resource types only once? That seems excessive.

Can you share the insight of calculateRelatableResourceTypes? Maybe even a blackfire trace we can drill down on?

ndobromirov’s picture

FileSize
103.95 KB
82.91 KB
99.13 KB

I do not have blackfire running, but here's 2 levels deeper:

As well as the bigger offenders underneath...

ndobromirov’s picture

Issue summary: View changes
FileSize
25.97 KB
25.2 KB

Here are more contextual results...

Xhprof summary + AB results (no xdebug, no xhprof) before:

$ ab -n 100 -c 1 "http://HOSTNAME/router/translate-path?path=/&_format=json&requestId=router"
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking HOSTNAME (be patient).....done


Server Software:        Apache/2.4.18
Server Hostname:        HOSTNAME
Server Port:            80

Document Path:          /router/translate-path?path=/&_format=json&requestId=router
Document Length:        721 bytes

Concurrency Level:      1
Time taken for tests:   12.982 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      152304 bytes
HTML transferred:       72100 bytes
Requests per second:    7.70 [#/sec] (mean)
Time per request:       129.819 [ms] (mean)
Time per request:       129.819 [ms] (mean, across all concurrent requests)
Transfer rate:          11.46 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:   123  130   5.5    129     157
Waiting:      123  129   5.5    128     156
Total:        123  130   5.6    129     157

Percentage of the requests served within a certain time (ms)
  50%    129
  66%    130
  75%    130
  80%    131
  90%    132
  95%    142
  98%    156
  99%    157
 100%    157 (longest request)

Xhprof summary + AB results after:

$ ab -n 100 -c 1 "http://HOSTNAME/router/translate-path?path=/&_format=json&requestId=router"
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking HOSTNAME (be patient).....done


Server Software:        Apache/2.4.18
Server Hostname:        HOSTNAME
Server Port:            80

Document Path:          /router/translate-path?path=/&_format=json&requestId=router
Document Length:        721 bytes

Concurrency Level:      1
Time taken for tests:   4.125 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      152808 bytes
HTML transferred:       72100 bytes
Requests per second:    24.24 [#/sec] (mean)
Time per request:       41.248 [ms] (mean)
Time per request:       41.248 [ms] (mean, across all concurrent requests)
Transfer rate:          36.18 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:    37   41   2.8     41      57
Waiting:       37   41   2.9     41      56
Total:         37   41   2.9     41      57

Percentage of the requests served within a certain time (ms)
  50%     41
  66%     42
  75%     42
  80%     43
  90%     43
  95%     44
  98%     52
  99%     57
 100%     57 (longest request)
e0ipso’s picture

Ugh. Even though I don't like the extra complexity this brings in the numbers make a good point. NOTE: I do not think this will be a problem in non-paragraph based data models.

I'm on the fence on this. This goes against the simplification we hoped to introduce in #3016733: Simplify ResourceTypeRepository; use a protected property in place of an in-memory cache bin..

e0ipso’s picture

ndobromirov’s picture

I do not think this will be a problem in non-paragraph based data models.

The module degrades proportionally to relationships and total entity types amount. Whether this are paragraphs, custom blocks, nodes, terms, custom entities, webforms, it will degrade with the site's complexity.


The cache hierarchy I am introducing can be a moved outside from the class in 2 ways:

  1. Define the chained cache as a service and inject it - no code changes to current class.
  2. Out-scope cache code fully out of the repository code and move to decoration paradigm. Then all cache related code can be added in a standalone decorator. This will remove code complexity (amounts of dependencies) in both places. Maybe ease the work that extras will need to do to overwrite the service as well (TBD).

During further checks i found that Drupal\jsonapi\ResourceType\ResourceTypeRepository::isReferenceFieldDefinition is executed SO many times (1800+), but it's executed over 133 fields (amount taken from fields report UI). So adding a static cache there should speed things quite a bit for cold cache scenario as well.


Research is ongoing...

ndobromirov’s picture

FileSize
114.44 KB

So the static cache helped the cold scenario dropping the Drupal\jsonapi\ResourceType\ResourceTypeRepository::isReferenceFieldDefinition resulting in this speed-up without any caches:

Here is the AB as well (no xhprof):

$ ab -n 100 -c 1 "http://HOSTNAME/router/translate-path?path=/&_format=json&requestId=router"
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking HOSTNAME (be patient).....done


Server Software:        Apache/2.4.18
Server Hostname:        HOSTNAME
Server Port:            80

Document Path:          /router/translate-path?path=/&_format=json&requestId=router
Document Length:        721 bytes

Concurrency Level:      1
Time taken for tests:   9.641 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      152748 bytes
HTML transferred:       72100 bytes
Requests per second:    10.37 [#/sec] (mean)
Time per request:       96.410 [ms] (mean)
Time per request:       96.410 [ms] (mean, across all concurrent requests)
Transfer rate:          15.47 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:    89   96  14.5     94     222
Waiting:       89   96  14.5     93     222
Total:         89   96  14.5     94     223

Percentage of the requests served within a certain time (ms)
  50%     94
  66%     95
  75%     96
  80%     96
  90%     98
  95%    110
  98%    141
  99%    223
 100%    223 (longest request)

PоC patch is coming shortly.

ndobromirov’s picture

Status: Active » Needs review
FileSize
1.06 KB

This dropped some good 40-50k function calls based on the xhprof top level summary.
Code here can be moved to a decorator without any issues.


Note: The persistent cache is still an open question.
ndobromirov’s picture

FileSize
3.29 KB
2.46 KB

I've managed to speed-up the other slow internal underneath all from ~35ms to ~15ms on cold cache. Total function calls are down to around 35k from around 170k.

The high level idea is that the list of resources instead of numerically indexed is now keyed by entity_type:bundle so we will not need to loop the list over and over again, but just look-up the key for presence in the map.

This changes are small improvements to the internals of the service, but the persistent cache in there will still help, as it will spare all that computation on every request.

ndobromirov’s picture

FileSize
25.89 KB
143.57 KB

Here are the benchmarks that the last patch can achieve (without a persistent cache).

Top level summary:

Changes in Drupal\jsonapi\ResourceType\ResourceTypeRepository::getRelatableResourceTypesFromFieldDefinition:

AB (no xdebug, no xhprof):

$ ab -n 100 -c 1 "http://HOSTNAME/router/translate-path?path=/&_format=json&requestId=router"
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking HOSTNAME (be patient).....done


Server Software:        Apache/2.4.18
Server Hostname:        HOSTNAME
Server Port:            80

Document Path:          /router/translate-path?path=/&_format=json&requestId=router
Document Length:        721 bytes

Concurrency Level:      1
Time taken for tests:   7.947 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      152845 bytes
HTML transferred:       72100 bytes
Requests per second:    12.58 [#/sec] (mean)
Time per request:       79.472 [ms] (mean)
Time per request:       79.472 [ms] (mean, across all concurrent requests)
Transfer rate:          18.78 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:    73   79  10.8     78     179
Waiting:       73   79  10.8     78     179
Total:         73   79  10.8     78     180

Percentage of the requests served within a certain time (ms)
  50%     78
  66%     79
  75%     80
  80%     80
  90%     81
  95%     87
  98%    101
  99%    180
 100%    180 (longest request)
ndobromirov’s picture

Here is the final patch that will add the persistent cache with no further changes to code - just tweaks on the services that gets injected.
Cached performance is comparable to the one in the description.

ndobromirov’s picture

Issue summary: View changes
ndobromirov’s picture

@eoipso any idea why the builds are not triggered?

ndobromirov’s picture

Maybe change the flat list from "entity:bundle" => resource to "entity" => ["bundle" => resource]. This way we will do a micro-optimization, sparing a key preparation on every call and have direct array access on all places I've done changes in here. Nice to have...

ndobromirov’s picture

The 2 micro-optimizations can be moved to a separate issue, as they are not directly related to the persistent cache, but optimizing the old scenario that the cache will resolve.

ndobromirov’s picture

I need this patch in a way usable in composer. The one in #15 is conflicting for me with #2992833: Add a version negotiation to revisionable resource types (#153), so here is a much simpler one with just the persistent cache changes in.

Note: The cold cache scenarios are not included in here, but they are both still valid and the changes in 15 are resolving them.

PS: Use the patch from the next comment...

ndobromirov’s picture

Well - I need to upload a patch :D

Status: Needs review » Needs work

The last submitted patch, 21: jsonapi-3018287-persistent-cache-only-21.patch, failed testing. View results

ndobromirov’s picture

Here is a rebased patch on top of the latest dev.
It is including only the persistent cache tweaks in jsonapi.services.yml file.
The performance tweaks will be moved / implemented in their own issues, unless anyone objects to that.

Status: Needs review » Needs work

The last submitted patch, 23: 3018287-persistent-cache-only-23.patch, failed testing. View results

Wim Leers’s picture

1) Drupal\Tests\jsonapi\Functional\JsonApiFunctionalTest::testRead
Undefined index: meta

That's not failing in HEAD, so it seems like a legitimate failure. OTOH, it's pretty strange that a small abstract change like this causes such a specific failure. I'll try to reproduce.

Wim Leers’s picture

Title: ResourceTypeRepository::all - introduce persistent cache » ResourceTypeRepository computes ResourceType value objects on *every request*
Status: Needs work » Needs review
Issue tags: +API-First Initiative, +Needs manual testing

Reproduced. The failure is due to the persistent cache not being invalidated after the jsonapi_test_collection_count is installed.

Which brings us to another point: JSON:API Extras configuration changes need to invalidate this cache. \Drupal\jsonapi_extras\EventSubscriber\ConfigSubscriber::onResponse() is adding the 'config:jsonapi_extras.settings' and 'config:jsonapi_resource_config_list' cache tags, but they'll also need to be associated when the ResourceType objects are cached that are generated by \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository.

IOW: this needs more test coverage, and needs manual testing with JSON:API Extras. It potentially also needs changes in JSON:API Extras. We don't want to break JSON:API Extras during the 2.0 RCs.


I'm very glad to see that the work we did on making things more isolated, including making ResourceType value objects actual value objects (and no longer call any services), are paying off. This sort of optimization is exactly what I had in mind while we were working on that 😀

ndobromirov’s picture

Will something like this help in an install file on the test module:

function jsonapi_test_collection_count_install() {
  \Drupal::service('cache_tags.invalidator')->invalidateTags(['jsonapi_resource_types']);
}

I can extend the patch to have it, so we can spare the manual testing.


On top of that maybe expose the tags we are setting on the cached list as a protected method or something, so extras can overwrite it and add the additional tags in there.
Wim Leers’s picture

Yes it would. At least for jsonapi. But jsonapi_extras 3.x doesn't use those cache tags, so you'd still need manual testing. Or … better yet, expand the automated tests of JSON:API Extras :)

ndobromirov’s picture

Wim Leers’s picture

ndobromirov’s picture

Here are the proposed changes
- Tweak on the install of the test module.
- New protected property to be used by jsonapi_extras to modify cache tags used.

Status: Needs review » Needs work

The last submitted patch, 31: 3018287-persistent-cache-only-31.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

This LGTM. Just needs to fix the CS violation:

FILE: ...napi_test_collection_count/jsonapi_test_collection_count.install
----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
 1 | ERROR | [x] Missing file doc comment
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------
Wim Leers’s picture

Agreed that this is looking close!

  1. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -70,6 +70,13 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +   * List of default cache tags added by the repository.
    

    Cache tags used for caching the repository.

  2. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -70,6 +70,13 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +   * @var array
    

    Nit: string[]

  3. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -70,6 +70,13 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +  protected $defaultCacheTags = ['jsonapi_resource_types'];
    

    s/$defaultCacheTags/$cacheTags/

  4. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -107,7 +114,7 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    -      $this->staticCache->set('jsonapi.resource_types', $resource_types, Cache::PERMANENT, ['jsonapi_resource_types']);
    +      $this->staticCache->set('jsonapi.resource_types', $resource_types, Cache::PERMANENT, $this->defaultCacheTags);
    
    +++ b/tests/modules/jsonapi_test_collection_count/jsonapi_test_collection_count.install
    @@ -0,0 +1,8 @@
    +  \Drupal::service('cache_tags.invalidator')->invalidateTags(['jsonapi_resource_types']);
    

    Alternatively, we could remove this hook and instead do Cache::mergeTags($this->cacheTags, ['config:core.extension']), then it'd be invalidated whenever a module is installed, which seems the more prudent approach anyway :)

ndobromirov’s picture

Here are Wim's comments. They should resolve the on from #33 as well, as we do not need the install file now.
1-3: Fixed.
4. Nice idea :)! This will be way more durable and as any module might change resources configuration so jsonapi will react on that.

ndobromirov’s picture

Status: Needs work » Needs review
e0ipso’s picture

Or … better yet, expand the automated tests of JSON:API Extras :)

Please do!

Wim Leers’s picture

Issue tags: +needs profiling

This is looking much better now! 😀

+++ b/jsonapi.services.yml
@@ -142,9 +142,17 @@ services:
+  cache.jsonapi_resource_types:
+    class: Drupal\Core\Cache\BackendChain
+    arguments: ['default']
+    calls:
+      - ['appendBackend', ['@cache.jsonapi_memory']]
+      - ['appendBackend', ['@cache.default']]

This is looking quite complex. In fact, this is the first time I've ever seen this. I understand how this works, but how do we know we absolutely need this? Are we concerned about repeated unserialization? Needs profiling to prove we really need both layers.

e0ipso’s picture

Are we concerned about repeated unserialization? Needs profiling to prove we really need both layers.

We are. In fact this proved to be a problem already with the previous cache.static backend.

ndobromirov’s picture

This exact case was the root cause for #3014232: [regression] ResourceTypeRepository is significantly slower in JSON:API 2, becomes noticeable when handling hundreds of interlinked resources, in my case around 60% of the response time (~7 seconds out of 12).

ndobromirov’s picture

Status: Needs review » Needs work

If I recall correctly we need to tune the backwards compatibility layer as well.

Wim Leers’s picture

Status: Needs work » Needs review
Issue tags: -Needs manual testing, -needs profiling
FileSize
1.12 KB
2.58 KB

Thanks for confirming.

  1. +++ b/jsonapi.services.yml
    @@ -142,9 +142,17 @@ services:
    +  cache.jsonapi_resource_types:
    

    I think we can mark this private too.

  2. +++ b/jsonapi.services.yml
    @@ -142,9 +142,17 @@ services:
    +    arguments: ['default']
    ...
    +    # We need this, so Drupal's cache_tags.invalidator service knows about it.
         # This way it can invalidate the data in here based on tags.
         tags: [{ name: cache.bin }]
    

    And rather than repeating this, I wanted to inherit it using parent: cache.default, but that unfortunately fails; it somehow insists on inheriting the class too then.

  3. +++ b/jsonapi.services.yml
    @@ -142,9 +142,17 @@ services:
    +    arguments: ['default']
    

    This doesn't quite make sense, this is not the default bin, but the jsonapi_resource_types bin. Due to chaining, it just happens to reuse some of it.

  4. +++ b/jsonapi.services.yml
    @@ -142,9 +142,17 @@ services:
    +    # We need this, so Drupal's cache_tags.invalidator service knows about it.
         # This way it can invalidate the data in here based on tags.
    

    This comment can now go away, because this is now clearly a cache bin, whereas before, when this was just using the MemoryCache class, that was not obvious.

  5. Most importantly: this could use a comment documenting the rationale.

Addressed all that.

ndobromirov’s picture

Here is the backwards compatible change for 8.5 - dropping the 2 levels of cache.

Wim Leers’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests
+++ b/src/ResourceType/ResourceTypeRepository.php
@@ -107,7 +114,11 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
+      $cache_tags = Cache::mergeTags($this->cacheTags, ['config:core.extension']);

We should also be invalidating whenever the set of fields is modified through for example adding a new configurable field (the entity_field_info cache tag) and/or when the set of entity types changes (the entity_types cache tag).

At least the former can easily be tested by adding a new required field with a default value; this should immediately show up.

Right now, this patch's more aggressive caching is breaking that. That needs test coverage!

Wim Leers’s picture

#43 looks good!

ndobromirov’s picture

Here is the changes from #42 incorporated onto #43.
Added the 2 new tags mentioned in #44.

The only things are the tests. I will not be proficient in writing them so anyone can take over.

ndobromirov’s picture

ndobromirov’s picture

Any examples I can steal test code for this :) ?

gabesullice’s picture

Status: Needs work » Needs review
FileSize
1.54 KB
5.11 KB

This test should do it.

It will fail because there's still a class property used as a cache that's not cleared with the ->all() cache is cleared.

Status: Needs review » Needs work

The last submitted patch, 49: 3018287-49.patch, failed testing. View results

ndobromirov’s picture

We can likely drop the $cache property if we introduce the indexing from #13's interdiff.

Will have to profile it again at that point...

ndobromirov’s picture

Status: Needs work » Needs review
FileSize
7.03 KB
2.42 KB

Here is the proposed change...

Status: Needs review » Needs work

The last submitted patch, 52: 3018287-52.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

ndobromirov’s picture

Status: Needs work » Needs review
FileSize
7.03 KB
788 bytes

Fixed CS issue...

Status: Needs review » Needs work

The last submitted patch, 54: 3018287-54.patch, failed testing. View results

ndobromirov’s picture

Status: Needs work » Needs review
FileSize
7.42 KB
1.19 KB

Hopefully the last CS fix...

ndobromirov’s picture

This will likely need a follow-up in jsonapi_extras as well to trigger the cache clear whenever resource configs are changed.

e0ipso’s picture

I'd like to see a patch for #3019729: Invalidate JSON:API Extras' cache tags when needed before merging this. Otherwise JSON:API Extras will break, people will get impatient and I'll have the pressure to fix it quick-quick-quick.

gabesullice’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
2.42 KB
8.07 KB

Thanks @ndobromirov!

Since we're changing ResourceTypeRepository, it's a good opportunity to clean up a little. So, don't mind me. Your patch looked good and I haven't fundamentally changed any of it.

I won't commit this till #3019729: Invalidate JSON:API Extras' cache tags when needed lands, per @e0ipso.

ndobromirov’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
1.06 KB
8.9 KB

Re-adding the changes from #12 (speeds-up the cold cache scenario a bit).

ndobromirov’s picture

FileSize
117.38 KB
93.92 KB

Here are benchmarks on the patch from #56. There is a small performance regression in the ResourceTypeRepository::get($entity_type, $bundle). But I think it's a good enough compromise. Compared to the original speed couple of weeks back - 109ms.

More details about the original issue: #3017239: Optimize ResourceTypeRepository::get(): don't loop over all possible resource types in every call

Here is the before (6ms):

And after the patch (18ms).

Wim Leers’s picture

Title: ResourceTypeRepository computes ResourceType value objects on *every request* » [PP-1] ResourceTypeRepository computes ResourceType value objects on *every request*
Status: Needs review » Postponed
Issue tags: -Needs tests
FileSize
1.29 KB
8.58 KB
  1. +++ b/README.md
    @@ -1 +1,2 @@
    -Please read the [jsonapi.api.php](jsonapi.api.php) file and the online [JSON API](https://www.drupal.org/docs/8/modules/json-api) documentation.
    +Please read the [jsonapi.api.php](jsonapi.api.php) file and the online
    +[JSON API](https://www.drupal.org/docs/8/modules/json-api) documentation.
    

    Let's not change this.

  2. +++ b/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php
    @@ -97,4 +98,13 @@ class ResourceTypeRepositoryTest extends KernelTestBase {
    +  public function testCaching() {
    +    $this->assertEmpty($this->resourceTypeRepository->get('node', 'article')->getRelatableResourceTypesByField('field_relationship'));
    +    $this->createEntityReferenceField('node', 'article', 'field_relationship', 'Related entity', 'node');
    +    $this->assertCount(2, $this->resourceTypeRepository->get('node', 'article')->getRelatableResourceTypesByField('field_relationship'));
    +  }
    

    I asked for test coverage in #44.

    This tests that. So removing the tag.

  3. +++ b/jsonapi.services.yml
    @@ -142,11 +142,23 @@ services:
    +    private: true
    ...
    +    private: true
    

    These don't have any effect; it needs to be public: false. Yes, this is incredibly bad DX on Symfony's behalf…

#3019729: Invalidate JSON:API Extras' cache tags when needed was blocked on #3020237: [Regression] Broken with latest jsonapi 2.0-rc3. #3020237 landed yesterday. I just re-RTBC'd #3019729. Soon this will be RTBC too.

Wim Leers’s picture

Title: [PP-1] ResourceTypeRepository computes ResourceType value objects on *every request* » ResourceTypeRepository computes ResourceType value objects on *every request*
Status: Postponed » Needs review
FileSize
7.42 KB

#3019729: Invalidate JSON:API Extras' cache tags when needed landed.

It conflicted with #2992833: Add a version negotiation to revisionable resource types, which added a hunk that this patch was also adding. So this patch became slightly smaller & simpler :)

Status: Needs review » Needs work

The last submitted patch, 63: 3018287-63.patch, failed testing. View results

ndobromirov’s picture

I am not getting why the system thinks the service is not there as it's clearly defined in the services.yml file. So strange...

ndobromirov’s picture

Status: Needs work » Needs review

Came back here as we are in the process of upgrading RC3 to 2.1 :)

Rebased my local version and fixed the comments from #63 - public: false & README.
This should be the same as the patch in 64 but I am hoping to be somewhat different and tests to pass :D.
If not, we start from there...

ndobromirov’s picture

And the patch...

Status: Needs review » Needs work

The last submitted patch, 67: jsonapi-3018287-66.patch, failed testing. View results

ndobromirov’s picture

Status: Needs work » Needs review
FileSize
8.19 KB
385 bytes

Found it... Cache API is using the tagged services in container get calls, so it can not be private... Dropped one private flag on the service definition.

Here is the new patch with it.

Wim Leers’s picture

Status: Needs review » Postponed (maintainer needs more info)
Issue tags: +Needs issue summary update, +needs profiling

I'm very sorry for the silence here, @ndobromirov! 😞


Do we still need this with #3014232-62: [regression] ResourceTypeRepository is significantly slower in JSON:API 2, becomes noticeable when handling hundreds of interlinked resources committed? The issue summary is no longer accurate.

#38 + #39 + #40 say we need the chained cache (with first static caching and then persistent caching) to avoid the cost of repeated unserialization. Is that still true in HEAD? (Since December, we refactored away a lot of places where the resource type repository is accessed repeatedly.)

#61 shows this makes things slower?

ndobromirov’s picture

Status: Postponed (maintainer needs more info) » Needs work

Putting in needs work for the profiling and IS update. Details below...


#38 + #39 + #40 say we need the chained cache (with first static caching and then persistent caching) to avoid the cost of repeated unserialization. Is that still true in HEAD? (Since December, we refactored away a lot of places where the resource type repository is accessed repeatedly.)

Yes it is still relevant and we are still running with patch from #69.


#61 shows this makes things slower?

Yes but compared to #49.

Note that the 2 xhprof-s on the screen are comparing pre-#49 performance of the patch to the one that actually works correctly. And both are way faster than the no-patch scenario of 100+ ms.

The overhead likely comes from 2 places:
1. The fact we've moved from cached data in a private property (non cleanable mid-request) to an external service that can invalidate based on cache tags mid-request. Fixing #49.
2. The XHprof tool function calls overhead.

I think we can have another iteration to adopt the old inline property cache and get VERY similar performance to the original 6ms but we will likely loose the mid-request cache clear functionality or introduce some indirect complexity as discussed before (somewhere in the issues). And that was not liked by eoipso at the time...

Re-profiling with and without the patch should be ok to do.


The issue summary is no longer accurate.

I am pretty sure it is, as an overall idea :D...
Once I have the profiles will update it as well.

ndobromirov’s picture

Issue summary: View changes

Updated IS...

ndobromirov’s picture

Status: Needs work » Needs review
FileSize
129.38 KB
61.68 KB
26.4 KB
25.61 KB
26.15 KB
133.2 KB

All comments from previous reviews are resolved.
Here are the profiles for the patch with all the related changes:

All tests are done with disabled:
- Page cache. (disabled module)
- Dynamic page cache: cache.null
- Render cache: cache.null

No patch:

Patch (#69) cold cache:

Patch (#69) warm cache:

ndobromirov’s picture

Issue summary: View changes

Tweaks in IS.

ndobromirov’s picture

Tags clean-up...

This should still work and I am still using it in production for over 10 sites now :).
The supoprt code for jsonapi_extras is already committed.

I consider this very stable and a blocker for further improvements in #3039730: Include paths are resolved for every resource in a resource collection, instead of once per unique resource type.

e0ipso’s picture

  1. +++ b/jsonapi.services.yml
    @@ -144,10 +144,21 @@ services:
    +    class: Drupal\Core\Cache\BackendChain
    +    arguments: ['jsonapi_resource_types']
    +    calls:
    +      - ['appendBackend', ['@cache.jsonapi_memory']]
    +      - ['appendBackend', ['@cache.default']]
    

    Why not a regular cache.backend.chainedfast instead?

  2. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -70,6 +70,13 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +  protected $cacheTags = ['jsonapi_resource_types'];
    

    Why only these here? You add some additional below. Any reason to have some, but not all here?

  3. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -97,16 +104,32 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +        // Any module has the capability to expose new entities or alter
    +        // existing ones in multiple ways. So we merge 'config:core.extension'
    +        // tag to clear this data on any module install.
    +        'config:core.extension',
    

    If a module does that it should automatically clear the necessary tags by calling the save action on field / entity definitions. Right?

  4. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -97,16 +104,32 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +      $this->staticCache->set('jsonapi.resource_types', $resource_types, Cache::PERMANENT, $cache_tags);
    

    I'm not 100% sure we are getting the cache tags rights here. It's my own limited knowledge of the Entity API system's fault. Maybe we can get someone to confirm this looks right?

  5. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -143,20 +166,9 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +    $key = "$entity_type_id:$bundle";
    

    Let's keep the $cid as it was before. This way we only change the relevant bits.

  6. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -410,11 +419,17 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +    $cid = "jsonapi:is_reference:{$field_definition->getType()}";
    +    if (isset($this->cache[$cid])) {
    +      return $this->cache[$cid];
    +    }
    

    This seems like an unrelated change. However I'm +1. One thing, did you find this to be a performance bottleneck?

ndobromirov’s picture

Thanks for the reviews and questions.
1. The regular chained fast is APCu + DB. Overwriting the DB storage with something else is let's say tricky. Having the APCu means that we have state in the L1 cache layer. I am OK for testing that. We will spare the initial load from the L2 on every request and that should be a good thing in general :).
2. No particular reason, the ones below are just too generic and seemed OK to hard-code. The ones above are the ones that are kind of exposed through extension (already done by Extras).
3. Yes, as far as I've tested this.
4. Waiting for Wim or someone else on this one.
5. Ok :).
6. Yes. It turned out in the profiling output from XHProf. I do not recall the details now, but this was helping the cold cache scenario to compute the list faster.

ndobromirov’s picture

About 1 - if I recall correctly we needed memory cache as level one, because this is a VERY HOT piece of code. And the unserialize that APCu will do, will cause a regression already solved by using the memory cache used here. This one... #3014232: [regression] ResourceTypeRepository is significantly slower in JSON:API 2, becomes noticeable when handling hundreds of interlinked resources, As we can not use APCu, this is why we are not using the core's chained fast...

ndobromirov’s picture

After checking the APCu code in D8 core - there is no serialize / unserialize in there. This is a viable option to test.

e0ipso’s picture

Thanks for looping back in #79! I'm looking forward to get this in. I'd love to hear about what you find. Thanks for collaborating SO much on these issues. Your skills and expertise are deeply appreciated. Please try to not get discouraged by lack of progress.

gabesullice’s picture

Your skills and expertise are deeply appreciated. Please try to not get discouraged by lack of progress.

Amen! Thank you @ndobromirov! I suspect this will get some new life in the next few weeks. The 8.7 release and commit freeze + DrupalCon + a maintainer (me) being on paternity leave meant that the pace of the JSON:API issue queue was slowed down. I hope it will pick up again soon.

ndobromirov’s picture

@e0ipso & @gabesullice Thanks for the kind words!

There is some lack of time on my end due to pressing deadlines at the moment. Once I am a bit more free, I will get on this for sure (as it's saving servers).

ndobromirov’s picture

So, I've tested the APCu and it is counter-intuitive :(...
No patch as I do not see a meaningful reason to make one.

Using the chained fast with APCu was killing the performance by a HUGE margin (with XHProf running).

The main difference that I see is that the resource type repository was getting the ::wakeup call every time, where with the memory cache it was not... There were hidden overheads that were just part of the apcu_fetch (taking over 5 seconds for some 231 calls).

On top of that I had to increase the APCU_SCHM_SIZE to a lot more than it used to be (96MB -> 256MB) to make it actually do any cache hit... Before that it was falling back to the persistent cache all the time and it was even slower.

This will cause issues on the scalability side for multisites, as this APCU should be scaled based on the amount / count of multi-sites being installed.

Other thing - the memory usage went up like 6x+ times, further killing the scalability of the system. I have not tracked from where as I was just switching between the 2 cache implementations.

I've changed one of the existing service definitions with this
(Sample code taken from core's bootstrap cache bin definition and tweaked a little):

cache.jsonapi_resource_types:
    class: Drupal\Core\Cache\CacheBackendInterface
    tags: [{ name: cache.bin, default_backend: cache.backend.chainedfast }]
    factory: cache_factory:get
    arguments: [jsonapi]

Tests were executed against a particular article node with good amount of included relationships.
No page cache, no dynamic cache, no render cache. Tests were done on warm Drupal caches (no containers were being rebuild).

Chained fast with APCu (L1) (with the above definition)

Chained fast with Memory (L1) (as in the last patch)

From what I see, we should keep using the memory backed chained fast service.
I have no idea why, but fetching from APCu is WAY slower than the memory cache used in the patch, even if we need to reach to the persistent layer once.

ndobromirov’s picture

Project: JSON:API » Drupal core
Version: 8.x-2.x-dev » 8.8.x-dev
Component: Code » jsonapi.module
Issue tags: +DevDaysTransylvania
FileSize
7.53 KB

Here is a re-based patch for core. It's functionally the same as the last patch from #69.
The backwards compatibility layer is removed, as it is not needed with core 8.6+. No intrediff for that - just no changes in JsonapiServiceProvider.php
Comments from reviewers are resolved.

Wim Leers’s picture

#2819335: Resource (entity) normalization should use partial caching landed, let's move this forward again! This is the next big performance win.

  • I raised questions in my last comment (#70), and I see @ndobromirov answered them all 👍
  • The profiling done in #73 is super valuable and very convincing 👏
  • #75 indicates this is in production on 10 sites 🥳
  • @e0ipso did an excellent review in #76, which @ndobromirov addressed.
  • #76.1: to add to what @ndobromirov already wrote: APCu has extremely limited storage space. We should not add more to it, that'll slow Drupal overall down, since bootstrap-critical caches then won't have enough room in APCu anymore. This explains the counterintuitive results in #83.
  • #76.4: see below.
  1. Removed some unnecessary whitespace changes to keep the patch scope smaller.
  2. I agree with @e0ipso in #76.2 that it's weird to have some cache tags in a class property and others elsewhere, if all of them are the same always anyway. So moved all of them there.

    (I also think it makes sense to have this live at the class level rather than in code dealing with implementation details, because it ensures cache tags are not dynamically computed, which is something this further communicates.)

  3. +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
    @@ -101,16 +108,32 @@ public function all() {
    +        // Any module has the capability to expose new entities or alter
    +        // existing ones in multiple ways. So we merge 'config:core.extension'
    +        // tag to clear this data on any module install.
    +        'config:core.extension',
    

    I know I suggested this in #34, but this was at a time where entity_field_info and entity_types were still absent. It's safe to remove these now.

    It's easy to convince yourself of this: look at the DB row for these cache tags in the cachetags table, then install for example the Actions module, and observe both cache tags incrementing.

    Removed. ✅

  4. +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
    @@ -101,16 +108,32 @@ public function all() {
    +        // Clear the data on any field change in the system.
    +        'entity_field_info',
    +        // Clear the data on any entity type change in the system.
    +        'entity_types',
    

    These are used by \Drupal\Core\Entity\EntityDisplayRepository::getAllDisplayModesByEntityType() and \Drupal\Core\Entity\EntityFieldManager::getFieldDefinitions(), so 👍

    To be safe, we should add entity_bundles too though, which is used by \Drupal\Core\Entity\EntityTypeBundleInfo::getAllBundleInfo(). Done. ✅ Also expanded the test coverage for this.

  5. +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
    @@ -74,6 +74,13 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
       protected $cache = [];
    

    This was introduced by #3017239: Optimize ResourceTypeRepository::get(): don't loop over all possible resource types in every call but is now no longer necessary, thanks to ::all():
    A) returning a result faster (thanks to using caching itself)
    B) that result now has keys that allow a faster checking for a match

    Continuing on this observation:

    +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
    @@ -414,11 +423,17 @@ protected function getRelatableResourceTypesFromFieldDefinition(FieldDefinitionI
    +    $cid = "jsonapi:is_reference:{$field_definition->getType()}";
    +    if (isset($this->cache[$cid])) {
    +      return $this->cache[$cid];
    +    }
    

    This is now the only place where $this->cache is used. And this is one of the exceedingly rare cases where an unclearable static cache is actually okay. Because the result depends on a lot of loading of PHP objects and then checking if the final object in the chain is an instance of a particular class.

    So: it's fine to remove $this->cache. Done. ✅

  6. +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
    @@ -147,20 +170,9 @@ public function get($entity_type_id, $bundle) {
    -    return $this->cache[$cid];
    ...
    +    $resources = $this->all();
    +    return isset($resources[$cid]) ? $resources[$cid] : NULL;
    

    This change means it's switching from its own static cache to not having any caching and instead relying on ::all() itself being much faster (thanks to caching). 👍

    Continuing on this observation, let's look at the lines next to it:

    +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
    @@ -147,20 +170,9 @@ public function get($entity_type_id, $bundle) {
    +    $cid = "$entity_type_id:$bundle";
    
    @@ -395,11 +407,8 @@ protected function getRelatableResourceTypesFromFieldDefinition(FieldDefinitionI
    +      $cid = "$entity_type_id:$target_bundle";
    

    In both of these cases, this is not a CID but a key as returned by ResourceTypeRepository::all(). Because in both cases, making the call to ::all() return faster (thanks to caching) is just one aspect of the performance boost; the other aspect is removing the need to iterate over all returned resource types to find a match. Finding a match efficiently wasn't possible before, but it is now, thanks to this key.

    So renamed $cid to $key. ✅

  7. +++ b/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php
    @@ -13,12 +13,13 @@
    -class ResourceTypeRepositoryTest extends KernelTestBase {
    +class ResourceTypeRepositoryTest extends JsonapiKernelTestBase {
    

    Nit: If we're changing this, then we can also simplify $modules.

    Done. ✅

  8. +++ b/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php
    @@ -97,4 +98,13 @@ public function getProvider() {
    +    $this->assertCount(2, $this->resourceTypeRepository->get('node', 'article')->getRelatableResourceTypesByField('field_relationship'));
    

    👍 I didn't understand this at first, but it makes sense: this field_relationship field can have relationships to both any Node and since two bundles exist, that results in two relatable resource types: node--article and node--page.

  9. +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
    @@ -101,16 +108,32 @@ public function all() {
    +      $this->staticCache->set('jsonapi.resource_types', $resource_types, Cache::PERMANENT, $cache_tags);
    

    This is now no longer a simple static cache. When #2984886: Trigger route rebuild when new bundles/fields are added/removed introduced it, it was. But then #3014232: [regression] ResourceTypeRepository is significantly slower in JSON:API 2, becomes noticeable when handling hundreds of interlinked resources changed it to use MemoryCache (which is a pretty new thing in D8, see https://www.drupal.org/node/2973262). And this is then taking that further and changing it into something else entirely. Will do that in the next patch iteration to keep interdiffs easy to understand.

Wim Leers’s picture

This addresses #85.9.

Wim Leers’s picture

Remaining deprecation notices (5)

  3x: a:4:{s:11:"deprecation";s:148:"The $bin parameter is deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Omit the first parameter. See https://www.drupal.org/node/3061125";s:5:"class";s:67:"Drupal\Tests\jsonapi\Kernel\ResourceType\ResourceTypeRepositoryTest";s:6:"method";s:7:"testGet";s:15:"triggering_file";s:68:"/Users/wim.leers/Work/d8/core/lib/Drupal/Core/Cache/BackendChain.php";}
    3x in IDE_PHPUnit_Loader::init

  1x: a:4:{s:11:"deprecation";s:148:"The $bin parameter is deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Omit the first parameter. See https://www.drupal.org/node/3061125";s:5:"class";s:67:"Drupal\Tests\jsonapi\Kernel\ResourceType\ResourceTypeRepositoryTest";s:6:"method";s:7:"testAll";s:15:"triggering_file";s:68:"/Users/wim.leers/Work/d8/core/lib/Drupal/Core/Cache/BackendChain.php";}
    1x in IDE_PHPUnit_Loader::init

  1x: a:4:{s:11:"deprecation";s:148:"The $bin parameter is deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Omit the first parameter. See https://www.drupal.org/node/3061125";s:5:"class";s:67:"Drupal\Tests\jsonapi\Kernel\ResourceType\ResourceTypeRepositoryTest";s:6:"method";s:11:"testCaching";s:15:"triggering_file";s:68:"/Users/wim.leers/Work/d8/core/lib/Drupal/Core/Cache/BackendChain.php";}
    1x in IDE_PHPUnit_Loader::init

This is caused by the recent #3061117: Make optional and deprecate the BackendChain constructor parameter, easily fixed.

Wim Leers’s picture

Wim Leers’s picture

  1. While debugging this patch, I noticed that \Drupal\jsonapi\ParamConverter\ResourceTypeConverter::convert() is used on pretty much every JSON:API route, and that calls \Drupal\jsonapi\ResourceType\ResourceTypeRepository::getByTypeName(). Which happens to be the only method on the repository to not yet have been converted from iterating over all returned values until a match is found. Like #85.6 described, this is one aspect of the performance improvement.

    So let's make that also use a direct lookup rather than iterating.

  2. I think #85.6's s/cid/key/ was an improvement … but it made me realize something more:
    +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
    @@ -83,33 +91,36 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +            $entity_type->id() . ':' . $bundle => $this->createResourceType($entity_type, $bundle),
    
    @@ -148,19 +159,9 @@ public function get($entity_type_id, $bundle) {
    +    $key = "$entity_type_id:$bundle";
    
    @@ -395,11 +396,8 @@ protected function getRelatableResourceTypesFromFieldDefinition(FieldDefinitionI
    +      $key = "$entity_type_id:$target_bundle";
    

    This key is awfully reminiscent of something else … the resource type name. It uses -- as a separator instead of :.

    Furthermore, we already know for a fact that each ResourceType must have a unique name, per the JSON:API spec.

    So … let's use resource type names as the keys for ::all()?

    I think this makes overwhelmingly much sense 😂

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

My initial thought:

With #2819335 having landed, the before vs after performance picture will likely have changed. Would you be able to do the profiling in #73 one last time? 🙏

… but then I realized that actually, this is completely independent of #2819335!

That issue dealt with normalizations of resources (entities). This issue deals with bootstrapping the ResourceTypeRepository.

The profiling in #73 is therefore still accurate. Which means I think this is ready! Thanks for your patience and excellent work here, @ndobromirov! 🙏

Since all I did was renaming things, adding a few comments, removing some whitespace, adding a single test assertion — in short, nothing but trivial bits that did not materially change the patch but merely made it easier to understand, I think it's okay for me to RTBC.

The last submitted patch, 88: 3018287-85.patch, failed testing. View results

The last submitted patch, 88: 3018287-86.patch, failed testing. View results

ndobromirov’s picture

Status: Reviewed & tested by the community » Needs work

@Wim thanks for the thorough clean-up! :)
I also think it's ready to commit, but by moving the cache tags from the bottom to the top in the property. Now we need to change things in JSON:API Extras. #3019729: Invalidate JSON:API Extras' cache tags when needed

We should do any of the following ones:
- Undo the cache tags move to the property, introduced in #85.
- Redo the commit in #3019729: Invalidate JSON:API Extras' cache tags when needed to include the newly added tags as they are now in json:api.
- Tweak how the value is initialized in the descendant class in JSON:API Extras, so it will add only new unique ones to the list (merge them).

If we commit it like this it will conflict with Eoipso's comment in #58 in the thread here.
Personally I think it's best to have the last option implemented, as that is the best behavior in general and it will not degrade in strange ways in future.

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community

Now we need to change things in JSON:API Extras.

We shouldn't block core commits for mere convenience (because requiring trivial changes) of contrib modules that are not using official APIs. We're careful (and I was too), but it should never result in an un-RTBC unless it's making things completely impossible.

#3019729: Invalidate JSON:API Extras' cache tags when needed will continue to work fine as-is, it'll just need have more cache tags added to it. Besides, #3019729 was a premature commit anyway, because it assumed that a particular implementation would land in JSON:API.

ndobromirov’s picture

Ok, noted :).
No issues then for a commit: RTBC +1

e0ipso’s picture

I'm wondering if the running assumption is that the in-memory cache entries can be invalidated mid-request. If there is no use case for that, I think that a property cache may be a more acceptable solution for the in-memory caching. Can you provide more clarification to that end?

Wim Leers’s picture

Such static caches make unit tests nearly impossible. They also bake in the assumption all over the code base that it's okay to have global state because it's going to disappear in the next request anyway. Baking in this assumption all over the place is what Drupal core used to have, and it hugely complicated tests.

So it's IMHO less about a concrete use case than it is about the risks and consequences of baking in certain assumptions.

e0ipso’s picture

Such static caches make unit tests nearly impossible.

Wouldn't that be solved with a setter?

it's okay to have global state because it's going to disappear in the next request anyway.

I think that is indeed OK. I would go further and guess that the in-memory cache back end leverages that fact.

it hugely complicated tests.

Would it be an acceptable approach if a setter or a ::clearMemoryCache() method made those tests simple again?

ndobromirov’s picture

To add to the discussion about the internal properties - once you start using something like PHP:PM the static cache will become persistent one. So having the capability to invalidate it will be huge advantage.

Wim Leers’s picture

@e0ipso I was answering in the abstract.

  1. This cache backend-based approach is not being introduced here, #2984886: Trigger route rebuild when new bundles/fields are added/removed introduced it due to observed bugs.
  2. I think you're forgetting that the in-memory cache is also persisted?

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 89: 3018287-89.patch, failed testing. View results

e0ipso’s picture

@ndobromirov that's super interesting! Are you using PHP:PM with a modern version of Drupal?

ndobromirov’s picture

Failure in #101 seems unrelated...

@e0ipso - this is just theoretical :(. No time to make Drupal run on that, even though I would like to see / make it happen :)

ndobromirov’s picture

Status: Needs work » Needs review
ndobromirov’s picture

Status: Needs review » Reviewed & tested by the community

Push the bot to retest... RTBC as it was...

catch’s picture

Status: Reviewed & tested by the community » Fixed

Very nice.

Committed 22a2503 and pushed to 8.8.x. Thanks!

  • catch committed 22a2503 on 8.8.x
    Issue #3018287 by ndobromirov, Wim Leers, gabesullice, e0ipso:...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

gabesullice’s picture

Issue tags: +8.8.0 highlights
ndobromirov’s picture

Here is a re-roll of #89 against Drupal 8.7.11 for anyone that might need it.

ndobromirov’s picture

And a functioning patch now...
There seems to be a small API change in the back-end chain that was missing from the 8.8 diff thus the new patch.