Skip to content

Commit 27794f9

Browse files
committed
Auto merge of rust-lang#118024 - notriddle:notriddle/search-speed, r=GuillaumeGomez
rustdoc-search: optimize unifyFunctionTypes Final profile output: https://notriddle.com/rustdoc-html-demo-5/profile-4/index.html This PR contains three commits that improve performance of this hot inner loop: reduces the number of allocations, a fast path for the 1-element basic query case, and reconstructing the multi-element query case to use recursion instead of an explicit `backtracking` array. It also adds new test cases that I found while working on this. r? `@GuillaumeGomez`
2 parents 097261f + d82b374 commit 27794f9

File tree

3 files changed

+188
-150
lines changed

3 files changed

+188
-150
lines changed

src/librustdoc/html/static/js/search.js

+153-150
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,7 @@ function initSearch(rawSearchIndex) {
13181318
* then this function will try with a different solution, or bail with false if it
13191319
* runs out of candidates.
13201320
*
1321-
* @param {Array<FunctionType>} fnTypes - The objects to check.
1321+
* @param {Array<FunctionType>} fnTypesIn - The objects to check.
13221322
* @param {Array<QueryElement>} queryElems - The elements from the parsed query.
13231323
* @param {[FunctionType]} whereClause - Trait bounds for generic items.
13241324
* @param {Map<number,number>|null} mgensIn
@@ -1329,179 +1329,180 @@ function initSearch(rawSearchIndex) {
13291329
*/
13301330
function unifyFunctionTypes(fnTypesIn, queryElems, whereClause, mgensIn, solutionCb) {
13311331
/**
1332-
* @type Map<integer, integer>
1332+
* @type Map<integer, integer>|null
13331333
*/
1334-
let mgens = new Map(mgensIn);
1334+
const mgens = mgensIn === null ? null : new Map(mgensIn);
13351335
if (queryElems.length === 0) {
13361336
return !solutionCb || solutionCb(mgens);
13371337
}
13381338
if (!fnTypesIn || fnTypesIn.length === 0) {
13391339
return false;
13401340
}
13411341
const ql = queryElems.length;
1342-
let fl = fnTypesIn.length;
1342+
const fl = fnTypesIn.length;
1343+
1344+
// One element fast path / base case
1345+
if (ql === 1 && queryElems[0].generics.length === 0) {
1346+
const queryElem = queryElems[0];
1347+
for (const fnType of fnTypesIn) {
1348+
if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens)) {
1349+
continue;
1350+
}
1351+
if (fnType.id < 0 && queryElem.id < 0) {
1352+
if (mgens && mgens.has(fnType.id) &&
1353+
mgens.get(fnType.id) !== queryElem.id) {
1354+
continue;
1355+
}
1356+
const mgensScratch = new Map(mgens);
1357+
mgensScratch.set(fnType.id, queryElem.id);
1358+
if (!solutionCb || solutionCb(mgensScratch)) {
1359+
return true;
1360+
}
1361+
} else if (!solutionCb || solutionCb(mgens ? new Map(mgens) : null)) {
1362+
// unifyFunctionTypeIsMatchCandidate already checks that ids match
1363+
return true;
1364+
}
1365+
}
1366+
for (const fnType of fnTypesIn) {
1367+
if (!unifyFunctionTypeIsUnboxCandidate(fnType, queryElem, whereClause, mgens)) {
1368+
continue;
1369+
}
1370+
if (fnType.id < 0) {
1371+
if (mgens && mgens.has(fnType.id) &&
1372+
mgens.get(fnType.id) !== 0) {
1373+
continue;
1374+
}
1375+
const mgensScratch = new Map(mgens);
1376+
mgensScratch.set(fnType.id, 0);
1377+
if (unifyFunctionTypes(
1378+
whereClause[(-fnType.id) - 1],
1379+
queryElems,
1380+
whereClause,
1381+
mgensScratch,
1382+
solutionCb
1383+
)) {
1384+
return true;
1385+
}
1386+
} else if (unifyFunctionTypes(
1387+
fnType.generics,
1388+
queryElems,
1389+
whereClause,
1390+
mgens ? new Map(mgens) : null,
1391+
solutionCb
1392+
)) {
1393+
return true;
1394+
}
1395+
}
1396+
return false;
1397+
}
1398+
1399+
// Multiple element recursive case
13431400
/**
13441401
* @type Array<FunctionType>
13451402
*/
1346-
let fnTypes = fnTypesIn.slice();
1403+
const fnTypes = fnTypesIn.slice();
13471404
/**
1348-
* loop works by building up a solution set in the working arrays
1405+
* Algorithm works by building up a solution set in the working arrays
13491406
* fnTypes gets mutated in place to make this work, while queryElems
1350-
* is left alone
1407+
* is left alone.
13511408
*
1352-
* vvvvvvv `i` points here
1353-
* queryElems = [ good, good, good, unknown, unknown ],
1354-
* fnTypes = [ good, good, good, unknown, unknown ],
1355-
* ---------------- ^^^^^^^^^^^^^^^^ `j` iterates after `i`,
1356-
* | looking for candidates
1357-
* everything before `i` is the
1358-
* current working solution
1409+
* It works backwards, because arrays can be cheaply truncated that way.
1410+
*
1411+
* vvvvvvv `queryElem`
1412+
* queryElems = [ unknown, unknown, good, good, good ]
1413+
* fnTypes = [ unknown, unknown, good, good, good ]
1414+
* ^^^^^^^^^^^^^^^^ loop over these elements to find candidates
13591415
*
13601416
* Everything in the current working solution is known to be a good
13611417
* match, but it might not be the match we wind up going with, because
13621418
* there might be more than one candidate match, and we need to try them all
13631419
* before giving up. So, to handle this, it backtracks on failure.
1364-
*
1365-
* @type Array<{
1366-
* "fnTypesScratch": Array<FunctionType>,
1367-
* "queryElemsOffset": integer,
1368-
* "fnTypesOffset": integer
1369-
* }>
13701420
*/
1371-
const backtracking = [];
1372-
let i = 0;
1373-
let j = 0;
1374-
const backtrack = () => {
1375-
while (backtracking.length !== 0) {
1376-
// this session failed, but there are other possible solutions
1377-
// to backtrack, reset to (a copy of) the old array, do the swap or unboxing
1378-
const {
1379-
fnTypesScratch,
1380-
mgensScratch,
1381-
queryElemsOffset,
1382-
fnTypesOffset,
1383-
unbox,
1384-
} = backtracking.pop();
1385-
mgens = new Map(mgensScratch);
1386-
const fnType = fnTypesScratch[fnTypesOffset];
1387-
const queryElem = queryElems[queryElemsOffset];
1388-
if (unbox) {
1389-
if (fnType.id < 0) {
1390-
if (mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) {
1391-
continue;
1392-
}
1393-
mgens.set(fnType.id, 0);
1394-
}
1395-
const generics = fnType.id < 0 ?
1396-
whereClause[(-fnType.id) - 1] :
1397-
fnType.generics;
1398-
fnTypes = fnTypesScratch.toSpliced(fnTypesOffset, 1, ...generics);
1399-
fl = fnTypes.length;
1400-
// re-run the matching algorithm on this item
1401-
i = queryElemsOffset - 1;
1402-
} else {
1403-
if (fnType.id < 0) {
1404-
if (mgens.has(fnType.id) && mgens.get(fnType.id) !== queryElem.id) {
1405-
continue;
1406-
}
1407-
mgens.set(fnType.id, queryElem.id);
1408-
}
1409-
fnTypes = fnTypesScratch.slice();
1410-
fl = fnTypes.length;
1411-
const tmp = fnTypes[queryElemsOffset];
1412-
fnTypes[queryElemsOffset] = fnTypes[fnTypesOffset];
1413-
fnTypes[fnTypesOffset] = tmp;
1414-
// this is known as a good match; go to the next one
1415-
i = queryElemsOffset;
1416-
}
1417-
return true;
1421+
const flast = fl - 1;
1422+
const qlast = ql - 1;
1423+
const queryElem = queryElems[qlast];
1424+
let queryElemsTmp = null;
1425+
for (let i = flast; i >= 0; i -= 1) {
1426+
const fnType = fnTypes[i];
1427+
if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens)) {
1428+
continue;
14181429
}
1419-
return false;
1420-
};
1421-
for (i = 0; i !== ql; ++i) {
1422-
const queryElem = queryElems[i];
1423-
/**
1424-
* list of potential function types that go with the current query element.
1425-
* @type Array<integer>
1426-
*/
1427-
const matchCandidates = [];
1428-
let fnTypesScratch = null;
1429-
let mgensScratch = null;
1430-
// don't try anything before `i`, because they've already been
1431-
// paired off with the other query elements
1432-
for (j = i; j !== fl; ++j) {
1433-
const fnType = fnTypes[j];
1434-
if (unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens)) {
1435-
if (!fnTypesScratch) {
1436-
fnTypesScratch = fnTypes.slice();
1430+
let mgensScratch;
1431+
if (fnType.id < 0) {
1432+
mgensScratch = new Map(mgens);
1433+
if (mgensScratch.has(fnType.id)
1434+
&& mgensScratch.get(fnType.id) !== queryElem.id) {
1435+
continue;
1436+
}
1437+
mgensScratch.set(fnType.id, queryElem.id);
1438+
} else {
1439+
mgensScratch = mgens;
1440+
}
1441+
// fnTypes[i] is a potential match
1442+
// fnTypes[flast] is the last item in the list
1443+
// swap them, and drop the potential match from the list
1444+
// check if the remaining function types also match
1445+
fnTypes[i] = fnTypes[flast];
1446+
fnTypes.length = flast;
1447+
if (!queryElemsTmp) {
1448+
queryElemsTmp = queryElems.slice(0, qlast);
1449+
}
1450+
const passesUnification = unifyFunctionTypes(
1451+
fnTypes,
1452+
queryElemsTmp,
1453+
whereClause,
1454+
mgensScratch,
1455+
mgensScratch => {
1456+
if (fnType.generics.length === 0 && queryElem.generics.length === 0) {
1457+
return !solutionCb || solutionCb(mgensScratch);
14371458
}
1438-
unifyFunctionTypes(
1459+
return unifyFunctionTypes(
14391460
fnType.generics,
14401461
queryElem.generics,
14411462
whereClause,
1442-
mgens,
1443-
mgensScratch => {
1444-
matchCandidates.push({
1445-
fnTypesScratch,
1446-
mgensScratch,
1447-
queryElemsOffset: i,
1448-
fnTypesOffset: j,
1449-
unbox: false,
1450-
});
1451-
return false; // "reject" all candidates to gather all of them
1452-
}
1453-
);
1454-
}
1455-
if (unifyFunctionTypeIsUnboxCandidate(fnType, queryElem, whereClause, mgens)) {
1456-
if (!fnTypesScratch) {
1457-
fnTypesScratch = fnTypes.slice();
1458-
}
1459-
if (!mgensScratch) {
1460-
mgensScratch = new Map(mgens);
1461-
}
1462-
backtracking.push({
1463-
fnTypesScratch,
14641463
mgensScratch,
1465-
queryElemsOffset: i,
1466-
fnTypesOffset: j,
1467-
unbox: true,
1468-
});
1464+
solutionCb
1465+
);
14691466
}
1467+
);
1468+
if (passesUnification) {
1469+
return true;
14701470
}
1471-
if (matchCandidates.length === 0) {
1472-
if (backtrack()) {
1473-
continue;
1474-
} else {
1475-
return false;
1476-
}
1471+
// backtrack
1472+
fnTypes[flast] = fnTypes[i];
1473+
fnTypes[i] = fnType;
1474+
fnTypes.length = fl;
1475+
}
1476+
for (let i = flast; i >= 0; i -= 1) {
1477+
const fnType = fnTypes[i];
1478+
if (!unifyFunctionTypeIsUnboxCandidate(fnType, queryElem, whereClause, mgens)) {
1479+
continue;
14771480
}
1478-
// use the current candidate
1479-
const {fnTypesOffset: candidate, mgensScratch: mgensNew} = matchCandidates.pop();
1480-
if (fnTypes[candidate].id < 0 && queryElems[i].id < 0) {
1481-
mgens.set(fnTypes[candidate].id, queryElems[i].id);
1482-
}
1483-
for (const [fid, qid] of mgensNew) {
1484-
mgens.set(fid, qid);
1485-
}
1486-
// `i` and `j` are paired off
1487-
// `queryElems[i]` is left in place
1488-
// `fnTypes[j]` is swapped with `fnTypes[i]` to pair them off
1489-
const tmp = fnTypes[candidate];
1490-
fnTypes[candidate] = fnTypes[i];
1491-
fnTypes[i] = tmp;
1492-
// write other candidates to backtracking queue
1493-
for (const otherCandidate of matchCandidates) {
1494-
backtracking.push(otherCandidate);
1495-
}
1496-
// If we're on the last item, check the solution with the callback
1497-
// backtrack if the callback says its unsuitable
1498-
while (i === (ql - 1) && solutionCb && !solutionCb(mgens)) {
1499-
if (!backtrack()) {
1500-
return false;
1481+
let mgensScratch;
1482+
if (fnType.id < 0) {
1483+
mgensScratch = new Map(mgens);
1484+
if (mgensScratch.has(fnType.id) && mgensScratch.get(fnType.id) !== 0) {
1485+
continue;
15011486
}
1487+
mgensScratch.set(fnType.id, 0);
1488+
} else {
1489+
mgensScratch = mgens;
1490+
}
1491+
const generics = fnType.id < 0 ?
1492+
whereClause[(-fnType.id) - 1] :
1493+
fnType.generics;
1494+
const passesUnification = unifyFunctionTypes(
1495+
fnTypes.toSpliced(i, 1, ...generics),
1496+
queryElems,
1497+
whereClause,
1498+
mgensScratch,
1499+
solutionCb
1500+
);
1501+
if (passesUnification) {
1502+
return true;
15021503
}
15031504
}
1504-
return true;
1505+
return false;
15051506
}
15061507
function unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens) {
15071508
// type filters look like `trait:Read` or `enum:Result`
@@ -1514,15 +1515,17 @@ function initSearch(rawSearchIndex) {
15141515
// or, if mgens[fnType.id] = 0, then we've matched this generic with a bare trait
15151516
// and should make that same decision everywhere it appears
15161517
if (fnType.id < 0 && queryElem.id < 0) {
1517-
if (mgens.has(fnType.id) && mgens.get(fnType.id) !== queryElem.id) {
1518-
return false;
1519-
}
1520-
for (const [fid, qid] of mgens.entries()) {
1521-
if (fnType.id !== fid && queryElem.id === qid) {
1518+
if (mgens !== null) {
1519+
if (mgens.has(fnType.id) && mgens.get(fnType.id) !== queryElem.id) {
15221520
return false;
15231521
}
1524-
if (fnType.id === fid && queryElem.id !== qid) {
1525-
return false;
1522+
for (const [fid, qid] of mgens.entries()) {
1523+
if (fnType.id !== fid && queryElem.id === qid) {
1524+
return false;
1525+
}
1526+
if (fnType.id === fid && queryElem.id !== qid) {
1527+
return false;
1528+
}
15261529
}
15271530
}
15281531
} else {
@@ -1575,7 +1578,7 @@ function initSearch(rawSearchIndex) {
15751578
}
15761579
// mgens[fnType.id] === 0 indicates that we committed to unboxing this generic
15771580
// mgens[fnType.id] === null indicates that we haven't decided yet
1578-
if (mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) {
1581+
if (mgens !== null && mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) {
15791582
return false;
15801583
}
15811584
// This is only a potential unbox if the search query appears in the where clause

tests/rustdoc-js/generics2.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// exact-check
2+
3+
const EXPECTED = [
4+
{
5+
'query': 'outside<U>, outside<V> -> outside<W>',
6+
'others': [],
7+
},
8+
{
9+
'query': 'outside<V>, outside<U> -> outside<W>',
10+
'others': [],
11+
},
12+
{
13+
'query': 'outside<U>, outside<U> -> outside<W>',
14+
'others': [],
15+
},
16+
{
17+
'query': 'outside<U>, outside<U> -> outside<U>',
18+
'others': [
19+
{"path": "generics2", "name": "should_match_3"}
20+
],
21+
},
22+
];

0 commit comments

Comments
 (0)