Improve performance of SessionHandler::getSpiderID()
authorTim Düsterhus <duesterhus@woltlab.com>
Tue, 1 Mar 2022 11:50:41 +0000 (12:50 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Tue, 1 Mar 2022 12:02:05 +0000 (13:02 +0100)
99f28057e7aeb29ed6728917174fd8fc6b7bb1a1 already optimized this to avoid the
need of calling ->getSpiderID() for logged-in users, but guest sessions still
call ->getSpiderID() on every request to look up the legacy session.

This commit massively improves the performance of ->getSpiderID() for all
cases, but especially for requests where no spider can be matched. The latter
previously required a full O(n) search across the spider list and thus was the
worst case situation. This worst case situation likely happened for the vast
majority of guest requests. But even cases where a spider can be matched will
benefit from this.

The improvements are achieved by two things:

1. The size of the cache that needs to be read and unserialized is reduced from
87k to 17k.
2. Instead of searching linearly through the list of spiders, needing to
implicitly call ->__get() twice for each, the matching is performed by an
optimized regular expression that effectively implements a prefix tree. If this
regular expression matches, then the spiderID will be efficiently looked up in
an array that is keyed by the matched string.

Numbers for 10,000 calls to ->getSpiderID() on my computer running PHP 8.1:

- Google Bot: From 0.44s down to 0.14s.
- Firefox 98: From 1.05s down to 0.07s.

wcfsetup/install/files/lib/system/cache/builder/SpiderCacheBuilder.class.php
wcfsetup/install/files/lib/system/session/SessionHandler.class.php

index 342218564d522fd1adab49e956949a88d5ed4828..7e94ca3d35f3a904aaec97ccc6cd718e92176a46 100644 (file)
@@ -23,6 +23,38 @@ class SpiderCacheBuilder extends AbstractCacheBuilder
         $spiderList->sqlOrderBy = "spider.spiderID ASC";
         $spiderList->readObjects();
 
+        if (isset($parameters['fastLookup'])) {
+            $firstCharacter = [];
+            $mapping = [];
+            foreach ($spiderList as $spider) {
+                if (!isset($firstCharacter[$spider->spiderIdentifier[0]])) {
+                    $firstCharacter[$spider->spiderIdentifier[0]] = [];
+                }
+                $firstCharacter[$spider->spiderIdentifier[0]][] = \substr($spider->spiderIdentifier, 1);
+
+                $mapping[$spider->spiderIdentifier] = $spider->spiderID;
+            }
+
+            $regex = '';
+            foreach ($firstCharacter as $char => $spiders) {
+                if ($regex !== '') {
+                    $regex .= '|';
+                }
+                $regex .= \sprintf(
+                    '(?:%s(?:%s))',
+                    \preg_quote($char, '/'),
+                    \implode('|', \array_map(static function ($identifier) {
+                        return \preg_quote($identifier, '/');
+                    }, $spiders))
+                );
+            }
+
+            return [
+                'regex' => "/{$regex}/",
+                'mapping' => $mapping,
+            ];
+        }
+
         return $spiderList->getObjects();
     }
 }
index 21dd6ff7a37af90d679c585ddadc33bfd3b98947..d3f789596eace0bacac91e913184c6306d3bd9e0 100644 (file)
@@ -1410,16 +1410,14 @@ final class SessionHandler extends SingletonFactory
      */
     protected function getSpiderID(string $userAgent): ?int
     {
-        $spiderList = SpiderCacheBuilder::getInstance()->getData();
+        $data = SpiderCacheBuilder::getInstance()->getData(['fastLookup' => true]);
         $userAgent = \strtolower($userAgent);
 
-        foreach ($spiderList as $spider) {
-            if (\strpos($userAgent, $spider->spiderIdentifier) !== false) {
-                return \intval($spider->spiderID);
-            }
+        if (!\preg_match($data['regex'], $userAgent, $matches)) {
+            return null;
         }
 
-        return null;
+        return $data['mapping'][$matches[0]];
     }
 
     /**