Skip to content

Commit ed79205

Browse files
Improve planning performances (#232)
* Store organization/planning in cache based on request filters hash * Invalidate cache older than lastUpdate planning * Fix review * Remove useless author tag * Fix review
1 parent d388a5f commit ed79205

File tree

12 files changed

+255
-20
lines changed

12 files changed

+255
-20
lines changed

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"symfony/validator": "5.*",
3030
"symfony/webpack-encore-bundle": "^1.7",
3131
"symfony/yaml": "5.*",
32+
"twig/cache-extension": "^1.4",
3233
"twig/intl-extra": "^3.0"
3334
},
3435
"require-dev": {

composer.lock

+72-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/packages/cache.yaml

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
services:
2+
cache.adapter.planning:
3+
class: App\Twig\Cache\PlanningFilesystemAdapter
4+
parent: cache.adapter.filesystem
5+
16
framework:
27
cache:
38
# Unique name of your app: used to compute stable namespaces for cache keys.
@@ -15,5 +20,6 @@ framework:
1520
app: cache.adapter.apcu
1621

1722
# Namespaced pools use the above "app" backend by default
18-
#pools:
19-
#my.dedicated.cache: null
23+
pools:
24+
cache.twig:
25+
adapter: cache.adapter.planning

config/services.yaml

+14
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,17 @@ services:
6161
- !service { class: PDO, factory: ['@database_connection', 'getWrappedConnection'] }
6262
# If you get transaction issues (e.g. after login) uncomment the line below
6363
- { lock_mode: 1 }
64+
65+
# twig/cache-extension
66+
Twig\CacheExtension\CacheProvider\PsrCacheAdapter:
67+
arguments:
68+
$cache: '@cache.twig'
69+
Twig\CacheExtension\CacheStrategy\GenerationalCacheStrategy:
70+
arguments:
71+
$cache: '@Twig\CacheExtension\CacheProvider\PsrCacheAdapter'
72+
$keyGenerator: '@App\Twig\Cache\RequestGenerator'
73+
$lifetime: 86400 # 1 day
74+
Twig\CacheExtension\CacheStrategyInterface: '@Twig\CacheExtension\CacheStrategy\GenerationalCacheStrategy'
75+
Twig\CacheExtension\Extension:
76+
arguments:
77+
$cacheStrategy: '@Twig\CacheExtension\CacheStrategy\GenerationalCacheStrategy'

src/Controller/Organization/Planning/PlanningController.php

+22-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@
99
use App\Domain\PlanningDomain;
1010
use App\Domain\SkillSetDomain;
1111
use App\Entity\CommissionableAsset;
12+
use Psr\Cache\CacheItemPoolInterface;
1213
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
14+
use Symfony\Component\Cache\CacheItem;
1315
use Symfony\Component\HttpFoundation\Request;
1416
use Symfony\Component\HttpFoundation\Response;
1517
use Symfony\Component\Routing\Annotation\Route;
18+
use Twig\CacheExtension\CacheStrategyInterface;
1619

1720
/**
1821
* @Route("/", name="planning", methods={"GET"})
@@ -21,13 +24,19 @@ class PlanningController extends AbstractController
2124
{
2225
private SkillSetDomain $skillSetDomain;
2326
private PlanningDomain $planningDomain;
27+
private CacheStrategyInterface $cacheStrategy;
28+
private CacheItemPoolInterface $cacheTwig;
2429

2530
public function __construct(
2631
SkillSetDomain $skillSetDomain,
27-
PlanningDomain $planningDomain
32+
PlanningDomain $planningDomain,
33+
CacheStrategyInterface $cacheStrategy,
34+
CacheItemPoolInterface $cacheTwig
2835
) {
2936
$this->skillSetDomain = $skillSetDomain;
3037
$this->planningDomain = $planningDomain;
38+
$this->cacheStrategy = $cacheStrategy;
39+
$this->cacheTwig = $cacheTwig;
3140
}
3241

3342
public function __invoke(Request $request): Response
@@ -46,12 +55,22 @@ public function __invoke(Request $request): Response
4655
$filters['to']
4756
);
4857

49-
$availabilities = $this->planningDomain->generateAvailabilities($filters, $periodCalculator->getPeriod());
58+
$lastUpdate = $this->planningDomain->generateLastUpdateAndCount($filters)['lastUpdate'];
59+
$cacheKey = $this->cacheStrategy->generateKey('organization_planning', $filters);
60+
/** @var CacheItem $item */
61+
$item = $this->cacheTwig->getItem($cacheKey);
62+
if ($item->isHit()
63+
&& isset($item->getMetadata()[CacheItem::METADATA_CTIME])
64+
&& $item->getMetadata()[CacheItem::METADATA_CTIME] < ceil($lastUpdate / 100)
65+
) {
66+
// New availabilities in planning: invalidate planning cache
67+
$this->cacheTwig->deleteItem($cacheKey);
68+
}
5069

5170
return $this->render('organization/planning/planning.html.twig', [
71+
'filters' => $filters,
5272
'form' => $form->createView(),
5373
'periodCalculator' => $periodCalculator,
54-
'availabilities' => $availabilities,
5574
'assetsTypes' => CommissionableAsset::TYPES,
5675
'usersSkills' => $this->skillSetDomain->getSkillSet(),
5776
'importantSkills' => $this->skillSetDomain->getImportantSkills(),

src/Domain/PlanningDomain.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public function generateLastUpdateAndCount(array $filters): array
100100
$users = $filters['hideUsers'] ?? false ? [] : $this->userRepository->findByFilters($filters, true);
101101
$assets = $filters['hideAssets'] ?? false ? [] : $this->assetRepository->findByFilters($filters, true);
102102

103-
// TODO Handle deleted availabities
103+
// TODO Handle deleted availabilities
104104

105105
$availabilitiesCount = 0;
106106
$userLastUpdate = 0;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Twig\Cache;
6+
7+
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
8+
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
9+
10+
final class PlanningFilesystemAdapter extends FilesystemAdapter
11+
{
12+
private string $cacheDirectory;
13+
14+
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null)
15+
{
16+
parent::__construct($namespace, $defaultLifetime, $directory, $marshaller);
17+
18+
$this->cacheDirectory = $directory.\DIRECTORY_SEPARATOR.$namespace.\DIRECTORY_SEPARATOR;
19+
}
20+
21+
/**
22+
* {@inheritdoc}
23+
*/
24+
protected function doFetch(array $ids)
25+
{
26+
/** @var array $values */
27+
$values = parent::doFetch($ids);
28+
foreach ($values as $id => $value) {
29+
$file = $this->getFile($id);
30+
if (!$handle = @fopen($file, 'rb')) {
31+
continue;
32+
}
33+
$values[$id] = [
34+
"\x9D".pack('VN', (int) (0.1 + (int) fgets($handle) - 1527506807), ceil(filectime($file) / 100))."\x5F" => $value,
35+
];
36+
fclose($handle);
37+
}
38+
39+
return $values;
40+
}
41+
42+
private function getFile(string $id): string
43+
{
44+
// Use MD5 to favor speed over security, which is not an issue here
45+
$hash = str_replace('/', '-', base64_encode(hash('md5', static::class.$id, true)));
46+
$dir = $this->cacheDirectory.strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR);
47+
48+
return $dir.substr($hash, 2, 20);
49+
}
50+
}

src/Twig/Cache/RequestGenerator.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Twig\Cache;
6+
7+
use Twig\CacheExtension\CacheStrategy\KeyGeneratorInterface;
8+
9+
final class RequestGenerator implements KeyGeneratorInterface
10+
{
11+
/**
12+
* {@inheritdoc}
13+
*/
14+
public function generateKey($filters): string
15+
{
16+
return sha1(serialize($filters));
17+
}
18+
}
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Twig\Extension;
6+
7+
use App\Domain\DatePeriodCalculator;
8+
use App\Domain\PlanningDomain;
9+
use Twig\Extension\AbstractExtension;
10+
use Twig\TwigFunction;
11+
12+
final class PlanningExtension extends AbstractExtension
13+
{
14+
private PlanningDomain $planningDomain;
15+
16+
public function __construct(PlanningDomain $planningDomain)
17+
{
18+
$this->planningDomain = $planningDomain;
19+
}
20+
21+
/**
22+
* {@inheritdoc}
23+
*/
24+
public function getFunctions(): array
25+
{
26+
return [
27+
new TwigFunction('getAvailabilities', [$this, 'getAvailabilities']),
28+
];
29+
}
30+
31+
public function getAvailabilities(DatePeriodCalculator $periodCalculator, array $filters): array
32+
{
33+
return $this->planningDomain->generateAvailabilities($filters, $periodCalculator->getPeriod());
34+
}
35+
}

symfony.lock

+3
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,9 @@
631631
"theseer/tokenizer": {
632632
"version": "1.1.3"
633633
},
634+
"twig/cache-extension": {
635+
"version": "1.4.0"
636+
},
634637
"twig/extra-bundle": {
635638
"version": "v3.0.3"
636639
},

0 commit comments

Comments
 (0)