Skip to content

Commit dde8639

Browse files
committed
Add lodash queries
1 parent 1af57d3 commit dde8639

10 files changed

Lines changed: 314 additions & 1 deletion

repository/jsrepository-ast.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,5 +500,52 @@ exports.queries = {
500500
".",
501501
/Property[/:key/:name == "minorVersion"]/:value/:value
502502
)`,
503+
],
504+
lodash: [
505+
/*
506+
3.x/4.x — top-level IIFE:
507+
var VERSION = '4.17.21';
508+
...
509+
var PLACEHOLDER = '__lodash_placeholder__';
510+
...
511+
function runInContext(context) { ... lodash.VERSION = VERSION; ... }
512+
The local VERSION var is renamed by minifiers, so instead of matching its
513+
name we anchor on the exported `.VERSION =` property (survives minification)
514+
and require the enclosing block to also declare the lodash-unique
515+
'__lodash_placeholder__' string constant. Does not match lodash below 3.0.0
516+
(predates this constant) — covered by the second query below instead.
517+
*/
518+
`//BlockStatement[
519+
//VariableDeclarator[/:init/:value == "__lodash_placeholder__"]
520+
]//AssignmentExpression[
521+
/:left/:property/:name == "VERSION"
522+
]/$$:right/:value`,
523+
/*
524+
0.x/1.x/2.x — pre-runInContext era, e.g.:
525+
lodash.forOwn = forOwn;
526+
lodash.forIn = forIn;
527+
...
528+
lodash.VERSION = '2.4.2';
529+
No '__lodash_placeholder__' constant this far back, so anchor on `.forOwn =`
530+
and `.forIn =` instead — both are Lo-Dash's own additions over Underscore.js's
531+
API (Underscore never had per-own/inherited-property iteration helpers under
532+
these names), and both have existed since lodash 0.3.0, so this covers the
533+
full pre-3.0.0 range.
534+
Deliberately checking co-occurrence within the enclosing block rather than
535+
requiring all three properties to resolve to the same object binding
536+
(`/:left/$:object == ...`): the object-identity check is the most expensive
537+
part of this query (~15-20% slower per scanned file, paid on every file since
538+
all libraries' queries share one AST pass, not just lodash files) and two
539+
independent anchors sharing a block is already a strong enough signal — an
540+
unrelated bundle would need three separate objects independently named
541+
VERSION/forOwn/forIn co-located in the same block to false-positive here.
542+
*/
543+
`//BlockStatement[
544+
//AssignmentExpression/:left/:property/:name == "forOwn" &&
545+
//AssignmentExpression/:left/:property/:name == "forIn" &&
546+
//AssignmentExpression/:left/:property/:name == "VERSION"
547+
]//AssignmentExpression[
548+
/:left/:property/:name == "VERSION"
549+
]/$$:right/:value`,
503550
]
504551
};

repository/jsrepository-v2.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13633,6 +13633,10 @@
1363313633
],
1363413634
"uri": [
1363513635
"/(§§version§§)/lodash(\\.min)?\\.js"
13636+
],
13637+
"ast": [
13638+
"//BlockStatement[ //VariableDeclarator[/:init/:value == \"__lodash_placeholder__\"] ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value",
13639+
"//BlockStatement[ //AssignmentExpression/:left/:property/:name == \"forOwn\" && //AssignmentExpression/:left/:property/:name == \"forIn\" && //AssignmentExpression/:left/:property/:name == \"VERSION\" ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value"
1363613640
]
1363713641
}
1363813642
},

repository/jsrepository-v3.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13819,6 +13819,10 @@
1381913819
],
1382013820
"uri": [
1382113821
"/(§§version§§)/lodash(\\.min)?\\.js"
13822+
],
13823+
"ast": [
13824+
"//BlockStatement[ //VariableDeclarator[/:init/:value == \"__lodash_placeholder__\"] ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value",
13825+
"//BlockStatement[ //AssignmentExpression/:left/:property/:name == \"forOwn\" && //AssignmentExpression/:left/:property/:name == \"forIn\" && //AssignmentExpression/:left/:property/:name == \"VERSION\" ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value"
1382213826
]
1382313827
}
1382413828
},

repository/jsrepository-v4.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13818,6 +13818,10 @@
1381813818
],
1381913819
"uri": [
1382013820
"/(§§version§§)/lodash(\\.min)?\\.js"
13821+
],
13822+
"ast": [
13823+
"//BlockStatement[ //VariableDeclarator[/:init/:value == \"__lodash_placeholder__\"] ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value",
13824+
"//BlockStatement[ //AssignmentExpression/:left/:property/:name == \"forOwn\" && //AssignmentExpression/:left/:property/:name == \"forIn\" && //AssignmentExpression/:left/:property/:name == \"VERSION\" ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value"
1382113825
]
1382213826
}
1382313827
},

repository/jsrepository-v5-combined.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13825,6 +13825,10 @@
1382513825
],
1382613826
"uri": [
1382713827
"/(§§version§§)/lodash(\\.min)?\\.js"
13828+
],
13829+
"ast": [
13830+
"//BlockStatement[ //VariableDeclarator[/:init/:value == \"__lodash_placeholder__\"] ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value",
13831+
"//BlockStatement[ //AssignmentExpression/:left/:property/:name == \"forOwn\" && //AssignmentExpression/:left/:property/:name == \"forIn\" && //AssignmentExpression/:left/:property/:name == \"VERSION\" ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value"
1382813832
]
1382913833
}
1383013834
},

repository/jsrepository-v5.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13824,6 +13824,10 @@
1382413824
],
1382513825
"uri": [
1382613826
"/(§§version§§)/lodash(\\.min)?\\.js"
13827+
],
13828+
"ast": [
13829+
"//BlockStatement[ //VariableDeclarator[/:init/:value == \"__lodash_placeholder__\"] ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value",
13830+
"//BlockStatement[ //AssignmentExpression/:left/:property/:name == \"forOwn\" && //AssignmentExpression/:left/:property/:name == \"forIn\" && //AssignmentExpression/:left/:property/:name == \"VERSION\" ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value"
1382713831
]
1382813832
}
1382913833
},

repository/jsrepository-v6-combined.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14094,6 +14094,10 @@
1409414094
],
1409514095
"uri": [
1409614096
"/(§§version§§)/lodash(\\.min)?\\.js"
14097+
],
14098+
"ast": [
14099+
"//BlockStatement[ //VariableDeclarator[/:init/:value == \"__lodash_placeholder__\"] ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value",
14100+
"//BlockStatement[ //AssignmentExpression/:left/:property/:name == \"forOwn\" && //AssignmentExpression/:left/:property/:name == \"forIn\" && //AssignmentExpression/:left/:property/:name == \"VERSION\" ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value"
1409714101
]
1409814102
}
1409914103
},

repository/jsrepository-v6.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14093,6 +14093,10 @@
1409314093
],
1409414094
"uri": [
1409514095
"/(§§version§§)/lodash(\\.min)?\\.js"
14096+
],
14097+
"ast": [
14098+
"//BlockStatement[ //VariableDeclarator[/:init/:value == \"__lodash_placeholder__\"] ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value",
14099+
"//BlockStatement[ //AssignmentExpression/:left/:property/:name == \"forOwn\" && //AssignmentExpression/:left/:property/:name == \"forIn\" && //AssignmentExpression/:left/:property/:name == \"VERSION\" ]//AssignmentExpression[ /:left/:property/:name == \"VERSION\" ]/$$:right/:value"
1409614100
]
1409714101
}
1409814102
},

repository/profile-ast-queries.js

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env node
2+
3+
// Profiles individual AST queries (repository/jsrepository-ast.js) against the
4+
// largest real library files available, to find which queries contribute the
5+
// most to deepScan's per-file runtime cost.
6+
//
7+
// deepScan (node/src/deepscan.ts) runs every library's AST queries together in
8+
// one shared multiQuery() traversal per scanned file, so an expensive query for
9+
// one library is paid on every file retire.js scans, not just files belonging to
10+
// that library. This script isolates each query and times it individually to
11+
// find the worst offenders.
12+
//
13+
// Usage:
14+
// node profile-ast-queries.js # auto-pick the 3 largest cached libraries
15+
// node profile-ast-queries.js echarts video.js ember # profile specific libraries
16+
// node profile-ast-queries.js --count 5 --top 10 # 5 libraries, top 10 queries
17+
18+
const fs = require("fs");
19+
const path = require("path");
20+
const https = require("https");
21+
const testCases = require("./testcases.json");
22+
const repoLib = require("../node/lib/repo.js");
23+
const reporting = require("../node/lib/reporting.js");
24+
const { multiQuery, parseSource } = require("../node/node_modules/astronomical");
25+
26+
const options = { log: reporting.open({}) };
27+
28+
const args = process.argv.slice(2);
29+
let libCount = 3;
30+
let topN = 5;
31+
const explicitLibs = [];
32+
for (let i = 0; i < args.length; i++) {
33+
if (args[i] === "--count") libCount = parseInt(args[++i], 10);
34+
else if (args[i] === "--top") topN = parseInt(args[++i], 10);
35+
else explicitLibs.push(args[i]);
36+
}
37+
38+
if (!fs.existsSync("tmp")) fs.mkdirSync("tmp");
39+
40+
function tmpPathFor(uri) {
41+
return "tmp/" + uri.replace(/[^a-z0-9.]/gi, "_");
42+
}
43+
44+
function download(uri) {
45+
return new Promise((resolve, reject) => {
46+
const p = tmpPathFor(uri);
47+
if (fs.existsSync(p)) return resolve(fs.readFileSync(p, "utf-8"));
48+
process.stdout.write(` Downloading ${uri} ... `);
49+
https
50+
.get(uri, (res) => {
51+
if (res.statusCode != 200) {
52+
console.log(`failed (${res.statusCode})`);
53+
return reject(new Error(`status ${res.statusCode}`));
54+
}
55+
const chunks = [];
56+
res.on("data", (d) => chunks.push(d));
57+
res.on("end", () => {
58+
const data = Buffer.concat(chunks);
59+
fs.writeFileSync(p, data);
60+
console.log("done");
61+
resolve(data.toString("utf-8"));
62+
});
63+
})
64+
.on("error", reject);
65+
});
66+
}
67+
68+
// Every concrete (library, uri, size-on-disk-if-cached) combination described
69+
// by testcases.json, so we can rank libraries by the size of their largest file
70+
// without guessing filenames.
71+
function enumerateTestFiles() {
72+
const files = []; // { library, uri, cachedSize }
73+
for (const [library, templates] of Object.entries(testCases)) {
74+
for (const [template, tcontent] of Object.entries(templates)) {
75+
const versions = (tcontent.versions || []).concat(tcontent.additionalVersions || []);
76+
const subversions = tcontent.subversions || [""];
77+
for (const version of versions) {
78+
for (const sub of subversions) {
79+
const uri = template.replace(/§§version§§/g, version).replace(/§§subversion§§/g, sub);
80+
const p = tmpPathFor(uri);
81+
const cachedSize = fs.existsSync(p) ? fs.statSync(p).size : -1;
82+
files.push({ library, uri, cachedSize });
83+
}
84+
}
85+
}
86+
}
87+
return files;
88+
}
89+
90+
async function pickLibraryFiles() {
91+
const allFiles = enumerateTestFiles();
92+
93+
if (explicitLibs.length) {
94+
const picks = [];
95+
for (const library of explicitLibs) {
96+
const candidates = allFiles.filter((f) => f.library === library);
97+
if (!candidates.length) {
98+
console.warn(`No testcases entry for "${library}", skipping`);
99+
continue;
100+
}
101+
// Prefer the largest already-cached file; fall back to the first known
102+
// uri (and download it) if nothing for this library is cached yet.
103+
candidates.sort((a, b) => b.cachedSize - a.cachedSize);
104+
picks.push(candidates[0]);
105+
}
106+
return picks;
107+
}
108+
109+
// Auto-pick: rank libraries by the size of their largest *cached* file, since
110+
// downloading every candidate just to measure size would be slow. This is
111+
// "largest known so far" rather than "largest ever possible", which is fine
112+
// for a profiling tool — run the full test suite first to warm the cache.
113+
const byLibrary = new Map();
114+
for (const f of allFiles) {
115+
if (f.cachedSize < 0) continue;
116+
const cur = byLibrary.get(f.library);
117+
if (!cur || f.cachedSize > cur.cachedSize) byLibrary.set(f.library, f);
118+
}
119+
const ranked = [...byLibrary.values()].sort((a, b) => b.cachedSize - a.cachedSize);
120+
if (ranked.length < libCount) {
121+
console.warn(
122+
`Only ${ranked.length} libraries have cached files (run test-detection.js first to warm the cache). Using what's available.`
123+
);
124+
}
125+
return ranked.slice(0, libCount);
126+
}
127+
128+
function median(arr) {
129+
const s = [...arr].sort((a, b) => a - b);
130+
return s[Math.floor(s.length / 2)];
131+
}
132+
133+
// Times each query in isolation against a file, re-parsing fresh for every
134+
// call. Reusing one parsed AST object across repeated multiQuery() calls was
135+
// found to corrupt scope-binding resolution ($:object / $:name) on later
136+
// calls, silently invalidating results from the 2nd call onward — always
137+
// re-parse per measurement.
138+
function profileFile(code, astQueries, iterations) {
139+
const keys = Object.keys(astQueries);
140+
const costs = {};
141+
keys.forEach((k) => (costs[k] = []));
142+
143+
// warmup (JIT + file system caches), still with fresh parses
144+
for (let i = 0; i < 3; i++) {
145+
const ast = parseSource(code);
146+
multiQuery(ast, astQueries);
147+
}
148+
149+
for (let iter = 0; iter < iterations; iter++) {
150+
const order = [...keys].sort(() => Math.random() - 0.5);
151+
for (const key of order) {
152+
const ast = parseSource(code);
153+
const t0 = process.hrtime.bigint();
154+
multiQuery(ast, { [key]: astQueries[key] });
155+
const t1 = process.hrtime.bigint();
156+
costs[key].push(Number(t1 - t0) / 1e6);
157+
}
158+
}
159+
160+
const result = {};
161+
keys.forEach((k) => (result[k] = median(costs[k])));
162+
return result;
163+
}
164+
165+
async function main() {
166+
console.log("Loading repository (jsrepository-v5.json) ...");
167+
const jsRepo = await repoLib.loadrepositoryFromFile("./jsrepository-v5.json", options);
168+
169+
const astQueries = {};
170+
const backMap = {};
171+
Object.entries(jsRepo).forEach(([name, data]) => {
172+
(data.extractors.ast || []).forEach((q, i) => {
173+
astQueries[`${name}_${i}`] = q;
174+
backMap[`${name}_${i}`] = name;
175+
});
176+
});
177+
console.log(`Loaded ${Object.keys(astQueries).length} AST queries across ${new Set(Object.values(backMap)).size} libraries.\n`);
178+
179+
const picks = await pickLibraryFiles();
180+
if (!picks.length) {
181+
console.error("No candidate library files found. Run test-detection.js first to populate tmp/.");
182+
process.exit(1);
183+
}
184+
185+
console.log("Profiling against:");
186+
picks.forEach((p) => console.log(` - ${p.library}: ${p.uri}`));
187+
console.log();
188+
189+
const iterations = 8;
190+
const aggregate = {}; // key -> total ms across all profiled files
191+
const perFile = {}; // library -> { key -> ms }
192+
193+
for (const pick of picks) {
194+
let code;
195+
try {
196+
code = await download(pick.uri);
197+
} catch (e) {
198+
console.warn(` Could not obtain ${pick.uri}: ${e.message}, skipping`);
199+
continue;
200+
}
201+
console.log(`Profiling ${pick.library} (${(code.length / 1024).toFixed(0)} KB) ...`);
202+
const costs = profileFile(code, astQueries, iterations);
203+
perFile[pick.library] = costs;
204+
Object.entries(costs).forEach(([key, ms]) => {
205+
aggregate[key] = (aggregate[key] || 0) + ms;
206+
});
207+
}
208+
209+
const ranked = Object.entries(aggregate).sort((a, b) => b[1] - a[1]);
210+
211+
console.log(`\n=== Top ${topN} most expensive queries (summed across ${Object.keys(perFile).length} profiled files) ===\n`);
212+
console.log(
213+
"Rank Library".padEnd(24) + "Query idx".padEnd(11) + "Total ms".padStart(10) + " Per-file breakdown"
214+
);
215+
ranked.slice(0, topN).forEach(([key, total], i) => {
216+
const component = backMap[key];
217+
const idx = key.slice(component.length + 1);
218+
const breakdown = Object.keys(perFile)
219+
.map((lib) => `${lib}=${(perFile[lib][key] || 0).toFixed(2)}ms`)
220+
.join(", ");
221+
console.log(
222+
`${String(i + 1).padEnd(6)}${component.padEnd(18)}${idx.padEnd(11)}${total.toFixed(2).padStart(8)}ms ${breakdown}`
223+
);
224+
});
225+
226+
const grandTotal = ranked.reduce((s, [, v]) => s + v, 0);
227+
const topTotal = ranked.slice(0, topN).reduce((s, [, v]) => s + v, 0);
228+
console.log(
229+
`\nTop ${topN} account for ${topTotal.toFixed(2)}ms of ${grandTotal.toFixed(2)}ms total isolated query cost (${((100 * topTotal) / grandTotal).toFixed(1)}%).`
230+
);
231+
}
232+
233+
main().catch((err) => {
234+
console.error("Failed:", err);
235+
process.exit(1);
236+
});

repository/testcases.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -747,12 +747,14 @@
747747
"4.17.21",
748748
"4.5.0",
749749
"3.9.3",
750+
"2.4.2",
750751
"0.3.0"
751752
],
752753
"subversions": [
753754
"",
754755
".min"
755-
]
756+
],
757+
"allowContentMiss": ["2.4.2"]
756758
}
757759
},
758760
"marked": {

0 commit comments

Comments
 (0)