Commit 43757d0

Browse files
codebytereaduh95
authored andcommitted
module: cache nearest parent package.json per directory
getNearestParentPackageJSON() memoized its answer per file, so every module loaded still made one native call, and TraverseParent() builds several std::filesystem::path temporaries per directory level and serializes the whole package.json, which the JS side then usually discarded because it already had that package.json deserialized. The native traversal starts at the directory of the given path, so the answer only depends on that directory: key the memo by it (following NormalizePath()'s trailing-separator rule), so that all modules in a directory share one native call. When the permission model is enabled the traversal also depends on the read permissions in effect at call time, so that configuration keeps the per-file cache. Loading a 1000-module tree spread over ~240 directories goes from 1000 to 236 native calls. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65326 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com>
1 parent 77fedbd commit 43757d0

2 files changed

Lines changed: 106 additions & 19 deletions

File tree

β€Žlib/internal/modules/package_json_reader.jsβ€Ž

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const {
2323
ERR_MODULE_NOT_FOUND,
2424
},
2525
}=require('internal/errors');
26-
const{ kEmptyObject }=require('internal/util');
26+
const{ kEmptyObject, isWindows }=require('internal/util');
27+
constpermission=require('internal/process/permission');
2728
constmodulesBinding=internalBinding('modules');
2829
constpath=require('path');
2930
const{ validateString }=require('internal/validators');
@@ -140,10 +141,18 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
140141
}
141142

142143
/**
143-
* A cache mapping a module's path to its parent `package.json` file's path.
144-
* This is used in concert with `deserializedPackageJSONCache` to improve
145-
* the performance of `getNearestParentPackageJSON` when called repeatedly
146-
* on the same module paths.
144+
* A cache mapping a directory to the path of the nearest `package.json` at or
145+
* above it (`null` when there is none). The native traversal for a module
146+
* starts at the module's directory, so every module in a directory shares one
147+
* entry and one native call. Used in concert with
148+
* `deserializedPackageJSONCache`.
149+
*/
150+
constdirectoryToParentPackageJSONPathCache=newSafeMap();
151+
152+
/**
153+
* When the permission model is enabled the native traversal also depends on
154+
* the read permissions in effect at the time of the call, so results are only
155+
* remembered per exact module path, as before.
147156
*/
148157
constmoduleToParentPackageJSONCache=newSafeMap();
149158

@@ -158,30 +167,51 @@ const moduleToParentPackageJSONCache = new SafeMap();
158167
*/
159168
constdeserializedPackageJSONCache=newSafeMap();
160169

170+
/**
171+
* The directory the native nearest-parent traversal starts from for `checkPath`
172+
* (see BindingData::NormalizePath/TraverseParent): the path itself when it has
173+
* a trailing separator, its dirname otherwise.
174+
* @param {string} checkPath
175+
* @returns {string}
176+
*/
177+
functiongetTraversalStartDirectory(checkPath){
178+
constlast=checkPath[checkPath.length-1];
179+
if(last==='/'||(isWindows&&last==='\\')){
180+
returnStringPrototypeSlice(checkPath,0,-1);
181+
}
182+
returnpath.dirname(checkPath);
183+
}
184+
161185
/**
162186
* Get the nearest parent package.json file from a given path.
163187
* Return the package.json data and the path to the package.json file, or undefined.
164188
* @param {string} checkPath The path to start searching from.
165189
* @returns {undefined | DeserializedPackageConfig}
166190
*/
167191
functiongetNearestParentPackageJSON(checkPath){
168-
constparentPackageJSONPath=moduleToParentPackageJSONCache.get(checkPath);
169-
if(parentPackageJSONPath!==undefined){
170-
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
192+
constpermissionEnabled=permission.isEnabled();
193+
constcache=permissionEnabled ? moduleToParentPackageJSONCache : directoryToParentPackageJSONPathCache;
194+
constkey=permissionEnabled ? checkPath : getTraversalStartDirectory(checkPath);
195+
letparentPackageJSONPath=cache.get(key);
196+
if(parentPackageJSONPath===undefined){
197+
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
198+
if(result===undefined){
199+
parentPackageJSONPath=null;
200+
}else{
201+
constpackageConfig=deserializePackageJSON(checkPath,result);
202+
parentPackageJSONPath=packageConfig.path;
203+
if(!deserializedPackageJSONCache.has(parentPackageJSONPath)){
204+
deserializedPackageJSONCache.set(parentPackageJSONPath,packageConfig);
205+
}
206+
}
207+
cache.set(key,parentPackageJSONPath);
171208
}
172209

173-
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
174-
constpackageConfig=deserializePackageJSON(checkPath,result);
175-
176-
moduleToParentPackageJSONCache.set(checkPath,packageConfig.path);
177-
178-
constmaybeCachedPackageConfig=deserializedPackageJSONCache.get(packageConfig.path);
179-
if(maybeCachedPackageConfig!==undefined){
180-
returnmaybeCachedPackageConfig;
210+
if(parentPackageJSONPath===null){
211+
// No package.json above this path: same shape as before, carrying the queried path.
212+
returndeserializePackageJSON(checkPath,undefined);
181213
}
182-
183-
deserializedPackageJSONCache.set(packageConfig.path,packageConfig);
184-
returnpackageConfig;
214+
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
185215
}
186216

187217
/**
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
// The nearest parent package.json lookup that every CommonJS module load
4+
// performs is answered once per directory, not once per file.
5+
constcommon=require('../common');
6+
consttmpdir=require('../common/tmpdir');
7+
constassert=require('assert');
8+
constfs=require('fs');
9+
constpath=require('path');
10+
const{ internalBinding }=require('internal/test/binding');
11+
constpackageJsonReader=require('internal/modules/package_json_reader');
12+
13+
tmpdir.refresh();
14+
constroot=tmpdir.resolve('pkg');
15+
constsub=path.join(root,'lib','sub');
16+
fs.mkdirSync(sub,{recursive: true});
17+
fs.writeFileSync(path.join(root,'package.json'),JSON.stringify({name: 'pkg',type: 'commonjs'}));
18+
constfiles=[];
19+
for(constdirof[path.join(root,'lib'),sub]){
20+
for(leti=0;i<5;i++){
21+
constfile=path.join(dir,`m${i}.js`);
22+
fs.writeFileSync(file,'module.exports = __filename;');
23+
files.push(file);
24+
}
25+
}
26+
27+
constmodulesBinding=internalBinding('modules');
28+
constoriginal=modulesBinding.getNearestParentPackageJSON;
29+
constcalls=[];
30+
modulesBinding.getNearestParentPackageJSON=common.mustCallAtLeast((checkPath)=>{
31+
calls.push(checkPath);
32+
returnoriginal(checkPath);
33+
},1);
34+
35+
for(constfileoffiles){
36+
assert.strictEqual(require(file),file);
37+
}
38+
// Ten modules in two directories: two lookups reach the binding.
39+
assert.strictEqual(calls.length,2,`binding called for: ${calls.join(', ')}`);
40+
41+
// Same answer (and the same object) for every file of a directory, and for
42+
// the directory itself when asked with a trailing separator.
43+
constviaFile=packageJsonReader.getNearestParentPackageJSON(files[0]);
44+
assert.strictEqual(viaFile.data.name,'pkg');
45+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(files[1]),viaFile);
46+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(root,'lib')+path.sep),viaFile);
47+
assert.strictEqual(calls.length,2);
48+
49+
// A directory that has not been seen yet is looked up once more.
50+
constother=path.join(root,'other');
51+
fs.mkdirSync(other);
52+
fs.writeFileSync(path.join(other,'x.js'),'');
53+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'x.js')).data.name,'pkg');
54+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'y.js')).data.name,'pkg');
55+
assert.strictEqual(calls.length,3);
56+
57+
modulesBinding.getNearestParentPackageJSON=original;

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit 43757d0

Browse files
codebytereaduh95
authored andcommitted
module: cache nearest parent package.json per directory
getNearestParentPackageJSON() memoized its answer per file, so every module loaded still made one native call, and TraverseParent() builds several std::filesystem::path temporaries per directory level and serializes the whole package.json, which the JS side then usually discarded because it already had that package.json deserialized. The native traversal starts at the directory of the given path, so the answer only depends on that directory: key the memo by it (following NormalizePath()'s trailing-separator rule), so that all modules in a directory share one native call. When the permission model is enabled the traversal also depends on the read permissions in effect at call time, so that configuration keeps the per-file cache. Loading a 1000-module tree spread over ~240 directories goes from 1000 to 236 native calls. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65326 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com>
1 parent 77fedbd commit 43757d0

2 files changed

Lines changed: 106 additions & 19 deletions

File tree

β€Žlib/internal/modules/package_json_reader.jsβ€Ž

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const {
2323
ERR_MODULE_NOT_FOUND,
2424
},
2525
}=require('internal/errors');
26-
const{ kEmptyObject }=require('internal/util');
26+
const{ kEmptyObject, isWindows }=require('internal/util');
27+
constpermission=require('internal/process/permission');
2728
constmodulesBinding=internalBinding('modules');
2829
constpath=require('path');
2930
const{ validateString }=require('internal/validators');
@@ -140,10 +141,18 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
140141
}
141142

142143
/**
143-
* A cache mapping a module's path to its parent `package.json` file's path.
144-
* This is used in concert with `deserializedPackageJSONCache` to improve
145-
* the performance of `getNearestParentPackageJSON` when called repeatedly
146-
* on the same module paths.
144+
* A cache mapping a directory to the path of the nearest `package.json` at or
145+
* above it (`null` when there is none). The native traversal for a module
146+
* starts at the module's directory, so every module in a directory shares one
147+
* entry and one native call. Used in concert with
148+
* `deserializedPackageJSONCache`.
149+
*/
150+
constdirectoryToParentPackageJSONPathCache=newSafeMap();
151+
152+
/**
153+
* When the permission model is enabled the native traversal also depends on
154+
* the read permissions in effect at the time of the call, so results are only
155+
* remembered per exact module path, as before.
147156
*/
148157
constmoduleToParentPackageJSONCache=newSafeMap();
149158

@@ -158,30 +167,51 @@ const moduleToParentPackageJSONCache = new SafeMap();
158167
*/
159168
constdeserializedPackageJSONCache=newSafeMap();
160169

170+
/**
171+
* The directory the native nearest-parent traversal starts from for `checkPath`
172+
* (see BindingData::NormalizePath/TraverseParent): the path itself when it has
173+
* a trailing separator, its dirname otherwise.
174+
* @param {string} checkPath
175+
* @returns {string}
176+
*/
177+
functiongetTraversalStartDirectory(checkPath){
178+
constlast=checkPath[checkPath.length-1];
179+
if(last==='/'||(isWindows&&last==='\\')){
180+
returnStringPrototypeSlice(checkPath,0,-1);
181+
}
182+
returnpath.dirname(checkPath);
183+
}
184+
161185
/**
162186
* Get the nearest parent package.json file from a given path.
163187
* Return the package.json data and the path to the package.json file, or undefined.
164188
* @param {string} checkPath The path to start searching from.
165189
* @returns {undefined | DeserializedPackageConfig}
166190
*/
167191
functiongetNearestParentPackageJSON(checkPath){
168-
constparentPackageJSONPath=moduleToParentPackageJSONCache.get(checkPath);
169-
if(parentPackageJSONPath!==undefined){
170-
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
192+
constpermissionEnabled=permission.isEnabled();
193+
constcache=permissionEnabled ? moduleToParentPackageJSONCache : directoryToParentPackageJSONPathCache;
194+
constkey=permissionEnabled ? checkPath : getTraversalStartDirectory(checkPath);
195+
letparentPackageJSONPath=cache.get(key);
196+
if(parentPackageJSONPath===undefined){
197+
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
198+
if(result===undefined){
199+
parentPackageJSONPath=null;
200+
}else{
201+
constpackageConfig=deserializePackageJSON(checkPath,result);
202+
parentPackageJSONPath=packageConfig.path;
203+
if(!deserializedPackageJSONCache.has(parentPackageJSONPath)){
204+
deserializedPackageJSONCache.set(parentPackageJSONPath,packageConfig);
205+
}
206+
}
207+
cache.set(key,parentPackageJSONPath);
171208
}
172209

173-
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
174-
constpackageConfig=deserializePackageJSON(checkPath,result);
175-
176-
moduleToParentPackageJSONCache.set(checkPath,packageConfig.path);
177-
178-
constmaybeCachedPackageConfig=deserializedPackageJSONCache.get(packageConfig.path);
179-
if(maybeCachedPackageConfig!==undefined){
180-
returnmaybeCachedPackageConfig;
210+
if(parentPackageJSONPath===null){
211+
// No package.json above this path: same shape as before, carrying the queried path.
212+
returndeserializePackageJSON(checkPath,undefined);
181213
}
182-
183-
deserializedPackageJSONCache.set(packageConfig.path,packageConfig);
184-
returnpackageConfig;
214+
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
185215
}
186216

187217
/**
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
// The nearest parent package.json lookup that every CommonJS module load
4+
// performs is answered once per directory, not once per file.
5+
constcommon=require('../common');
6+
consttmpdir=require('../common/tmpdir');
7+
constassert=require('assert');
8+
constfs=require('fs');
9+
constpath=require('path');
10+
const{ internalBinding }=require('internal/test/binding');
11+
constpackageJsonReader=require('internal/modules/package_json_reader');
12+
13+
tmpdir.refresh();
14+
constroot=tmpdir.resolve('pkg');
15+
constsub=path.join(root,'lib','sub');
16+
fs.mkdirSync(sub,{recursive: true});
17+
fs.writeFileSync(path.join(root,'package.json'),JSON.stringify({name: 'pkg',type: 'commonjs'}));
18+
constfiles=[];
19+
for(constdirof[path.join(root,'lib'),sub]){
20+
for(leti=0;i<5;i++){
21+
constfile=path.join(dir,`m${i}.js`);
22+
fs.writeFileSync(file,'module.exports = __filename;');
23+
files.push(file);
24+
}
25+
}
26+
27+
constmodulesBinding=internalBinding('modules');
28+
constoriginal=modulesBinding.getNearestParentPackageJSON;
29+
constcalls=[];
30+
modulesBinding.getNearestParentPackageJSON=common.mustCallAtLeast((checkPath)=>{
31+
calls.push(checkPath);
32+
returnoriginal(checkPath);
33+
},1);
34+
35+
for(constfileoffiles){
36+
assert.strictEqual(require(file),file);
37+
}
38+
// Ten modules in two directories: two lookups reach the binding.
39+
assert.strictEqual(calls.length,2,`binding called for: ${calls.join(', ')}`);
40+
41+
// Same answer (and the same object) for every file of a directory, and for
42+
// the directory itself when asked with a trailing separator.
43+
constviaFile=packageJsonReader.getNearestParentPackageJSON(files[0]);
44+
assert.strictEqual(viaFile.data.name,'pkg');
45+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(files[1]),viaFile);
46+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(root,'lib')+path.sep),viaFile);
47+
assert.strictEqual(calls.length,2);
48+
49+
// A directory that has not been seen yet is looked up once more.
50+
constother=path.join(root,'other');
51+
fs.mkdirSync(other);
52+
fs.writeFileSync(path.join(other,'x.js'),'');
53+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'x.js')).data.name,'pkg');
54+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'y.js')).data.name,'pkg');
55+
assert.strictEqual(calls.length,3);
56+
57+
modulesBinding.getNearestParentPackageJSON=original;

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 43757d0

Browse files
codebytereaduh95
authored andcommitted
module: cache nearest parent package.json per directory
getNearestParentPackageJSON() memoized its answer per file, so every module loaded still made one native call, and TraverseParent() builds several std::filesystem::path temporaries per directory level and serializes the whole package.json, which the JS side then usually discarded because it already had that package.json deserialized. The native traversal starts at the directory of the given path, so the answer only depends on that directory: key the memo by it (following NormalizePath()'s trailing-separator rule), so that all modules in a directory share one native call. When the permission model is enabled the traversal also depends on the read permissions in effect at call time, so that configuration keeps the per-file cache. Loading a 1000-module tree spread over ~240 directories goes from 1000 to 236 native calls. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65326 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com>
1 parent 77fedbd commit 43757d0

2 files changed

Lines changed: 106 additions & 19 deletions

File tree

β€Žlib/internal/modules/package_json_reader.jsβ€Ž

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const {
2323
ERR_MODULE_NOT_FOUND,
2424
},
2525
}=require('internal/errors');
26-
const{ kEmptyObject }=require('internal/util');
26+
const{ kEmptyObject, isWindows }=require('internal/util');
27+
constpermission=require('internal/process/permission');
2728
constmodulesBinding=internalBinding('modules');
2829
constpath=require('path');
2930
const{ validateString }=require('internal/validators');
@@ -140,10 +141,18 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
140141
}
141142

142143
/**
143-
* A cache mapping a module's path to its parent `package.json` file's path.
144-
* This is used in concert with `deserializedPackageJSONCache` to improve
145-
* the performance of `getNearestParentPackageJSON` when called repeatedly
146-
* on the same module paths.
144+
* A cache mapping a directory to the path of the nearest `package.json` at or
145+
* above it (`null` when there is none). The native traversal for a module
146+
* starts at the module's directory, so every module in a directory shares one
147+
* entry and one native call. Used in concert with
148+
* `deserializedPackageJSONCache`.
149+
*/
150+
constdirectoryToParentPackageJSONPathCache=newSafeMap();
151+
152+
/**
153+
* When the permission model is enabled the native traversal also depends on
154+
* the read permissions in effect at the time of the call, so results are only
155+
* remembered per exact module path, as before.
147156
*/
148157
constmoduleToParentPackageJSONCache=newSafeMap();
149158

@@ -158,30 +167,51 @@ const moduleToParentPackageJSONCache = new SafeMap();
158167
*/
159168
constdeserializedPackageJSONCache=newSafeMap();
160169

170+
/**
171+
* The directory the native nearest-parent traversal starts from for `checkPath`
172+
* (see BindingData::NormalizePath/TraverseParent): the path itself when it has
173+
* a trailing separator, its dirname otherwise.
174+
* @param {string} checkPath
175+
* @returns {string}
176+
*/
177+
functiongetTraversalStartDirectory(checkPath){
178+
constlast=checkPath[checkPath.length-1];
179+
if(last==='/'||(isWindows&&last==='\\')){
180+
returnStringPrototypeSlice(checkPath,0,-1);
181+
}
182+
returnpath.dirname(checkPath);
183+
}
184+
161185
/**
162186
* Get the nearest parent package.json file from a given path.
163187
* Return the package.json data and the path to the package.json file, or undefined.
164188
* @param {string} checkPath The path to start searching from.
165189
* @returns {undefined | DeserializedPackageConfig}
166190
*/
167191
functiongetNearestParentPackageJSON(checkPath){
168-
constparentPackageJSONPath=moduleToParentPackageJSONCache.get(checkPath);
169-
if(parentPackageJSONPath!==undefined){
170-
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
192+
constpermissionEnabled=permission.isEnabled();
193+
constcache=permissionEnabled ? moduleToParentPackageJSONCache : directoryToParentPackageJSONPathCache;
194+
constkey=permissionEnabled ? checkPath : getTraversalStartDirectory(checkPath);
195+
letparentPackageJSONPath=cache.get(key);
196+
if(parentPackageJSONPath===undefined){
197+
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
198+
if(result===undefined){
199+
parentPackageJSONPath=null;
200+
}else{
201+
constpackageConfig=deserializePackageJSON(checkPath,result);
202+
parentPackageJSONPath=packageConfig.path;
203+
if(!deserializedPackageJSONCache.has(parentPackageJSONPath)){
204+
deserializedPackageJSONCache.set(parentPackageJSONPath,packageConfig);
205+
}
206+
}
207+
cache.set(key,parentPackageJSONPath);
171208
}
172209

173-
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
174-
constpackageConfig=deserializePackageJSON(checkPath,result);
175-
176-
moduleToParentPackageJSONCache.set(checkPath,packageConfig.path);
177-
178-
constmaybeCachedPackageConfig=deserializedPackageJSONCache.get(packageConfig.path);
179-
if(maybeCachedPackageConfig!==undefined){
180-
returnmaybeCachedPackageConfig;
210+
if(parentPackageJSONPath===null){
211+
// No package.json above this path: same shape as before, carrying the queried path.
212+
returndeserializePackageJSON(checkPath,undefined);
181213
}
182-
183-
deserializedPackageJSONCache.set(packageConfig.path,packageConfig);
184-
returnpackageConfig;
214+
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
185215
}
186216

187217
/**
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
// The nearest parent package.json lookup that every CommonJS module load
4+
// performs is answered once per directory, not once per file.
5+
constcommon=require('../common');
6+
consttmpdir=require('../common/tmpdir');
7+
constassert=require('assert');
8+
constfs=require('fs');
9+
constpath=require('path');
10+
const{ internalBinding }=require('internal/test/binding');
11+
constpackageJsonReader=require('internal/modules/package_json_reader');
12+
13+
tmpdir.refresh();
14+
constroot=tmpdir.resolve('pkg');
15+
constsub=path.join(root,'lib','sub');
16+
fs.mkdirSync(sub,{recursive: true});
17+
fs.writeFileSync(path.join(root,'package.json'),JSON.stringify({name: 'pkg',type: 'commonjs'}));
18+
constfiles=[];
19+
for(constdirof[path.join(root,'lib'),sub]){
20+
for(leti=0;i<5;i++){
21+
constfile=path.join(dir,`m${i}.js`);
22+
fs.writeFileSync(file,'module.exports = __filename;');
23+
files.push(file);
24+
}
25+
}
26+
27+
constmodulesBinding=internalBinding('modules');
28+
constoriginal=modulesBinding.getNearestParentPackageJSON;
29+
constcalls=[];
30+
modulesBinding.getNearestParentPackageJSON=common.mustCallAtLeast((checkPath)=>{
31+
calls.push(checkPath);
32+
returnoriginal(checkPath);
33+
},1);
34+
35+
for(constfileoffiles){
36+
assert.strictEqual(require(file),file);
37+
}
38+
// Ten modules in two directories: two lookups reach the binding.
39+
assert.strictEqual(calls.length,2,`binding called for: ${calls.join(', ')}`);
40+
41+
// Same answer (and the same object) for every file of a directory, and for
42+
// the directory itself when asked with a trailing separator.
43+
constviaFile=packageJsonReader.getNearestParentPackageJSON(files[0]);
44+
assert.strictEqual(viaFile.data.name,'pkg');
45+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(files[1]),viaFile);
46+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(root,'lib')+path.sep),viaFile);
47+
assert.strictEqual(calls.length,2);
48+
49+
// A directory that has not been seen yet is looked up once more.
50+
constother=path.join(root,'other');
51+
fs.mkdirSync(other);
52+
fs.writeFileSync(path.join(other,'x.js'),'');
53+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'x.js')).data.name,'pkg');
54+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'y.js')).data.name,'pkg');
55+
assert.strictEqual(calls.length,3);
56+
57+
modulesBinding.getNearestParentPackageJSON=original;

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 43757d0

Browse files
codebytereaduh95
authored andcommitted
module: cache nearest parent package.json per directory
getNearestParentPackageJSON() memoized its answer per file, so every module loaded still made one native call, and TraverseParent() builds several std::filesystem::path temporaries per directory level and serializes the whole package.json, which the JS side then usually discarded because it already had that package.json deserialized. The native traversal starts at the directory of the given path, so the answer only depends on that directory: key the memo by it (following NormalizePath()'s trailing-separator rule), so that all modules in a directory share one native call. When the permission model is enabled the traversal also depends on the read permissions in effect at call time, so that configuration keeps the per-file cache. Loading a 1000-module tree spread over ~240 directories goes from 1000 to 236 native calls. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65326 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com>
1 parent 77fedbd commit 43757d0

2 files changed

Lines changed: 106 additions & 19 deletions

File tree

β€Žlib/internal/modules/package_json_reader.jsβ€Ž

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const {
2323
ERR_MODULE_NOT_FOUND,
2424
},
2525
}=require('internal/errors');
26-
const{ kEmptyObject }=require('internal/util');
26+
const{ kEmptyObject, isWindows }=require('internal/util');
27+
constpermission=require('internal/process/permission');
2728
constmodulesBinding=internalBinding('modules');
2829
constpath=require('path');
2930
const{ validateString }=require('internal/validators');
@@ -140,10 +141,18 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
140141
}
141142

142143
/**
143-
* A cache mapping a module's path to its parent `package.json` file's path.
144-
* This is used in concert with `deserializedPackageJSONCache` to improve
145-
* the performance of `getNearestParentPackageJSON` when called repeatedly
146-
* on the same module paths.
144+
* A cache mapping a directory to the path of the nearest `package.json` at or
145+
* above it (`null` when there is none). The native traversal for a module
146+
* starts at the module's directory, so every module in a directory shares one
147+
* entry and one native call. Used in concert with
148+
* `deserializedPackageJSONCache`.
149+
*/
150+
constdirectoryToParentPackageJSONPathCache=newSafeMap();
151+
152+
/**
153+
* When the permission model is enabled the native traversal also depends on
154+
* the read permissions in effect at the time of the call, so results are only
155+
* remembered per exact module path, as before.
147156
*/
148157
constmoduleToParentPackageJSONCache=newSafeMap();
149158

@@ -158,30 +167,51 @@ const moduleToParentPackageJSONCache = new SafeMap();
158167
*/
159168
constdeserializedPackageJSONCache=newSafeMap();
160169

170+
/**
171+
* The directory the native nearest-parent traversal starts from for `checkPath`
172+
* (see BindingData::NormalizePath/TraverseParent): the path itself when it has
173+
* a trailing separator, its dirname otherwise.
174+
* @param {string} checkPath
175+
* @returns {string}
176+
*/
177+
functiongetTraversalStartDirectory(checkPath){
178+
constlast=checkPath[checkPath.length-1];
179+
if(last==='/'||(isWindows&&last==='\\')){
180+
returnStringPrototypeSlice(checkPath,0,-1);
181+
}
182+
returnpath.dirname(checkPath);
183+
}
184+
161185
/**
162186
* Get the nearest parent package.json file from a given path.
163187
* Return the package.json data and the path to the package.json file, or undefined.
164188
* @param {string} checkPath The path to start searching from.
165189
* @returns {undefined | DeserializedPackageConfig}
166190
*/
167191
functiongetNearestParentPackageJSON(checkPath){
168-
constparentPackageJSONPath=moduleToParentPackageJSONCache.get(checkPath);
169-
if(parentPackageJSONPath!==undefined){
170-
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
192+
constpermissionEnabled=permission.isEnabled();
193+
constcache=permissionEnabled ? moduleToParentPackageJSONCache : directoryToParentPackageJSONPathCache;
194+
constkey=permissionEnabled ? checkPath : getTraversalStartDirectory(checkPath);
195+
letparentPackageJSONPath=cache.get(key);
196+
if(parentPackageJSONPath===undefined){
197+
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
198+
if(result===undefined){
199+
parentPackageJSONPath=null;
200+
}else{
201+
constpackageConfig=deserializePackageJSON(checkPath,result);
202+
parentPackageJSONPath=packageConfig.path;
203+
if(!deserializedPackageJSONCache.has(parentPackageJSONPath)){
204+
deserializedPackageJSONCache.set(parentPackageJSONPath,packageConfig);
205+
}
206+
}
207+
cache.set(key,parentPackageJSONPath);
171208
}
172209

173-
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
174-
constpackageConfig=deserializePackageJSON(checkPath,result);
175-
176-
moduleToParentPackageJSONCache.set(checkPath,packageConfig.path);
177-
178-
constmaybeCachedPackageConfig=deserializedPackageJSONCache.get(packageConfig.path);
179-
if(maybeCachedPackageConfig!==undefined){
180-
returnmaybeCachedPackageConfig;
210+
if(parentPackageJSONPath===null){
211+
// No package.json above this path: same shape as before, carrying the queried path.
212+
returndeserializePackageJSON(checkPath,undefined);
181213
}
182-
183-
deserializedPackageJSONCache.set(packageConfig.path,packageConfig);
184-
returnpackageConfig;
214+
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
185215
}
186216

187217
/**
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
// The nearest parent package.json lookup that every CommonJS module load
4+
// performs is answered once per directory, not once per file.
5+
constcommon=require('../common');
6+
consttmpdir=require('../common/tmpdir');
7+
constassert=require('assert');
8+
constfs=require('fs');
9+
constpath=require('path');
10+
const{ internalBinding }=require('internal/test/binding');
11+
constpackageJsonReader=require('internal/modules/package_json_reader');
12+
13+
tmpdir.refresh();
14+
constroot=tmpdir.resolve('pkg');
15+
constsub=path.join(root,'lib','sub');
16+
fs.mkdirSync(sub,{recursive: true});
17+
fs.writeFileSync(path.join(root,'package.json'),JSON.stringify({name: 'pkg',type: 'commonjs'}));
18+
constfiles=[];
19+
for(constdirof[path.join(root,'lib'),sub]){
20+
for(leti=0;i<5;i++){
21+
constfile=path.join(dir,`m${i}.js`);
22+
fs.writeFileSync(file,'module.exports = __filename;');
23+
files.push(file);
24+
}
25+
}
26+
27+
constmodulesBinding=internalBinding('modules');
28+
constoriginal=modulesBinding.getNearestParentPackageJSON;
29+
constcalls=[];
30+
modulesBinding.getNearestParentPackageJSON=common.mustCallAtLeast((checkPath)=>{
31+
calls.push(checkPath);
32+
returnoriginal(checkPath);
33+
},1);
34+
35+
for(constfileoffiles){
36+
assert.strictEqual(require(file),file);
37+
}
38+
// Ten modules in two directories: two lookups reach the binding.
39+
assert.strictEqual(calls.length,2,`binding called for: ${calls.join(', ')}`);
40+
41+
// Same answer (and the same object) for every file of a directory, and for
42+
// the directory itself when asked with a trailing separator.
43+
constviaFile=packageJsonReader.getNearestParentPackageJSON(files[0]);
44+
assert.strictEqual(viaFile.data.name,'pkg');
45+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(files[1]),viaFile);
46+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(root,'lib')+path.sep),viaFile);
47+
assert.strictEqual(calls.length,2);
48+
49+
// A directory that has not been seen yet is looked up once more.
50+
constother=path.join(root,'other');
51+
fs.mkdirSync(other);
52+
fs.writeFileSync(path.join(other,'x.js'),'');
53+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'x.js')).data.name,'pkg');
54+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'y.js')).data.name,'pkg');
55+
assert.strictEqual(calls.length,3);
56+
57+
modulesBinding.getNearestParentPackageJSON=original;

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit 43757d0

Browse files
codebytereaduh95
authored andcommitted
module: cache nearest parent package.json per directory
getNearestParentPackageJSON() memoized its answer per file, so every module loaded still made one native call, and TraverseParent() builds several std::filesystem::path temporaries per directory level and serializes the whole package.json, which the JS side then usually discarded because it already had that package.json deserialized. The native traversal starts at the directory of the given path, so the answer only depends on that directory: key the memo by it (following NormalizePath()'s trailing-separator rule), so that all modules in a directory share one native call. When the permission model is enabled the traversal also depends on the read permissions in effect at call time, so that configuration keeps the per-file cache. Loading a 1000-module tree spread over ~240 directories goes from 1000 to 236 native calls. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65326 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com>
1 parent 77fedbd commit 43757d0

2 files changed

Lines changed: 106 additions & 19 deletions

File tree

β€Žlib/internal/modules/package_json_reader.jsβ€Ž

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const {
2323
ERR_MODULE_NOT_FOUND,
2424
},
2525
}=require('internal/errors');
26-
const{ kEmptyObject }=require('internal/util');
26+
const{ kEmptyObject, isWindows }=require('internal/util');
27+
constpermission=require('internal/process/permission');
2728
constmodulesBinding=internalBinding('modules');
2829
constpath=require('path');
2930
const{ validateString }=require('internal/validators');
@@ -140,10 +141,18 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
140141
}
141142

142143
/**
143-
* A cache mapping a module's path to its parent `package.json` file's path.
144-
* This is used in concert with `deserializedPackageJSONCache` to improve
145-
* the performance of `getNearestParentPackageJSON` when called repeatedly
146-
* on the same module paths.
144+
* A cache mapping a directory to the path of the nearest `package.json` at or
145+
* above it (`null` when there is none). The native traversal for a module
146+
* starts at the module's directory, so every module in a directory shares one
147+
* entry and one native call. Used in concert with
148+
* `deserializedPackageJSONCache`.
149+
*/
150+
constdirectoryToParentPackageJSONPathCache=newSafeMap();
151+
152+
/**
153+
* When the permission model is enabled the native traversal also depends on
154+
* the read permissions in effect at the time of the call, so results are only
155+
* remembered per exact module path, as before.
147156
*/
148157
constmoduleToParentPackageJSONCache=newSafeMap();
149158

@@ -158,30 +167,51 @@ const moduleToParentPackageJSONCache = new SafeMap();
158167
*/
159168
constdeserializedPackageJSONCache=newSafeMap();
160169

170+
/**
171+
* The directory the native nearest-parent traversal starts from for `checkPath`
172+
* (see BindingData::NormalizePath/TraverseParent): the path itself when it has
173+
* a trailing separator, its dirname otherwise.
174+
* @param {string} checkPath
175+
* @returns {string}
176+
*/
177+
functiongetTraversalStartDirectory(checkPath){
178+
constlast=checkPath[checkPath.length-1];
179+
if(last==='/'||(isWindows&&last==='\\')){
180+
returnStringPrototypeSlice(checkPath,0,-1);
181+
}
182+
returnpath.dirname(checkPath);
183+
}
184+
161185
/**
162186
* Get the nearest parent package.json file from a given path.
163187
* Return the package.json data and the path to the package.json file, or undefined.
164188
* @param {string} checkPath The path to start searching from.
165189
* @returns {undefined | DeserializedPackageConfig}
166190
*/
167191
functiongetNearestParentPackageJSON(checkPath){
168-
constparentPackageJSONPath=moduleToParentPackageJSONCache.get(checkPath);
169-
if(parentPackageJSONPath!==undefined){
170-
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
192+
constpermissionEnabled=permission.isEnabled();
193+
constcache=permissionEnabled ? moduleToParentPackageJSONCache : directoryToParentPackageJSONPathCache;
194+
constkey=permissionEnabled ? checkPath : getTraversalStartDirectory(checkPath);
195+
letparentPackageJSONPath=cache.get(key);
196+
if(parentPackageJSONPath===undefined){
197+
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
198+
if(result===undefined){
199+
parentPackageJSONPath=null;
200+
}else{
201+
constpackageConfig=deserializePackageJSON(checkPath,result);
202+
parentPackageJSONPath=packageConfig.path;
203+
if(!deserializedPackageJSONCache.has(parentPackageJSONPath)){
204+
deserializedPackageJSONCache.set(parentPackageJSONPath,packageConfig);
205+
}
206+
}
207+
cache.set(key,parentPackageJSONPath);
171208
}
172209

173-
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
174-
constpackageConfig=deserializePackageJSON(checkPath,result);
175-
176-
moduleToParentPackageJSONCache.set(checkPath,packageConfig.path);
177-
178-
constmaybeCachedPackageConfig=deserializedPackageJSONCache.get(packageConfig.path);
179-
if(maybeCachedPackageConfig!==undefined){
180-
returnmaybeCachedPackageConfig;
210+
if(parentPackageJSONPath===null){
211+
// No package.json above this path: same shape as before, carrying the queried path.
212+
returndeserializePackageJSON(checkPath,undefined);
181213
}
182-
183-
deserializedPackageJSONCache.set(packageConfig.path,packageConfig);
184-
returnpackageConfig;
214+
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
185215
}
186216

187217
/**
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
// The nearest parent package.json lookup that every CommonJS module load
4+
// performs is answered once per directory, not once per file.
5+
constcommon=require('../common');
6+
consttmpdir=require('../common/tmpdir');
7+
constassert=require('assert');
8+
constfs=require('fs');
9+
constpath=require('path');
10+
const{ internalBinding }=require('internal/test/binding');
11+
constpackageJsonReader=require('internal/modules/package_json_reader');
12+
13+
tmpdir.refresh();
14+
constroot=tmpdir.resolve('pkg');
15+
constsub=path.join(root,'lib','sub');
16+
fs.mkdirSync(sub,{recursive: true});
17+
fs.writeFileSync(path.join(root,'package.json'),JSON.stringify({name: 'pkg',type: 'commonjs'}));
18+
constfiles=[];
19+
for(constdirof[path.join(root,'lib'),sub]){
20+
for(leti=0;i<5;i++){
21+
constfile=path.join(dir,`m${i}.js`);
22+
fs.writeFileSync(file,'module.exports = __filename;');
23+
files.push(file);
24+
}
25+
}
26+
27+
constmodulesBinding=internalBinding('modules');
28+
constoriginal=modulesBinding.getNearestParentPackageJSON;
29+
constcalls=[];
30+
modulesBinding.getNearestParentPackageJSON=common.mustCallAtLeast((checkPath)=>{
31+
calls.push(checkPath);
32+
returnoriginal(checkPath);
33+
},1);
34+
35+
for(constfileoffiles){
36+
assert.strictEqual(require(file),file);
37+
}
38+
// Ten modules in two directories: two lookups reach the binding.
39+
assert.strictEqual(calls.length,2,`binding called for: ${calls.join(', ')}`);
40+
41+
// Same answer (and the same object) for every file of a directory, and for
42+
// the directory itself when asked with a trailing separator.
43+
constviaFile=packageJsonReader.getNearestParentPackageJSON(files[0]);
44+
assert.strictEqual(viaFile.data.name,'pkg');
45+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(files[1]),viaFile);
46+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(root,'lib')+path.sep),viaFile);
47+
assert.strictEqual(calls.length,2);
48+
49+
// A directory that has not been seen yet is looked up once more.
50+
constother=path.join(root,'other');
51+
fs.mkdirSync(other);
52+
fs.writeFileSync(path.join(other,'x.js'),'');
53+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'x.js')).data.name,'pkg');
54+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'y.js')).data.name,'pkg');
55+
assert.strictEqual(calls.length,3);
56+
57+
modulesBinding.getNearestParentPackageJSON=original;

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 43757d0

Browse files
codebytereaduh95
authored andcommitted
module: cache nearest parent package.json per directory
getNearestParentPackageJSON() memoized its answer per file, so every module loaded still made one native call, and TraverseParent() builds several std::filesystem::path temporaries per directory level and serializes the whole package.json, which the JS side then usually discarded because it already had that package.json deserialized. The native traversal starts at the directory of the given path, so the answer only depends on that directory: key the memo by it (following NormalizePath()'s trailing-separator rule), so that all modules in a directory share one native call. When the permission model is enabled the traversal also depends on the read permissions in effect at call time, so that configuration keeps the per-file cache. Loading a 1000-module tree spread over ~240 directories goes from 1000 to 236 native calls. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65326 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com>
1 parent 77fedbd commit 43757d0

2 files changed

Lines changed: 106 additions & 19 deletions

File tree

β€Žlib/internal/modules/package_json_reader.jsβ€Ž

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const {
2323
ERR_MODULE_NOT_FOUND,
2424
},
2525
}=require('internal/errors');
26-
const{ kEmptyObject }=require('internal/util');
26+
const{ kEmptyObject, isWindows }=require('internal/util');
27+
constpermission=require('internal/process/permission');
2728
constmodulesBinding=internalBinding('modules');
2829
constpath=require('path');
2930
const{ validateString }=require('internal/validators');
@@ -140,10 +141,18 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
140141
}
141142

142143
/**
143-
* A cache mapping a module's path to its parent `package.json` file's path.
144-
* This is used in concert with `deserializedPackageJSONCache` to improve
145-
* the performance of `getNearestParentPackageJSON` when called repeatedly
146-
* on the same module paths.
144+
* A cache mapping a directory to the path of the nearest `package.json` at or
145+
* above it (`null` when there is none). The native traversal for a module
146+
* starts at the module's directory, so every module in a directory shares one
147+
* entry and one native call. Used in concert with
148+
* `deserializedPackageJSONCache`.
149+
*/
150+
constdirectoryToParentPackageJSONPathCache=newSafeMap();
151+
152+
/**
153+
* When the permission model is enabled the native traversal also depends on
154+
* the read permissions in effect at the time of the call, so results are only
155+
* remembered per exact module path, as before.
147156
*/
148157
constmoduleToParentPackageJSONCache=newSafeMap();
149158

@@ -158,30 +167,51 @@ const moduleToParentPackageJSONCache = new SafeMap();
158167
*/
159168
constdeserializedPackageJSONCache=newSafeMap();
160169

170+
/**
171+
* The directory the native nearest-parent traversal starts from for `checkPath`
172+
* (see BindingData::NormalizePath/TraverseParent): the path itself when it has
173+
* a trailing separator, its dirname otherwise.
174+
* @param {string} checkPath
175+
* @returns {string}
176+
*/
177+
functiongetTraversalStartDirectory(checkPath){
178+
constlast=checkPath[checkPath.length-1];
179+
if(last==='/'||(isWindows&&last==='\\')){
180+
returnStringPrototypeSlice(checkPath,0,-1);
181+
}
182+
returnpath.dirname(checkPath);
183+
}
184+
161185
/**
162186
* Get the nearest parent package.json file from a given path.
163187
* Return the package.json data and the path to the package.json file, or undefined.
164188
* @param {string} checkPath The path to start searching from.
165189
* @returns {undefined | DeserializedPackageConfig}
166190
*/
167191
functiongetNearestParentPackageJSON(checkPath){
168-
constparentPackageJSONPath=moduleToParentPackageJSONCache.get(checkPath);
169-
if(parentPackageJSONPath!==undefined){
170-
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
192+
constpermissionEnabled=permission.isEnabled();
193+
constcache=permissionEnabled ? moduleToParentPackageJSONCache : directoryToParentPackageJSONPathCache;
194+
constkey=permissionEnabled ? checkPath : getTraversalStartDirectory(checkPath);
195+
letparentPackageJSONPath=cache.get(key);
196+
if(parentPackageJSONPath===undefined){
197+
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
198+
if(result===undefined){
199+
parentPackageJSONPath=null;
200+
}else{
201+
constpackageConfig=deserializePackageJSON(checkPath,result);
202+
parentPackageJSONPath=packageConfig.path;
203+
if(!deserializedPackageJSONCache.has(parentPackageJSONPath)){
204+
deserializedPackageJSONCache.set(parentPackageJSONPath,packageConfig);
205+
}
206+
}
207+
cache.set(key,parentPackageJSONPath);
171208
}
172209

173-
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
174-
constpackageConfig=deserializePackageJSON(checkPath,result);
175-
176-
moduleToParentPackageJSONCache.set(checkPath,packageConfig.path);
177-
178-
constmaybeCachedPackageConfig=deserializedPackageJSONCache.get(packageConfig.path);
179-
if(maybeCachedPackageConfig!==undefined){
180-
returnmaybeCachedPackageConfig;
210+
if(parentPackageJSONPath===null){
211+
// No package.json above this path: same shape as before, carrying the queried path.
212+
returndeserializePackageJSON(checkPath,undefined);
181213
}
182-
183-
deserializedPackageJSONCache.set(packageConfig.path,packageConfig);
184-
returnpackageConfig;
214+
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
185215
}
186216

187217
/**
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
// The nearest parent package.json lookup that every CommonJS module load
4+
// performs is answered once per directory, not once per file.
5+
constcommon=require('../common');
6+
consttmpdir=require('../common/tmpdir');
7+
constassert=require('assert');
8+
constfs=require('fs');
9+
constpath=require('path');
10+
const{ internalBinding }=require('internal/test/binding');
11+
constpackageJsonReader=require('internal/modules/package_json_reader');
12+
13+
tmpdir.refresh();
14+
constroot=tmpdir.resolve('pkg');
15+
constsub=path.join(root,'lib','sub');
16+
fs.mkdirSync(sub,{recursive: true});
17+
fs.writeFileSync(path.join(root,'package.json'),JSON.stringify({name: 'pkg',type: 'commonjs'}));
18+
constfiles=[];
19+
for(constdirof[path.join(root,'lib'),sub]){
20+
for(leti=0;i<5;i++){
21+
constfile=path.join(dir,`m${i}.js`);
22+
fs.writeFileSync(file,'module.exports = __filename;');
23+
files.push(file);
24+
}
25+
}
26+
27+
constmodulesBinding=internalBinding('modules');
28+
constoriginal=modulesBinding.getNearestParentPackageJSON;
29+
constcalls=[];
30+
modulesBinding.getNearestParentPackageJSON=common.mustCallAtLeast((checkPath)=>{
31+
calls.push(checkPath);
32+
returnoriginal(checkPath);
33+
},1);
34+
35+
for(constfileoffiles){
36+
assert.strictEqual(require(file),file);
37+
}
38+
// Ten modules in two directories: two lookups reach the binding.
39+
assert.strictEqual(calls.length,2,`binding called for: ${calls.join(', ')}`);
40+
41+
// Same answer (and the same object) for every file of a directory, and for
42+
// the directory itself when asked with a trailing separator.
43+
constviaFile=packageJsonReader.getNearestParentPackageJSON(files[0]);
44+
assert.strictEqual(viaFile.data.name,'pkg');
45+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(files[1]),viaFile);
46+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(root,'lib')+path.sep),viaFile);
47+
assert.strictEqual(calls.length,2);
48+
49+
// A directory that has not been seen yet is looked up once more.
50+
constother=path.join(root,'other');
51+
fs.mkdirSync(other);
52+
fs.writeFileSync(path.join(other,'x.js'),'');
53+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'x.js')).data.name,'pkg');
54+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'y.js')).data.name,'pkg');
55+
assert.strictEqual(calls.length,3);
56+
57+
modulesBinding.getNearestParentPackageJSON=original;

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 43757d0

Browse files
codebytereaduh95
authored andcommitted
module: cache nearest parent package.json per directory
getNearestParentPackageJSON() memoized its answer per file, so every module loaded still made one native call, and TraverseParent() builds several std::filesystem::path temporaries per directory level and serializes the whole package.json, which the JS side then usually discarded because it already had that package.json deserialized. The native traversal starts at the directory of the given path, so the answer only depends on that directory: key the memo by it (following NormalizePath()'s trailing-separator rule), so that all modules in a directory share one native call. When the permission model is enabled the traversal also depends on the read permissions in effect at call time, so that configuration keeps the per-file cache. Loading a 1000-module tree spread over ~240 directories goes from 1000 to 236 native calls. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65326 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com>
1 parent 77fedbd commit 43757d0

2 files changed

Lines changed: 106 additions & 19 deletions

File tree

β€Žlib/internal/modules/package_json_reader.jsβ€Ž

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const {
2323
ERR_MODULE_NOT_FOUND,
2424
},
2525
}=require('internal/errors');
26-
const{ kEmptyObject }=require('internal/util');
26+
const{ kEmptyObject, isWindows }=require('internal/util');
27+
constpermission=require('internal/process/permission');
2728
constmodulesBinding=internalBinding('modules');
2829
constpath=require('path');
2930
const{ validateString }=require('internal/validators');
@@ -140,10 +141,18 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
140141
}
141142

142143
/**
143-
* A cache mapping a module's path to its parent `package.json` file's path.
144-
* This is used in concert with `deserializedPackageJSONCache` to improve
145-
* the performance of `getNearestParentPackageJSON` when called repeatedly
146-
* on the same module paths.
144+
* A cache mapping a directory to the path of the nearest `package.json` at or
145+
* above it (`null` when there is none). The native traversal for a module
146+
* starts at the module's directory, so every module in a directory shares one
147+
* entry and one native call. Used in concert with
148+
* `deserializedPackageJSONCache`.
149+
*/
150+
constdirectoryToParentPackageJSONPathCache=newSafeMap();
151+
152+
/**
153+
* When the permission model is enabled the native traversal also depends on
154+
* the read permissions in effect at the time of the call, so results are only
155+
* remembered per exact module path, as before.
147156
*/
148157
constmoduleToParentPackageJSONCache=newSafeMap();
149158

@@ -158,30 +167,51 @@ const moduleToParentPackageJSONCache = new SafeMap();
158167
*/
159168
constdeserializedPackageJSONCache=newSafeMap();
160169

170+
/**
171+
* The directory the native nearest-parent traversal starts from for `checkPath`
172+
* (see BindingData::NormalizePath/TraverseParent): the path itself when it has
173+
* a trailing separator, its dirname otherwise.
174+
* @param {string} checkPath
175+
* @returns {string}
176+
*/
177+
functiongetTraversalStartDirectory(checkPath){
178+
constlast=checkPath[checkPath.length-1];
179+
if(last==='/'||(isWindows&&last==='\\')){
180+
returnStringPrototypeSlice(checkPath,0,-1);
181+
}
182+
returnpath.dirname(checkPath);
183+
}
184+
161185
/**
162186
* Get the nearest parent package.json file from a given path.
163187
* Return the package.json data and the path to the package.json file, or undefined.
164188
* @param {string} checkPath The path to start searching from.
165189
* @returns {undefined | DeserializedPackageConfig}
166190
*/
167191
functiongetNearestParentPackageJSON(checkPath){
168-
constparentPackageJSONPath=moduleToParentPackageJSONCache.get(checkPath);
169-
if(parentPackageJSONPath!==undefined){
170-
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
192+
constpermissionEnabled=permission.isEnabled();
193+
constcache=permissionEnabled ? moduleToParentPackageJSONCache : directoryToParentPackageJSONPathCache;
194+
constkey=permissionEnabled ? checkPath : getTraversalStartDirectory(checkPath);
195+
letparentPackageJSONPath=cache.get(key);
196+
if(parentPackageJSONPath===undefined){
197+
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
198+
if(result===undefined){
199+
parentPackageJSONPath=null;
200+
}else{
201+
constpackageConfig=deserializePackageJSON(checkPath,result);
202+
parentPackageJSONPath=packageConfig.path;
203+
if(!deserializedPackageJSONCache.has(parentPackageJSONPath)){
204+
deserializedPackageJSONCache.set(parentPackageJSONPath,packageConfig);
205+
}
206+
}
207+
cache.set(key,parentPackageJSONPath);
171208
}
172209

173-
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
174-
constpackageConfig=deserializePackageJSON(checkPath,result);
175-
176-
moduleToParentPackageJSONCache.set(checkPath,packageConfig.path);
177-
178-
constmaybeCachedPackageConfig=deserializedPackageJSONCache.get(packageConfig.path);
179-
if(maybeCachedPackageConfig!==undefined){
180-
returnmaybeCachedPackageConfig;
210+
if(parentPackageJSONPath===null){
211+
// No package.json above this path: same shape as before, carrying the queried path.
212+
returndeserializePackageJSON(checkPath,undefined);
181213
}
182-
183-
deserializedPackageJSONCache.set(packageConfig.path,packageConfig);
184-
returnpackageConfig;
214+
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
185215
}
186216

187217
/**
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
// The nearest parent package.json lookup that every CommonJS module load
4+
// performs is answered once per directory, not once per file.
5+
constcommon=require('../common');
6+
consttmpdir=require('../common/tmpdir');
7+
constassert=require('assert');
8+
constfs=require('fs');
9+
constpath=require('path');
10+
const{ internalBinding }=require('internal/test/binding');
11+
constpackageJsonReader=require('internal/modules/package_json_reader');
12+
13+
tmpdir.refresh();
14+
constroot=tmpdir.resolve('pkg');
15+
constsub=path.join(root,'lib','sub');
16+
fs.mkdirSync(sub,{recursive: true});
17+
fs.writeFileSync(path.join(root,'package.json'),JSON.stringify({name: 'pkg',type: 'commonjs'}));
18+
constfiles=[];
19+
for(constdirof[path.join(root,'lib'),sub]){
20+
for(leti=0;i<5;i++){
21+
constfile=path.join(dir,`m${i}.js`);
22+
fs.writeFileSync(file,'module.exports = __filename;');
23+
files.push(file);
24+
}
25+
}
26+
27+
constmodulesBinding=internalBinding('modules');
28+
constoriginal=modulesBinding.getNearestParentPackageJSON;
29+
constcalls=[];
30+
modulesBinding.getNearestParentPackageJSON=common.mustCallAtLeast((checkPath)=>{
31+
calls.push(checkPath);
32+
returnoriginal(checkPath);
33+
},1);
34+
35+
for(constfileoffiles){
36+
assert.strictEqual(require(file),file);
37+
}
38+
// Ten modules in two directories: two lookups reach the binding.
39+
assert.strictEqual(calls.length,2,`binding called for: ${calls.join(', ')}`);
40+
41+
// Same answer (and the same object) for every file of a directory, and for
42+
// the directory itself when asked with a trailing separator.
43+
constviaFile=packageJsonReader.getNearestParentPackageJSON(files[0]);
44+
assert.strictEqual(viaFile.data.name,'pkg');
45+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(files[1]),viaFile);
46+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(root,'lib')+path.sep),viaFile);
47+
assert.strictEqual(calls.length,2);
48+
49+
// A directory that has not been seen yet is looked up once more.
50+
constother=path.join(root,'other');
51+
fs.mkdirSync(other);
52+
fs.writeFileSync(path.join(other,'x.js'),'');
53+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'x.js')).data.name,'pkg');
54+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'y.js')).data.name,'pkg');
55+
assert.strictEqual(calls.length,3);
56+
57+
modulesBinding.getNearestParentPackageJSON=original;

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit 43757d0

Browse files
codebytereaduh95
authored andcommitted
module: cache nearest parent package.json per directory
getNearestParentPackageJSON() memoized its answer per file, so every module loaded still made one native call, and TraverseParent() builds several std::filesystem::path temporaries per directory level and serializes the whole package.json, which the JS side then usually discarded because it already had that package.json deserialized. The native traversal starts at the directory of the given path, so the answer only depends on that directory: key the memo by it (following NormalizePath()'s trailing-separator rule), so that all modules in a directory share one native call. When the permission model is enabled the traversal also depends on the read permissions in effect at call time, so that configuration keeps the per-file cache. Loading a 1000-module tree spread over ~240 directories goes from 1000 to 236 native calls. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65326 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com>
1 parent 77fedbd commit 43757d0

2 files changed

Lines changed: 106 additions & 19 deletions

File tree

β€Žlib/internal/modules/package_json_reader.jsβ€Ž

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const {
2323
ERR_MODULE_NOT_FOUND,
2424
},
2525
}=require('internal/errors');
26-
const{ kEmptyObject }=require('internal/util');
26+
const{ kEmptyObject, isWindows }=require('internal/util');
27+
constpermission=require('internal/process/permission');
2728
constmodulesBinding=internalBinding('modules');
2829
constpath=require('path');
2930
const{ validateString }=require('internal/validators');
@@ -140,10 +141,18 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
140141
}
141142

142143
/**
143-
* A cache mapping a module's path to its parent `package.json` file's path.
144-
* This is used in concert with `deserializedPackageJSONCache` to improve
145-
* the performance of `getNearestParentPackageJSON` when called repeatedly
146-
* on the same module paths.
144+
* A cache mapping a directory to the path of the nearest `package.json` at or
145+
* above it (`null` when there is none). The native traversal for a module
146+
* starts at the module's directory, so every module in a directory shares one
147+
* entry and one native call. Used in concert with
148+
* `deserializedPackageJSONCache`.
149+
*/
150+
constdirectoryToParentPackageJSONPathCache=newSafeMap();
151+
152+
/**
153+
* When the permission model is enabled the native traversal also depends on
154+
* the read permissions in effect at the time of the call, so results are only
155+
* remembered per exact module path, as before.
147156
*/
148157
constmoduleToParentPackageJSONCache=newSafeMap();
149158

@@ -158,30 +167,51 @@ const moduleToParentPackageJSONCache = new SafeMap();
158167
*/
159168
constdeserializedPackageJSONCache=newSafeMap();
160169

170+
/**
171+
* The directory the native nearest-parent traversal starts from for `checkPath`
172+
* (see BindingData::NormalizePath/TraverseParent): the path itself when it has
173+
* a trailing separator, its dirname otherwise.
174+
* @param {string} checkPath
175+
* @returns {string}
176+
*/
177+
functiongetTraversalStartDirectory(checkPath){
178+
constlast=checkPath[checkPath.length-1];
179+
if(last==='/'||(isWindows&&last==='\\')){
180+
returnStringPrototypeSlice(checkPath,0,-1);
181+
}
182+
returnpath.dirname(checkPath);
183+
}
184+
161185
/**
162186
* Get the nearest parent package.json file from a given path.
163187
* Return the package.json data and the path to the package.json file, or undefined.
164188
* @param {string} checkPath The path to start searching from.
165189
* @returns {undefined | DeserializedPackageConfig}
166190
*/
167191
functiongetNearestParentPackageJSON(checkPath){
168-
constparentPackageJSONPath=moduleToParentPackageJSONCache.get(checkPath);
169-
if(parentPackageJSONPath!==undefined){
170-
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
192+
constpermissionEnabled=permission.isEnabled();
193+
constcache=permissionEnabled ? moduleToParentPackageJSONCache : directoryToParentPackageJSONPathCache;
194+
constkey=permissionEnabled ? checkPath : getTraversalStartDirectory(checkPath);
195+
letparentPackageJSONPath=cache.get(key);
196+
if(parentPackageJSONPath===undefined){
197+
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
198+
if(result===undefined){
199+
parentPackageJSONPath=null;
200+
}else{
201+
constpackageConfig=deserializePackageJSON(checkPath,result);
202+
parentPackageJSONPath=packageConfig.path;
203+
if(!deserializedPackageJSONCache.has(parentPackageJSONPath)){
204+
deserializedPackageJSONCache.set(parentPackageJSONPath,packageConfig);
205+
}
206+
}
207+
cache.set(key,parentPackageJSONPath);
171208
}
172209

173-
constresult=modulesBinding.getNearestParentPackageJSON(checkPath);
174-
constpackageConfig=deserializePackageJSON(checkPath,result);
175-
176-
moduleToParentPackageJSONCache.set(checkPath,packageConfig.path);
177-
178-
constmaybeCachedPackageConfig=deserializedPackageJSONCache.get(packageConfig.path);
179-
if(maybeCachedPackageConfig!==undefined){
180-
returnmaybeCachedPackageConfig;
210+
if(parentPackageJSONPath===null){
211+
// No package.json above this path: same shape as before, carrying the queried path.
212+
returndeserializePackageJSON(checkPath,undefined);
181213
}
182-
183-
deserializedPackageJSONCache.set(packageConfig.path,packageConfig);
184-
returnpackageConfig;
214+
returndeserializedPackageJSONCache.get(parentPackageJSONPath);
185215
}
186216

187217
/**
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
// The nearest parent package.json lookup that every CommonJS module load
4+
// performs is answered once per directory, not once per file.
5+
constcommon=require('../common');
6+
consttmpdir=require('../common/tmpdir');
7+
constassert=require('assert');
8+
constfs=require('fs');
9+
constpath=require('path');
10+
const{ internalBinding }=require('internal/test/binding');
11+
constpackageJsonReader=require('internal/modules/package_json_reader');
12+
13+
tmpdir.refresh();
14+
constroot=tmpdir.resolve('pkg');
15+
constsub=path.join(root,'lib','sub');
16+
fs.mkdirSync(sub,{recursive: true});
17+
fs.writeFileSync(path.join(root,'package.json'),JSON.stringify({name: 'pkg',type: 'commonjs'}));
18+
constfiles=[];
19+
for(constdirof[path.join(root,'lib'),sub]){
20+
for(leti=0;i<5;i++){
21+
constfile=path.join(dir,`m${i}.js`);
22+
fs.writeFileSync(file,'module.exports = __filename;');
23+
files.push(file);
24+
}
25+
}
26+
27+
constmodulesBinding=internalBinding('modules');
28+
constoriginal=modulesBinding.getNearestParentPackageJSON;
29+
constcalls=[];
30+
modulesBinding.getNearestParentPackageJSON=common.mustCallAtLeast((checkPath)=>{
31+
calls.push(checkPath);
32+
returnoriginal(checkPath);
33+
},1);
34+
35+
for(constfileoffiles){
36+
assert.strictEqual(require(file),file);
37+
}
38+
// Ten modules in two directories: two lookups reach the binding.
39+
assert.strictEqual(calls.length,2,`binding called for: ${calls.join(', ')}`);
40+
41+
// Same answer (and the same object) for every file of a directory, and for
42+
// the directory itself when asked with a trailing separator.
43+
constviaFile=packageJsonReader.getNearestParentPackageJSON(files[0]);
44+
assert.strictEqual(viaFile.data.name,'pkg');
45+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(files[1]),viaFile);
46+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(root,'lib')+path.sep),viaFile);
47+
assert.strictEqual(calls.length,2);
48+
49+
// A directory that has not been seen yet is looked up once more.
50+
constother=path.join(root,'other');
51+
fs.mkdirSync(other);
52+
fs.writeFileSync(path.join(other,'x.js'),'');
53+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'x.js')).data.name,'pkg');
54+
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other,'y.js')).data.name,'pkg');
55+
assert.strictEqual(calls.length,3);
56+
57+
modulesBinding.getNearestParentPackageJSON=original;

0 commit comments

Comments
Β (0)