From e61be1bfae982b991828d3edd64a0eaea8bc2d55 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Mon, 8 Jan 2024 12:00:40 -0700 Subject: [PATCH 1/2] rustdoc-search: reuse empty map/array in function signatures Map is implemented as a pointer to a mutable object. Rustdoc never mutates function signatures after constructing them, but the JS engine doesn't know that. To save a bunch of memory, use a single immutable map for every decoded type object with no bindings or generics. --- src/librustdoc/html/static/js/search.js | 26 ++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index a5e2bc1c7afd0..5bd375a86a787 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -2717,9 +2717,25 @@ ${item.displayPath}${name}\ * @return {Array} */ function buildItemSearchTypeAll(types, lowercasePaths) { - return types.map(type => buildItemSearchType(type, lowercasePaths)); + return types.length > 0 ? + types.map(type => buildItemSearchType(type, lowercasePaths)) : + EMPTY_GENERICS_ARRAY; } + /** + * Empty, immutable map used in item search types with no bindings. + * + * @type {Map>} + */ + const EMPTY_BINDINGS_MAP = new Map(); + + /** + * Empty, immutable map used in item search types with no bindings. + * + * @type {Array} + */ + const EMPTY_GENERICS_ARRAY = []; + /** * Converts a single type. * @@ -2732,15 +2748,15 @@ ${item.displayPath}${name}\ let pathIndex, generics, bindings; if (typeof type === "number") { pathIndex = type; - generics = []; - bindings = new Map(); + generics = EMPTY_GENERICS_ARRAY; + bindings = EMPTY_BINDINGS_MAP; } else { pathIndex = type[PATH_INDEX_DATA]; generics = buildItemSearchTypeAll( type[GENERICS_DATA], lowercasePaths ); - if (type.length > BINDINGS_DATA) { + if (type.length > BINDINGS_DATA && type[BINDINGS_DATA].length > 0) { bindings = new Map(type[BINDINGS_DATA].map(binding => { const [assocType, constraints] = binding; // Associated type constructors are represented sloppily in rustdoc's @@ -2759,7 +2775,7 @@ ${item.displayPath}${name}\ ]; })); } else { - bindings = new Map(); + bindings = EMPTY_BINDINGS_MAP; } } if (pathIndex < 0) { From c3cd657210403b9b8b0e8557bc1604d1890e6821 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Mon, 8 Jan 2024 14:29:55 -0700 Subject: [PATCH 2/2] rustdoc-search: intern function search types This takes advantage of more reuse opportunities. Along with the empty object commit, they bringing memory usage down about 20% over the original. --- src/librustdoc/html/static/js/search.js | 89 ++++++++++++++++++++----- 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index 5bd375a86a787..7995a33f09f9b 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -2725,17 +2725,25 @@ ${item.displayPath}${name}\ /** * Empty, immutable map used in item search types with no bindings. * - * @type {Map>} + * @type {Map>} */ const EMPTY_BINDINGS_MAP = new Map(); /** * Empty, immutable map used in item search types with no bindings. * - * @type {Array} + * @type {Array} */ const EMPTY_GENERICS_ARRAY = []; + /** + * Object pool for function types with no bindings or generics. + * This is reset after loading the index. + * + * @type {Map} + */ + let TYPES_POOL = new Map(); + /** * Converts a single type. * @@ -2778,35 +2786,80 @@ ${item.displayPath}${name}\ bindings = EMPTY_BINDINGS_MAP; } } + /** + * @type {FunctionType} + */ + let result; if (pathIndex < 0) { // types less than 0 are generic parameters // the actual names of generic parameters aren't stored, since they aren't API - return { + result = { id: pathIndex, ty: TY_GENERIC, path: null, generics, bindings, }; - } - if (pathIndex === 0) { + } else if (pathIndex === 0) { // `0` is used as a sentinel because it's fewer bytes than `null` - return { + result = { id: null, ty: null, path: null, generics, bindings, }; + } else { + const item = lowercasePaths[pathIndex - 1]; + result = { + id: buildTypeMapIndex(item.name, isAssocType), + ty: item.ty, + path: item.path, + generics, + bindings, + }; } - const item = lowercasePaths[pathIndex - 1]; - return { - id: buildTypeMapIndex(item.name, isAssocType), - ty: item.ty, - path: item.path, - generics, - bindings, - }; + const cr = TYPES_POOL.get(result.id); + if (cr) { + // Shallow equality check. Since this function is used + // to construct every type object, this should be mostly + // equivalent to a deep equality check, except if there's + // a conflict, we don't keep the old one around, so it's + // not a fully precise implementation of hashcons. + if (cr.generics.length === result.generics.length && + cr.generics !== result.generics && + cr.generics.every((x, i) => result.generics[i] === x) + ) { + result.generics = cr.generics; + } + if (cr.bindings.size === result.bindings.size && cr.bindings !== result.bindings) { + let ok = true; + for (const [k, v] of cr.bindings.entries()) { + const v2 = result.bindings.get(v); + if (!v2) { + ok = false; + break; + } + if (v !== v2 && v.length === v2.length && v.every((x, i) => v2[i] === x)) { + result.bindings.set(k, v); + } else if (v !== v2) { + ok = false; + break; + } + } + if (ok) { + result.bindings = cr.bindings; + } + } + if (cr.ty === result.ty && cr.path === result.path + && cr.bindings === result.bindings && cr.generics === result.generics + && cr.ty === result.ty + ) { + return cr; + } + } + TYPES_POOL.set(result.id, result); + return result; } /** @@ -2817,7 +2870,7 @@ ${item.displayPath}${name}\ * object-based encoding so that the actual search code is more readable and easier to debug. * * The raw function search type format is generated using serde in - * librustdoc/html/render/mod.rs: impl Serialize for IndexItemFunctionType + * librustdoc/html/render/mod.rs: IndexItemFunctionType::write_to_string * * @param {{ * string: string, @@ -2986,8 +3039,8 @@ ${item.displayPath}${name}\ const fb = { id: null, ty: 0, - generics: [], - bindings: new Map(), + generics: EMPTY_GENERICS_ARRAY, + bindings: EMPTY_BINDINGS_MAP, }; for (const [k, v] of type.bindings.entries()) { fb.id = k; @@ -3215,6 +3268,8 @@ ${item.displayPath}${name}\ } currentIndex += itemTypes.length; } + // Drop the (rather large) hash table used for reusing function items + TYPES_POOL = new Map(); } /**