Skip to content

Commit 9bed5dc

Browse files
isaacsry
authored andcommitted
Support caching for realpath, use in module load
This adds support for a cache object to be passed to the fs.realpath and fs.realpathSync functions. The Module loader keeps an object around which caches the resulting realpaths that it looks up in the process of loading modules. This means that (at least as a result of loading modules) the same files and folders are never lstat()ed more than once. To reset the cache, set require("module")._realpathCache to an empty object. To disable the caching behavior, set it to null.
1 parent 9de5043 commit 9bed5dc

File tree

3 files changed

+109
-24
lines changed

3 files changed

+109
-24
lines changed

lib/fs.js

+76-22
Original file line numberDiff line numberDiff line change
@@ -514,15 +514,27 @@ if (isWindows) {
514514
// windows version
515515
fs.realpathSync = function realpathSync(p) {
516516
var p = path.resolve(p);
517+
if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
518+
return cache[p];
519+
}
517520
fs.statSync(p);
521+
if (cache) cache[p] = p;
518522
return p;
519523
};
520524

521525
// windows version
522-
fs.realpath = function(p, cb) {
526+
fs.realpath = function(p, cache, cb) {
527+
if (typeof cb !== 'function') {
528+
cb = cache;
529+
cache = null;
530+
}
523531
var p = path.resolve(p);
532+
if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
533+
return cb(null, cache[p]);
534+
}
524535
fs.stat(p, function(err) {
525536
if (err) cb(err);
537+
if (cache) cache[p] = p;
526538
cb(null, p);
527539
});
528540
};
@@ -535,11 +547,16 @@ if (isWindows) {
535547
var nextPartRe = /(.*?)(?:[\/]+|$)/g;
536548

537549
// posix version
538-
fs.realpathSync = function realpathSync(p) {
550+
fs.realpathSync = function realpathSync(p, cache) {
539551
// make p is absolute
540552
p = path.resolve(p);
541553

542-
var seenLinks = {},
554+
if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
555+
return cache[p];
556+
}
557+
558+
var original = p,
559+
seenLinks = {},
543560
knownHard = {};
544561

545562
// current character position in p
@@ -564,38 +581,61 @@ if (isWindows) {
564581
pos = nextPartRe.lastIndex;
565582

566583
// continue if not a symlink, or if root
567-
if (!base || knownHard[base]) {
568-
continue;
569-
}
570-
var stat = fs.lstatSync(base);
571-
if (!stat.isSymbolicLink()) {
572-
knownHard[base] = true;
584+
if (!base || knownHard[base] || (cache && cache[base] === base)) {
573585
continue;
574586
}
575587

576-
// read the link if it wasn't read before
577-
var id = stat.dev.toString(32) + ':' + stat.ino.toString(32);
578-
if (!seenLinks[id]) {
579-
fs.statSync(base);
580-
seenLinks[id] = fs.readlinkSync(base);
588+
var resolvedLink;
589+
if (cache && Object.prototype.hasOwnProperty.call(cache, base)) {
590+
// some known symbolic link. no need to stat again.
591+
resolvedLink = cache[base];
592+
} else {
593+
var stat = fs.lstatSync(base);
594+
if (!stat.isSymbolicLink()) {
595+
knownHard[base] = true;
596+
if (cache) cache[base] = base;
597+
continue;
598+
}
599+
600+
// read the link if it wasn't read before
601+
var id = stat.dev.toString(32) + ':' + stat.ino.toString(32);
602+
if (!seenLinks[id]) {
603+
fs.statSync(base);
604+
seenLinks[id] = fs.readlinkSync(base);
605+
resolvedLink = path.resolve(previous, seenLinks[id]);
606+
// track this, if given a cache.
607+
if (cache) cache[base] = resolvedLink;
608+
}
581609
}
582610

583611
// resolve the link, then start over
584-
p = path.resolve(previous, seenLinks[id], p.slice(pos));
612+
p = path.resolve(resolvedLink, p.slice(pos));
585613
pos = 0;
586614
previous = base = current = '';
587615
}
588616

617+
if (cache) cache[original] = p;
618+
589619
return p;
590620
};
591621

592622

593623
// posix version
594-
fs.realpath = function realpath(p, cb) {
624+
fs.realpath = function realpath(p, cache, cb) {
625+
if (typeof cb !== 'function') {
626+
cb = cache;
627+
cache = null;
628+
}
629+
595630
// make p is absolute
596631
p = path.resolve(p);
597632

598-
var seenLinks = {},
633+
if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
634+
return cb(null, cache[p]);
635+
}
636+
637+
var original = p,
638+
seenLinks = {},
599639
knownHard = {};
600640

601641
// current character position in p
@@ -613,6 +653,7 @@ if (isWindows) {
613653
function LOOP() {
614654
// stop if scanned past end of path
615655
if (pos >= p.length) {
656+
if (cache) cache[original] = p;
616657
return cb(null, p);
617658
}
618659

@@ -624,11 +665,16 @@ if (isWindows) {
624665
base = previous + result[1];
625666
pos = nextPartRe.lastIndex;
626667

627-
// continue if known to be hard or if root
628-
if (!base || knownHard[base]) {
668+
// continue if known to be hard or if root or in cache already.
669+
if (!base || knownHard[base] || (cache && cache[base] === base)) {
629670
return process.nextTick(LOOP);
630671
}
631672

673+
if (cache && Object.prototype.hasOwnProperty.call(cache, base)) {
674+
// known symbolic link. no need to stat again.
675+
return gotResolvedLink(cache[base]);
676+
}
677+
632678
return fs.lstat(base, gotStat);
633679
}
634680

@@ -638,14 +684,15 @@ if (isWindows) {
638684
// if not a symlink, skip to the next path part
639685
if (!stat.isSymbolicLink()) {
640686
knownHard[base] = true;
687+
if (cache) cache[base] = base;
641688
return process.nextTick(LOOP);
642689
}
643690

644691
// stat & read the link if not read before
645692
// call gotTarget as soon as the link target is known
646693
var id = stat.dev.toString(32) + ':' + stat.ino.toString(32);
647694
if (seenLinks[id]) {
648-
return gotTarget(null, seenLinks[id]);
695+
return gotTarget(null, seenLinks[id], base);
649696
}
650697
fs.stat(base, function(err) {
651698
if (err) return cb(err);
@@ -656,11 +703,18 @@ if (isWindows) {
656703
});
657704
}
658705

659-
function gotTarget(err, target) {
706+
function gotTarget(err, target, base) {
660707
if (err) return cb(err);
661708

709+
var resolvedLink = path.resolve(previous, target);
710+
if (cache) cache[base] = resolvedLink;
711+
gotResolvedLink(resolvedLink);
712+
}
713+
714+
function gotResolvedLink(resolvedLink) {
715+
662716
// resolve the link, then start over
663-
p = path.resolve(previous, target, p.slice(pos));
717+
p = path.resolve(resolvedLink, p.slice(pos));
664718
pos = 0;
665719
previous = base = current = '';
666720

lib/module.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,17 @@ function tryPackage(requestPath, exts) {
8888
return tryFile(filename) || tryExtensions(filename, exts);
8989
}
9090

91+
// In order to minimize unnecessary lstat() calls,
92+
// this cache is a list of known-real paths.
93+
// Set to an empty object to reset.
94+
Module._realpathCache = {}
95+
9196
// check if the file exists and is not a directory
9297
function tryFile(requestPath) {
9398
var fs = NativeModule.require('fs');
9499
var stats = statPath(requestPath);
95100
if (stats && !stats.isDirectory()) {
96-
return fs.realpathSync(requestPath);
101+
return fs.realpathSync(requestPath, Module._realpathCache);
97102
}
98103
return false;
99104
}

test/simple/test-fs-realpath.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,31 @@ function test_abs_with_kids(cb) {
363363
});
364364
}
365365

366+
function test_lying_cache_liar(cb) {
367+
// this should not require *any* stat calls, since everything
368+
// checked by realpath will be found in the cache.
369+
console.log('test_lying_cache_liar');
370+
var cache = { '/foo/bar/baz/bluff' : '/foo/bar/bluff',
371+
'/1/2/3/4/5/6/7' : '/1',
372+
'/a' : '/a',
373+
'/a/b' : '/a/b',
374+
'/a/b/c' : '/a/b',
375+
'/a/b/d' : '/a/b/d' };
376+
var rps = fs.realpathSync('/foo/bar/baz/bluff', cache);
377+
assert.equal(cache['/foo/bar/baz/bluff'], rps);
378+
fs.realpath('/1/2/3/4/5/6/7', cache, function(er, rp) {
379+
assert.equal(cache['/1/2/3/4/5/6/7'], rp);
380+
});
381+
382+
var test = '/a/b/c/d',
383+
expect = '/a/b/d';
384+
var actual = fs.realpathSync(test, cache);
385+
assert.equal(expect, actual);
386+
fs.realpath(test, cache, function(er, actual) {
387+
assert.equal(expect, actual);
388+
});
389+
}
390+
366391
// ----------------------------------------------------------------------------
367392

368393
var tests = [
@@ -376,7 +401,8 @@ var tests = [
376401
test_deep_symlink_mix,
377402
test_non_symlinks,
378403
test_escape_cwd,
379-
test_abs_with_kids
404+
test_abs_with_kids,
405+
test_lying_cache_liar
380406
];
381407
var numtests = tests.length;
382408
function runNextTest(err) {

0 commit comments

Comments
 (0)