Skip to content

Commit 1757a20

Browse files
committed
Functions: Memoize wp_normalize_path().
`wp_normalize_path()` is called thousands of times on a given request. This patch adds memoization via a function-local static variable. This reduces the call count to the underlying `wp_is_stream()` function, and measured in testing around a 66% cache hit rate. In testing, for a site making 4000 calls to `wp_normalize_path()`, this patch led to a reduction in runtime from 1.4 ms to 0.4 ms on the test computer. While small, this time occurs early in the hotpath of the loading WordPress. Developed in: WordPress#10770 Discussed in: https://core.trac.wordpress.org/ticket/64538 Props dmsnell, josephscott, mreishus, westonruter. Fixes #64538. git-svn-id: https://develop.svn.wordpress.org/trunk@61857 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 2b6cd61 commit 1757a20

2 files changed

Lines changed: 71 additions & 4 deletions

File tree

src/wp-includes/functions.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2179,12 +2179,21 @@ function path_join( $base, $path ) {
21792179
* @since 4.4.0 Ensures upper-case drive letters on Windows systems.
21802180
* @since 4.5.0 Allows for Windows network shares.
21812181
* @since 4.9.7 Allows for PHP file wrappers.
2182+
* @since 7.0.0 Uses a static cache to store normalized paths.
21822183
*
21832184
* @param string $path Path to normalize.
21842185
* @return string Normalized path.
21852186
*/
2186-
function wp_normalize_path( $path ) {
2187-
$wrapper = '';
2187+
function wp_normalize_path( $path ): string {
2188+
$path = (string) $path;
2189+
2190+
static $cache = array();
2191+
if ( isset( $cache[ $path ] ) ) {
2192+
return $cache[ $path ];
2193+
}
2194+
2195+
$original_path = $path;
2196+
$wrapper = '';
21882197

21892198
if ( wp_is_stream( $path ) ) {
21902199
list( $wrapper, $path ) = explode( '://', $path, 2 );
@@ -2196,14 +2205,15 @@ function wp_normalize_path( $path ) {
21962205
$path = str_replace( '\\', '/', $path );
21972206

21982207
// Replace multiple slashes down to a singular, allowing for network shares having two slashes.
2199-
$path = preg_replace( '|(?<=.)/+|', '/', $path );
2208+
$path = (string) preg_replace( '|(?<=.)/+|', '/', $path );
22002209

22012210
// Windows paths should uppercase the drive letter.
22022211
if ( ':' === substr( $path, 1, 1 ) ) {
22032212
$path = ucfirst( $path );
22042213
}
22052214

2206-
return $wrapper . $path;
2215+
$cache[ $original_path ] = $wrapper . $path;
2216+
return $cache[ $original_path ];
22072217
}
22082218

22092219
/**

tests/phpunit/tests/functions.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,66 @@ public function data_wp_normalize_path() {
222222
array( 'php://input', 'php://input' ),
223223
array( 'http://example.com//path.ext', 'http://example.com/path.ext' ),
224224
array( 'file://c:\\www\\path\\', 'file://C:/www/path/' ),
225+
226+
// Edge cases.
227+
array( '', '' ), // Empty string should return empty string.
228+
array( 123, '123' ), // Integer should be cast to string.
225229
);
226230
}
227231

232+
/**
233+
* Tests that wp_normalize_path() works with objects that have __toString().
234+
*
235+
* This is important because the function uses a static cache, and the input
236+
* must be cast to string before being used as an array key.
237+
*
238+
* @ticket 64538
239+
*/
240+
public function test_wp_normalize_path_with_stringable_object() {
241+
$file_info = new SplFileInfo( '/var/www/html\\test' );
242+
243+
$this->assertSame( '/var/www/html/test', wp_normalize_path( $file_info ) );
244+
}
245+
246+
/**
247+
* Tests that wp_normalize_path() returns consistent results on repeated calls.
248+
*
249+
* The function uses a static cache, so this verifies cache behavior.
250+
*
251+
* @ticket 64538
252+
*/
253+
public function test_wp_normalize_path_returns_consistent_results() {
254+
$path = 'C:\\www\\path\\';
255+
256+
$first_call = wp_normalize_path( $path );
257+
$second_call = wp_normalize_path( $path );
258+
$third_call = wp_normalize_path( $path );
259+
260+
$this->assertSame( $first_call, $second_call, 'Second call should return same result as first.' );
261+
$this->assertSame( $second_call, $third_call, 'Third call should return same result as second.' );
262+
$this->assertSame( 'C:/www/path/', $first_call, 'Normalized path should match expected value.' );
263+
}
264+
265+
/**
266+
* Tests that wp_normalize_path() static cache stores results.
267+
*
268+
* @ticket 64538
269+
*/
270+
public function test_wp_normalize_path_static_cache() {
271+
$path = '/var/www/cache-test\\subdir\\';
272+
$expected = '/var/www/cache-test/subdir/';
273+
274+
$result = wp_normalize_path( $path );
275+
$this->assertSame( $expected, $result );
276+
277+
$reflection = new ReflectionFunction( 'wp_normalize_path' );
278+
$static_vars = $reflection->getStaticVariables();
279+
280+
$this->assertArrayHasKey( 'cache', $static_vars, 'Static cache array should exist.' );
281+
$this->assertArrayHasKey( $path, $static_vars['cache'], 'Cache should contain the normalized path.' );
282+
$this->assertSame( $expected, $static_vars['cache'][ $path ], 'Cached value should match the expected normalized path.' );
283+
}
284+
228285
public function test_wp_unique_filename() {
229286

230287
$testdir = DIR_TESTDATA . '/images/';

0 commit comments

Comments
 (0)