Skip to content

Commit

Permalink
Support caching for realpath, use in module load
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
isaacs authored and ry committed Feb 9, 2011
1 parent 9de5043 commit 9bed5dc
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 24 deletions.
98 changes: 76 additions & 22 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,15 +514,27 @@ if (isWindows) {
// windows version
fs.realpathSync = function realpathSync(p) {
var p = path.resolve(p);
if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
return cache[p];
}
fs.statSync(p);
if (cache) cache[p] = p;
return p;
};

// windows version
fs.realpath = function(p, cb) {
fs.realpath = function(p, cache, cb) {
if (typeof cb !== 'function') {
cb = cache;
cache = null;
}
var p = path.resolve(p);
if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
return cb(null, cache[p]);
}
fs.stat(p, function(err) {
if (err) cb(err);
if (cache) cache[p] = p;
cb(null, p);
});
};
Expand All @@ -535,11 +547,16 @@ if (isWindows) {
var nextPartRe = /(.*?)(?:[\/]+|$)/g;

// posix version
fs.realpathSync = function realpathSync(p) {
fs.realpathSync = function realpathSync(p, cache) {
// make p is absolute
p = path.resolve(p);

var seenLinks = {},
if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
return cache[p];
}

var original = p,
seenLinks = {},
knownHard = {};

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

// continue if not a symlink, or if root
if (!base || knownHard[base]) {
continue;
}
var stat = fs.lstatSync(base);
if (!stat.isSymbolicLink()) {
knownHard[base] = true;
if (!base || knownHard[base] || (cache && cache[base] === base)) {
continue;
}

// read the link if it wasn't read before
var id = stat.dev.toString(32) + ':' + stat.ino.toString(32);
if (!seenLinks[id]) {
fs.statSync(base);
seenLinks[id] = fs.readlinkSync(base);
var resolvedLink;
if (cache && Object.prototype.hasOwnProperty.call(cache, base)) {
// some known symbolic link. no need to stat again.
resolvedLink = cache[base];
} else {
var stat = fs.lstatSync(base);
if (!stat.isSymbolicLink()) {
knownHard[base] = true;
if (cache) cache[base] = base;
continue;
}

// read the link if it wasn't read before
var id = stat.dev.toString(32) + ':' + stat.ino.toString(32);
if (!seenLinks[id]) {
fs.statSync(base);
seenLinks[id] = fs.readlinkSync(base);
resolvedLink = path.resolve(previous, seenLinks[id]);
// track this, if given a cache.
if (cache) cache[base] = resolvedLink;
}
}

// resolve the link, then start over
p = path.resolve(previous, seenLinks[id], p.slice(pos));
p = path.resolve(resolvedLink, p.slice(pos));
pos = 0;
previous = base = current = '';
}

if (cache) cache[original] = p;

return p;
};


// posix version
fs.realpath = function realpath(p, cb) {
fs.realpath = function realpath(p, cache, cb) {
if (typeof cb !== 'function') {
cb = cache;
cache = null;
}

// make p is absolute
p = path.resolve(p);

var seenLinks = {},
if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
return cb(null, cache[p]);
}

var original = p,
seenLinks = {},
knownHard = {};

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

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

// continue if known to be hard or if root
if (!base || knownHard[base]) {
// continue if known to be hard or if root or in cache already.
if (!base || knownHard[base] || (cache && cache[base] === base)) {
return process.nextTick(LOOP);
}

if (cache && Object.prototype.hasOwnProperty.call(cache, base)) {
// known symbolic link. no need to stat again.
return gotResolvedLink(cache[base]);
}

return fs.lstat(base, gotStat);
}

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

// stat & read the link if not read before
// call gotTarget as soon as the link target is known
var id = stat.dev.toString(32) + ':' + stat.ino.toString(32);
if (seenLinks[id]) {
return gotTarget(null, seenLinks[id]);
return gotTarget(null, seenLinks[id], base);
}
fs.stat(base, function(err) {
if (err) return cb(err);
Expand All @@ -656,11 +703,18 @@ if (isWindows) {
});
}

function gotTarget(err, target) {
function gotTarget(err, target, base) {
if (err) return cb(err);

var resolvedLink = path.resolve(previous, target);
if (cache) cache[base] = resolvedLink;
gotResolvedLink(resolvedLink);
}

function gotResolvedLink(resolvedLink) {

// resolve the link, then start over
p = path.resolve(previous, target, p.slice(pos));
p = path.resolve(resolvedLink, p.slice(pos));
pos = 0;
previous = base = current = '';

Expand Down
7 changes: 6 additions & 1 deletion lib/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,17 @@ function tryPackage(requestPath, exts) {
return tryFile(filename) || tryExtensions(filename, exts);
}

// In order to minimize unnecessary lstat() calls,
// this cache is a list of known-real paths.
// Set to an empty object to reset.
Module._realpathCache = {}

// check if the file exists and is not a directory
function tryFile(requestPath) {
var fs = NativeModule.require('fs');
var stats = statPath(requestPath);
if (stats && !stats.isDirectory()) {
return fs.realpathSync(requestPath);
return fs.realpathSync(requestPath, Module._realpathCache);
}
return false;
}
Expand Down
28 changes: 27 additions & 1 deletion test/simple/test-fs-realpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,31 @@ function test_abs_with_kids(cb) {
});
}

function test_lying_cache_liar(cb) {
// this should not require *any* stat calls, since everything
// checked by realpath will be found in the cache.
console.log('test_lying_cache_liar');
var cache = { '/foo/bar/baz/bluff' : '/foo/bar/bluff',
'/1/2/3/4/5/6/7' : '/1',
'/a' : '/a',
'/a/b' : '/a/b',
'/a/b/c' : '/a/b',
'/a/b/d' : '/a/b/d' };
var rps = fs.realpathSync('/foo/bar/baz/bluff', cache);
assert.equal(cache['/foo/bar/baz/bluff'], rps);
fs.realpath('/1/2/3/4/5/6/7', cache, function(er, rp) {
assert.equal(cache['/1/2/3/4/5/6/7'], rp);
});

var test = '/a/b/c/d',
expect = '/a/b/d';
var actual = fs.realpathSync(test, cache);
assert.equal(expect, actual);
fs.realpath(test, cache, function(er, actual) {
assert.equal(expect, actual);
});
}

// ----------------------------------------------------------------------------

var tests = [
Expand All @@ -376,7 +401,8 @@ var tests = [
test_deep_symlink_mix,
test_non_symlinks,
test_escape_cwd,
test_abs_with_kids
test_abs_with_kids,
test_lying_cache_liar
];
var numtests = tests.length;
function runNextTest(err) {
Expand Down

0 comments on commit 9bed5dc

Please sign in to comment.