Minor optimization to the AutoLoader
authorAdam Roses Wight <awight@wikimedia.org>
Sun, 27 Oct 2013 05:59:01 +0000 (22:59 -0700)
committerTim Starling <tstarling@wikimedia.org>
Mon, 28 Oct 2013 21:39:57 +0000 (21:39 +0000)
When MediaWiki autoloading fails, we should gracefully return false.
Instead, we have been calling strtolower roughly 1,000 times in the hope
of finding a case-insensitive match.

This patch preserves the legacy case-insensitivity, but improves its
performance by approximately 100x, by storing the case-insensitive class
lookups as a static variable.

There is a new global $wgAutoloadAttemptLowercase which will switch the
behavior if desired.  The default is to support case-insensitive loading.

Change-Id: Ifb12e05614a48390b730167e9d4ddcd8545db764

includes/AutoLoader.php
includes/DefaultSettings.php
tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php [new file with mode: 0644]
tests/phpunit/data/autoloader/TestAutoloadedClass.php [new file with mode: 0644]
tests/phpunit/data/autoloader/TestAutoloadedLocalClass.php [new file with mode: 0644]
tests/phpunit/data/autoloader/TestAutoloadedSerializedClass.php [new file with mode: 0644]
tests/phpunit/structure/AutoLoaderTest.php

index 0706fe3..dbba500 100644 (file)
@@ -1136,6 +1136,8 @@ $wgAutoloadLocalClasses = array(
 );
 
 class AutoLoader {
+       static $autoloadLocalClassesLower = null;
+
        /**
         * autoload - take a class name and attempt to load it
         *
@@ -1145,7 +1147,8 @@ class AutoLoader {
         * as well.
         */
        static function autoload( $className ) {
-               global $wgAutoloadClasses, $wgAutoloadLocalClasses;
+               global $wgAutoloadClasses, $wgAutoloadLocalClasses,
+                       $wgAutoloadAttemptLowercase;
 
                // Workaround for PHP bug <https://bugs.php.net/bug.php?id=49143> (5.3.2. is broken, it's
                // fixed in 5.3.6). Strip leading backslashes from class names. When namespaces are used,
@@ -1160,26 +1163,37 @@ class AutoLoader {
                        $filename = $wgAutoloadLocalClasses[$className];
                } elseif ( isset( $wgAutoloadClasses[$className] ) ) {
                        $filename = $wgAutoloadClasses[$className];
-               } else {
-                       # Try a different capitalisation
-                       # The case can sometimes be wrong when unserializing PHP 4 objects
+               } elseif ( $wgAutoloadAttemptLowercase ) {
+                       /*
+                        * Try a different capitalisation.
+                        *
+                        * PHP 4 objects are always serialized with the classname coerced to lowercase,
+                        * and we are plagued with several legacy uses created by MediaWiki < 1.5, see
+                        * https://wikitech.wikimedia.org/wiki/Text_storage_data
+                        */
                        $filename = false;
                        $lowerClass = strtolower( $className );
 
-                       foreach ( $wgAutoloadLocalClasses as $class2 => $file2 ) {
-                               if ( strtolower( $class2 ) == $lowerClass ) {
-                                       $filename = $file2;
-                               }
+                       if ( self::$autoloadLocalClassesLower === null ) {
+                               self::$autoloadLocalClassesLower = array_change_key_case( $wgAutoloadLocalClasses, CASE_LOWER );
                        }
 
-                       if ( !$filename ) {
+                       if ( isset( self::$autoloadLocalClassesLower[$lowerClass] ) ) {
                                if ( function_exists( 'wfDebug' ) ) {
-                                       wfDebug( "Class {$className} not found; skipped loading\n" );
+                                       wfDebug( "Class {$className} was loaded using incorrect case.\n" );
                                }
+                               $filename = self::$autoloadLocalClassesLower[$lowerClass];
+                       }
+               }
 
-                               # Give up
-                               return false;
+               if ( !$filename ) {
+                       if ( function_exists( 'wfDebug' ) ) {
+                               # FIXME: This is not very polite.  Assume we do not manage the class.
+                               wfDebug( "Class {$className} not found; skipped loading\n" );
                        }
+
+                       # Give up
+                       return false;
                }
 
                # Make an absolute path, this improves performance by avoiding some stat calls
index dbdd89e..bf2d2fd 100644 (file)
@@ -5953,6 +5953,13 @@ $wgSpecialPages = array();
  */
 $wgAutoloadClasses = array();
 
+/**
+ * Switch controlling legacy case-insensitive classloading.
+ * Do not disable if your wiki must support data created by PHP4, or by
+ * MediaWiki 1.4 or earlier.
+ */
+$wgAutoloadAttemptLowercase = true;
+
 /**
  * An array of extension types and inside that their names, versions, authors,
  * urls, descriptions and pointers to localized description msgs. Note that
diff --git a/tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php b/tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php
new file mode 100644 (file)
index 0000000..6dfce7a
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+
+class TestAutoloadedCamlClass {
+}
diff --git a/tests/phpunit/data/autoloader/TestAutoloadedClass.php b/tests/phpunit/data/autoloader/TestAutoloadedClass.php
new file mode 100644 (file)
index 0000000..9ceedf6
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+
+class TestAutoloadedClass {
+}
diff --git a/tests/phpunit/data/autoloader/TestAutoloadedLocalClass.php b/tests/phpunit/data/autoloader/TestAutoloadedLocalClass.php
new file mode 100644 (file)
index 0000000..1b397cd
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+
+class TestAutoloadedLocalClass {
+}
diff --git a/tests/phpunit/data/autoloader/TestAutoloadedSerializedClass.php b/tests/phpunit/data/autoloader/TestAutoloadedSerializedClass.php
new file mode 100644 (file)
index 0000000..80b9d58
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+
+class TestAutoloadedSerializedClass {
+}
index 205ea36..f5ff1d9 100644 (file)
@@ -1,6 +1,26 @@
 <?php
 class AutoLoaderTest extends MediaWikiTestCase {
 
+       protected function setUp() {
+               global $wgAutoloadLocalClasses, $wgAutoloadClasses;
+
+               parent::setUp();
+
+               // Fancy dance to trigger a rebuild of AutoLoader::$autoloadLocalClassesLower
+               $this->testLocalClasses = array(
+                       'TestAutoloadedLocalClass' => __DIR__ . '/../data/autoloader/TestAutoloadedLocalClass.php',
+                       'TestAutoloadedCamlClass' => __DIR__ . '/../data/autoloader/TestAutoloadedCamlClass.php',
+                       'TestAutoloadedSerializedClass' => __DIR__ . '/../data/autoloader/TestAutoloadedSerializedClass.php',
+               );
+               $this->setMwGlobals( 'wgAutoloadLocalClasses', $this->testLocalClasses + $wgAutoloadLocalClasses );
+               InstrumentedAutoLoader::resetAutoloadLocalClassesLower();
+
+               $this->testExtensionClasses = array(
+                       'TestAutoloadedClass' => __DIR__ . '/../data/autoloader/TestAutoloadedClass.php',
+               );
+               $this->setMwGlobals( 'wgAutoloadClasses', $this->testExtensionClasses + $wgAutoloadClasses );
+       }
+
        /**
         * Assert that there were no classes loaded that are not registered with the AutoLoader.
         *
@@ -53,4 +73,32 @@ class AutoLoaderTest extends MediaWikiTestCase {
                        'actual' => $actual,
                );
        }
+
+       function testCoreClass() {
+               $this->assertTrue( class_exists( 'TestAutoloadedLocalClass' ) );
+       }
+
+       function testExtensionClass() {
+               $this->assertTrue( class_exists( 'TestAutoloadedClass' ) );
+       }
+
+       function testWrongCaseClass() {
+               $this->assertTrue( class_exists( 'testautoLoadedcamlCLASS' ) );
+       }
+
+       function testWrongCaseSerializedClass() {
+               $dummyCereal = 'O:29:"testautoloadedserializedclass":0:{}';
+               $uncerealized = unserialize( $dummyCereal );
+               $this->assertFalse( $uncerealized instanceof __PHP_Incomplete_Class,
+                       "unserialize() can load classes case-insensitively.");
+       }
+}
+
+/**
+ * Cheater to poke protected members
+ */
+class InstrumentedAutoLoader extends AutoLoader {
+       static function resetAutoloadLocalClassesLower() {
+               self::$autoloadLocalClassesLower = null;
+       }
 }