From cab8ccb0b1fbc1e726516ca08af8bfc383cfe453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Andr=C3=A9=20dos=20Santos=20Lopes?= Date: Wed, 25 Feb 2026 14:15:51 +0100 Subject: [PATCH] Optimize Symfony http.route caching with path map approach Replace per-route caching with a single cached map of all route paths. This significantly improves performance and reduces memory usage. Key improvements: - Cache entire route map under single key '_datadog.symfony.route_paths' - Store simple array: ['route_name' => '/path', ...] - Smart invalidation based on compiled routes file mtime - Cache indefinitely until Symfony recompiles routes or cache is invalidated - Gracefully disable if cache.app unavailable Cache invalidation logic: - Checks {cache_dir}/url_generating_routes.php modification time - Compares against cached timestamp (created at cache build time) - Rebuilds cache only when compiled routes file is newer --- .../Symfony/SymfonyIntegration.php | 100 +++++++++++++++--- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index 7fe3095201..20f2462d23 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -422,43 +422,109 @@ static function() { ); if (\dd_trace_env_config('DD_TRACE_SYMFONY_HTTP_ROUTE')) { + /** + * Resolves the http.route tag for a given route name by looking up + * the route path in a cached map of all routes. + * + * Caching strategy: + * - Caches the entire route path map under a single key: '_datadog.symfony.route_paths' + * - Stores: ['mtime' => timestamp, 'paths' => ['route_name' => '/path', ...]] + * - Invalidates cache when Symfony's compiled routes file is newer than cached mtime + * - Falls back gracefully if cache.app is unavailable (no http.route tag) + */ $handle_http_route = static function($route_name, $request, $rootSpan) { if (self::$kernel === null) { return; } + /** @var ContainerInterface $container */ $container = self::$kernel->getContainer(); + try { $cache = $container->get('cache.app'); } catch (\Exception $e) { return; } - /** @var \Symfony\Bundle\FrameworkBundle\Routing\Router $router */ - $router = $container->get('router'); if (!\method_exists($cache, 'getItem')) { return; } - $itemName = "_datadog.route.path.$route_name"; - $locale = $request->get('_locale'); - if ($locale !== null) { - $itemName .= ".$locale"; + + /** @var \Symfony\Bundle\FrameworkBundle\Routing\Router $router */ + try { + $router = $container->get('router'); + } catch (\Exception $e) { + return; } - $item = $cache->getItem($itemName); - if ($item->isHit()) { - $route = $item->get(); - } else { + + // Get the compiled routes file mtime for cache invalidation + $compiledRoutesMtime = null; + $cacheDir = \method_exists($router, 'getOption') ? $router->getOption('cache_dir') : null; + if ($cacheDir !== null) { + $compiledRoutesFile = $cacheDir . '/url_generating_routes.php'; + if (\file_exists($compiledRoutesFile)) { + $compiledRoutesMtime = @\filemtime($compiledRoutesFile); + } + } + + $cacheKey = '_datadog.symfony.route_paths'; + /** @var ItemInterface $item */ + $item = $cache->getItem($cacheKey); + $cachedData = $item->isHit() ? $item->get() : null; + + $routePathMap = null; + $needsRebuild = true; + + if (\is_array($cachedData) && isset($cachedData['paths']) && \is_array($cachedData['paths'])) { + // Check if cache is still valid + if ($compiledRoutesMtime === null) { + // No compiled file to check against - cache is valid + $needsRebuild = false; + $routePathMap = $cachedData['paths']; + } elseif (isset($cachedData['mtime']) && $cachedData['mtime'] >= $compiledRoutesMtime) { + // Cached data is newer than or equal to compiled routes - cache is valid + $needsRebuild = false; + $routePathMap = $cachedData['paths']; + } + // Otherwise: compiled routes file is newer, rebuild cache + } + + if ($needsRebuild) { + $startTime = \function_exists('hrtime') ? \hrtime(true) : null; + + $routePathMap = []; $routeCollection = $router->getRouteCollection(); - $route = $routeCollection->get($route_name); - if ($route == null && $locale !== null) { - $route = $routeCollection->get($route_name . '.' . $locale); + foreach ($routeCollection->all() as $name => $route) { + $routePathMap[$name] = $route->getPath(); + } + + if ($startTime !== null) { + $durationNanoseconds = \hrtime(true) - $startTime; + $durationMicroseconds = (int)($durationNanoseconds / 1000); + $rootSpan->metrics['_dd.symfony.route.map_build_duration_us'] = $durationMicroseconds; } - $item->set($route); - $item->expiresAfter(3600); + + $item->set([ + 'mtime' => \time(), + 'paths' => $routePathMap, + ]); $cache->save($item); } - if (isset($route)) { - $rootSpan->meta[Tag::HTTP_ROUTE] = $route->getPath(); + + // Look up the route path + $path = null; + if (isset($routePathMap[$route_name])) { + $path = $routePathMap[$route_name]; + } else { + // Try with locale suffix (Symfony i18n routing convention) + $locale = $request->get('_locale'); + if ($locale !== null && isset($routePathMap[$route_name . '.' . $locale])) { + $path = $routePathMap[$route_name . '.' . $locale]; + } + } + + if ($path !== null) { + $rootSpan->meta[Tag::HTTP_ROUTE] = $path; } };