From 4ef616921842a9e1939c5f178d336e44f09dd754 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 14 Nov 2023 12:01:50 -0500 Subject: [PATCH] [4.x] Nocache performance improvements (#8956) --- src/StaticCaching/NoCache/Session.php | 21 ++++++-- src/StaticCaching/NoCache/StringFragment.php | 16 +++--- src/StaticCaching/NoCache/Tags.php | 10 +++- src/StaticCaching/StaticCacheManager.php | 4 +- tests/StaticCaching/ManagerTest.php | 6 +++ tests/StaticCaching/NoCacheSessionTest.php | 22 +++++--- tests/StaticCaching/NocacheTagsTest.php | 54 ++++++++++++++++++++ 7 files changed, 113 insertions(+), 20 deletions(-) diff --git a/src/StaticCaching/NoCache/Session.php b/src/StaticCaching/NoCache/Session.php index d763ec9b31..f60f9c6b25 100644 --- a/src/StaticCaching/NoCache/Session.php +++ b/src/StaticCaching/NoCache/Session.php @@ -41,12 +41,12 @@ public function setUrl(string $url) */ public function regions(): Collection { - return $this->regions; + return $this->regions->mapWithKeys(fn ($key) => [$key => $this->region($key)]); } public function region(string $key): Region { - if ($region = $this->regions[$key] ?? null) { + if ($this->regions->contains($key) && ($region = Cache::get('nocache::region.'.$key))) { return $region; } @@ -57,14 +57,22 @@ public function pushRegion($contents, $context, $extension): StringRegion { $region = new StringRegion($this, trim($contents), $context, $extension); - return $this->regions[$region->key()] = $region; + $this->cacheRegion($region); + + $this->regions[] = $region->key(); + + return $region; } public function pushView($view, $context): ViewRegion { $region = new ViewRegion($this, $view, $context); - return $this->regions[$region->key()] = $region; + $this->cacheRegion($region); + + $this->regions[] = $region->key(); + + return $region; } public function cascade() @@ -115,4 +123,9 @@ private function restoreCascade() ->hydrate() ->toArray(); } + + private function cacheRegion(Region $region) + { + Cache::forever('nocache::region.'.$region->key(), $region); + } } diff --git a/src/StaticCaching/NoCache/StringFragment.php b/src/StaticCaching/NoCache/StringFragment.php index 6de7ca5780..4dc3025863 100644 --- a/src/StaticCaching/NoCache/StringFragment.php +++ b/src/StaticCaching/NoCache/StringFragment.php @@ -27,14 +27,18 @@ public function render(): string view()->addNamespace('nocache', $this->directory); File::makeDirectory($this->directory); - $this->createTemporaryView(); + $path = $this->createTemporaryView(); $this->data['__frontmatter'] = Arr::pull($this->data, 'view', []); - return view('nocache::'.$this->region, $this->data)->render(); + $rendered = view('nocache::'.$this->region, $this->data)->render(); + + File::delete($path); + + return $rendered; } - private function createTemporaryView() + private function createTemporaryView(): string { $path = vsprintf('%s/%s.%s', [ $this->directory, @@ -42,10 +46,10 @@ private function createTemporaryView() $this->extension, ]); - if (File::exists($path)) { - return; + if (! File::exists($path)) { + File::put($path, $this->contents); } - File::put($path, $this->contents); + return $path; } } diff --git a/src/StaticCaching/NoCache/Tags.php b/src/StaticCaching/NoCache/Tags.php index 0f174626e9..c26dc2b860 100644 --- a/src/StaticCaching/NoCache/Tags.php +++ b/src/StaticCaching/NoCache/Tags.php @@ -2,6 +2,8 @@ namespace Statamic\StaticCaching\NoCache; +use Statamic\Facades\Antlers; + class Tags extends \Statamic\Tags\Tags { public static $handle = 'nocache'; @@ -19,9 +21,15 @@ public function __construct(Session $nocache) public function index() { + if ($this->params->has('select')) { + $fields = $this->params->explode('select'); + } else { + $fields = Antlers::identifiers($this->content); + } + return $this ->nocache - ->pushRegion($this->content, $this->context->all(), 'antlers.html') + ->pushRegion($this->content, $this->context->only($fields)->all(), 'antlers.html') ->placeholder(); } } diff --git a/src/StaticCaching/StaticCacheManager.php b/src/StaticCaching/StaticCacheManager.php index 932adafb24..34bd5ae0c7 100644 --- a/src/StaticCaching/StaticCacheManager.php +++ b/src/StaticCaching/StaticCacheManager.php @@ -56,7 +56,9 @@ public function flush() $this->driver()->flush(); collect(Cache::get('nocache::urls', []))->each(function ($url) { - Cache::forget('nocache::session.'.md5($url)); + $session = Cache::get($sessionKey = 'nocache::session.'.md5($url)); + collect($session['regions'] ?? [])->each(fn ($region) => Cache::forget('nocache::region.'.$region)); + Cache::forget($sessionKey); }); Cache::forget('nocache::urls'); diff --git a/tests/StaticCaching/ManagerTest.php b/tests/StaticCaching/ManagerTest.php index db280f1997..be865df05e 100644 --- a/tests/StaticCaching/ManagerTest.php +++ b/tests/StaticCaching/ManagerTest.php @@ -22,6 +22,12 @@ public function it_flushes() StaticCache::extend('test', fn () => $mock); Cache::shouldReceive('get')->with('nocache::urls', [])->once()->andReturn(['/one', '/two']); + Cache::shouldReceive('get')->with('nocache::session.'.md5('/one'))->once()->andReturn(['regions' => ['r1', 'r2']]); + Cache::shouldReceive('get')->with('nocache::session.'.md5('/two'))->once()->andReturn(['regions' => ['r3', 'r4']]); + Cache::shouldReceive('forget')->with('nocache::region.r1')->once(); + Cache::shouldReceive('forget')->with('nocache::region.r2')->once(); + Cache::shouldReceive('forget')->with('nocache::region.r3')->once(); + Cache::shouldReceive('forget')->with('nocache::region.r4')->once(); Cache::shouldReceive('forget')->with('nocache::session.'.md5('/one'))->once(); Cache::shouldReceive('forget')->with('nocache::session.'.md5('/two'))->once(); Cache::shouldReceive('forget')->with('nocache::urls')->once(); diff --git a/tests/StaticCaching/NoCacheSessionTest.php b/tests/StaticCaching/NoCacheSessionTest.php index 753c569aa4..d5022fc16f 100644 --- a/tests/StaticCaching/NoCacheSessionTest.php +++ b/tests/StaticCaching/NoCacheSessionTest.php @@ -2,6 +2,7 @@ namespace Tests\StaticCaching; +use Carbon\Carbon; use Illuminate\Support\Facades\Cache; use Mockery; use Statamic\StaticCaching\NoCache\Session; @@ -75,6 +76,8 @@ public function it_gets_the_fragment_data() /** @test */ public function it_writes() { + Carbon::setTestNow('2014-02-15'); + // Testing that the cache key used is unique to the url. // The contents aren't really important. @@ -101,6 +104,11 @@ public function it_writes() ->with('nocache::urls', ['/', '/foo']) ->once(); + // When pushing regions, they will get written too... + Cache::shouldReceive('forever') + ->withArgs(fn ($arg) => str_starts_with($arg, 'nocache::region.')) + ->twice(); + tap(new Session('/'), function ($session) { $session->pushRegion('test', [], '.html'); })->write(); @@ -113,11 +121,10 @@ public function it_writes() /** @test */ public function it_restores_from_cache() { + Cache::forever('nocache::region.abc', $regionOne = Mockery::mock(StringRegion::class)); + Cache::forever('nocache::region.def', $regionTwo = Mockery::mock(StringRegion::class)); Cache::forever('nocache::session.'.md5('http://localhost/test'), [ - 'regions' => [ - $regionOne = Mockery::mock(StringRegion::class), - $regionTwo = Mockery::mock(StringRegion::class), - ], + 'regions' => ['abc', 'def'], ]); $this->createPage('/test', [ @@ -130,7 +137,7 @@ public function it_restores_from_cache() $session->restore(); - $this->assertEquals([$regionOne, $regionTwo], $session->regions()->all()); + $this->assertEquals(['abc' => $regionOne, 'def' => $regionTwo], $session->regions()->all()); $this->assertNotEquals([], $cascade = $session->cascade()); $this->assertEquals('/test', $cascade['url']); $this->assertEquals('Test page', $cascade['title']); @@ -202,10 +209,9 @@ public function it_restores_session_if_theres_a_nocache_placeholder_in_the_respo $this->viewShouldReturnRendered('default', 'Hello NOCACHE_PLACEHOLDER'); $this->createPage('test'); + Cache::forever('nocache::region.abc', $region = Mockery::mock(StringRegion::class)); Cache::put('nocache::session.'.md5('http://localhost/test'), [ - 'regions' => [ - 'abc' => $region = Mockery::mock(StringRegion::class), - ], + 'regions' => ['abc'], ]); $region->shouldReceive('render')->andReturn('world'); diff --git a/tests/StaticCaching/NocacheTagsTest.php b/tests/StaticCaching/NocacheTagsTest.php index 0272940d0f..f33f0314e8 100644 --- a/tests/StaticCaching/NocacheTagsTest.php +++ b/tests/StaticCaching/NocacheTagsTest.php @@ -2,7 +2,10 @@ namespace Tests\StaticCaching; +use Mockery; +use Statamic\Facades\Parse; use Statamic\StaticCaching\NoCache\Session; +use Statamic\StaticCaching\NoCache\StringRegion; use Tests\FakesContent; use Tests\FakesViews; use Tests\PreventSavingStacheItemsToDisk; @@ -99,4 +102,55 @@ public function it_can_keep_nested_nocache_tags_dynamic_inside_cache_tags() ->assertOk() ->assertSeeInOrder(['Updated', 'Updated', 'Existing', 'Updated']); } + + /** @test */ + public function it_only_adds_appropriate_fields_of_context_to_session() + { + // We will not add `baz` to the session because it is not used in the template. + // We will not add `nope` to the session because it is not in the context. + $expectedFields = ['foo', 'bar']; + $template = '{{ nocache }}{{ foo }}{{ bar }}{{ nope }}{{ /nocache }}'; + $context = [ + 'foo' => 'alfa', + 'bar' => 'bravo', + 'baz' => 'charlie', + ]; + + $region = Mockery::mock(StringRegion::class)->shouldReceive('placeholder')->andReturn('the placeholder')->getMock(); + + $this->mock(Session::class, fn ($mock) => $mock + ->shouldReceive('pushRegion') + ->withArgs(fn ($arg1, $arg2, $arg3) => array_keys($arg2) === $expectedFields) + ->once()->andReturn($region)); + + $this->assertEquals('the placeholder', $this->tag($template, $context)); + } + + /** @test */ + public function it_only_adds_explicitly_defined_fields_of_context_to_session() + { + // We will not add `bar` to the session because it is not explicitly defined. + // We will not add `nope` to the session because it is not in the context. + $expectedFields = ['foo', 'baz']; + $template = '{{ nocache select="foo|baz|nope" }}{{ foo }}{{ bar }}{{ nope }}{{ /nocache }}'; + $context = [ + 'foo' => 'alfa', + 'bar' => 'bravo', + 'baz' => 'charlie', + ]; + + $region = Mockery::mock(StringRegion::class)->shouldReceive('placeholder')->andReturn('the placeholder')->getMock(); + + $this->mock(Session::class, fn ($mock) => $mock + ->shouldReceive('pushRegion') + ->withArgs(fn ($arg1, $arg2, $arg3) => array_keys($arg2) === $expectedFields) + ->once()->andReturn($region)); + + $this->assertEquals('the placeholder', $this->tag($template, $context)); + } + + private function tag($tag, $data = []) + { + return (string) Parse::template($tag, $data); + } }