Skip to content

Commit

Permalink
Compress search index too
Browse files Browse the repository at this point in the history
Also write search index, write navigation index, and load highlighter
in parallel, which seems to save ~80ms when running typedoc on itself
  • Loading branch information
Gerrit0 committed Sep 4, 2023
1 parent d68ca2a commit 1f88a1f
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 61 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
to customize the displayed navigation tree, #2287.
Note: This change renders `navigation.fullTree` obsolete. If you set it, TypeDoc will warn that it is being ignored.
It will be removed in v0.26.
- The search index is now compressed before writing, which reduces most search index sizes by ~5-10x.
- TypeDoc will now attempt to cache icons when `DefaultThemeRenderContext.icons` is overwritten by a custom theme.
Note: To perform this optimization, TypeDoc relies on `DefaultThemeRenderContext.iconCache` being rendered within
each page. TypeDoc does it in the `defaultLayout` template.
Expand Down
23 changes: 19 additions & 4 deletions src/lib/output/plugins/JavascriptIndexPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import {
} from "../../models";
import { Component, RendererComponent } from "../components";
import { IndexEvent, RendererEvent } from "../events";
import { Option, writeFileSync } from "../../utils";
import { Option, writeFile } from "../../utils";
import { DefaultTheme } from "../themes/default/DefaultTheme";
import { gzip } from "zlib";
import { promisify } from "util";

const gzipP = promisify(gzip);

/**
* Keep this in sync with the interface in src/lib/output/themes/default/assets/typedoc/components/Search.ts
Expand Down Expand Up @@ -52,6 +56,14 @@ export class JavascriptIndexPlugin extends RendererComponent {
return;
}

this.owner.preRenderAsyncJobs.push((event) =>
this.buildSearchIndex(event),
);
}

private async buildSearchIndex(event: RendererEvent) {
const theme = this.owner.theme as DefaultTheme;

const rows: SearchDocument[] = [];

const initialSearchResults = Object.values(
Expand Down Expand Up @@ -105,7 +117,7 @@ export class JavascriptIndexPlugin extends RendererComponent {
kind: reflection.kind,
name: reflection.name,
url: reflection.url,
classes: this.owner.theme.getReflectionClasses(reflection),
classes: theme.getReflectionClasses(reflection),
};

if (parent) {
Expand Down Expand Up @@ -136,10 +148,13 @@ export class JavascriptIndexPlugin extends RendererComponent {
rows,
index,
});
const data = await gzipP(Buffer.from(jsonData));

writeFileSync(
await writeFile(
jsonFileName,
`window.searchData = JSON.parse(${JSON.stringify(jsonData)});`,
`window.searchData = "data:application/octet-stream;base64,${data.toString(
"base64",
)}";`,
);
}

Expand Down
21 changes: 16 additions & 5 deletions src/lib/output/plugins/NavigationPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as Path from "path";
import { Component, RendererComponent } from "../components";
import { RendererEvent } from "../events";
import { writeFileSync } from "../../utils";
import { writeFile } from "../../utils";
import { DefaultTheme } from "../themes/default/DefaultTheme";
import { gzipSync } from "zlib";
import { gzip } from "zlib";
import { promisify } from "util";

const gzipP = promisify(gzip);

@Component({ name: "navigation-tree" })
export class NavigationPlugin extends RendererComponent {
Expand All @@ -19,16 +22,24 @@ export class NavigationPlugin extends RendererComponent {
return;
}

this.owner.preRenderAsyncJobs.push((event) =>
this.buildNavigationIndex(event),
);
}

private async buildNavigationIndex(event: RendererEvent) {
const navigationJs = Path.join(
event.outputDirectory,
"assets",
"navigation.js",
);

const nav = this.owner.theme.getNavigation(event.project);
const gz = gzipSync(Buffer.from(JSON.stringify(nav)));
const nav = (this.owner.theme as DefaultTheme).getNavigation(
event.project,
);
const gz = await gzipP(Buffer.from(JSON.stringify(nav)));

writeFileSync(
await writeFile(
navigationJs,
`window.navigationData = "data:application/octet-stream;base64,${gz.toString(
"base64",
Expand Down
27 changes: 18 additions & 9 deletions src/lib/output/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,7 @@ export class Renderer extends ChildableComponent<

const momento = this.hooks.saveMomento();
this.renderStartTime = Date.now();
await loadHighlighter(this.lightTheme, this.darkTheme);
this.application.logger.verbose(
`Renderer: Loading highlighter took ${
Date.now() - this.renderStartTime
}ms`,
);

if (
!this.prepareTheme() ||
!(await this.prepareOutputDirectory(outputDirectory))
Expand All @@ -256,9 +251,7 @@ export class Renderer extends ChildableComponent<
output.urls = this.theme!.getUrls(project);

this.trigger(output);

await Promise.all(this.preRenderAsyncJobs.map((job) => job(output)));
this.preRenderAsyncJobs = [];
await this.runPreRenderJobs(output);

if (!output.isDefaultPrevented) {
this.application.logger.verbose(
Expand All @@ -281,6 +274,22 @@ export class Renderer extends ChildableComponent<
this.hooks.restoreMomento(momento);
}

private async runPreRenderJobs(output: RendererEvent) {
const start = Date.now();

this.preRenderAsyncJobs.push(this.loadHighlighter.bind(this));
await Promise.all(this.preRenderAsyncJobs.map((job) => job(output)));
this.preRenderAsyncJobs = [];

this.application.logger.verbose(
`Pre render async jobs took ${Date.now() - start}ms`,
);
}

private async loadHighlighter() {
await loadHighlighter(this.lightTheme, this.darkTheme);
}

/**
* Render a single page.
*
Expand Down
54 changes: 27 additions & 27 deletions src/lib/output/themes/default/assets/typedoc/components/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface IData {

declare global {
interface Window {
searchData?: IData;
searchData?: string;
}
}

Expand All @@ -32,10 +32,30 @@ interface SearchState {
index?: Index;
}

async function updateIndex(state: SearchState, searchEl: HTMLElement) {
if (!window.searchData) return;

const res = await fetch(window.searchData);
const json = new Blob([await res.arrayBuffer()])
.stream()
.pipeThrough(new DecompressionStream("gzip"));
const data: IData = await new Response(json).json();

state.data = data;
state.index = Index.load(data.index);

searchEl.classList.remove("loading");
searchEl.classList.add("ready");
}

export function initSearch() {
const searchEl = document.getElementById("tsd-search");
if (!searchEl) return;

const state: SearchState = {
base: searchEl.dataset["base"] + "/",
};

const searchScript = document.getElementById(
"tsd-search-script",
) as HTMLScriptElement | null;
Expand All @@ -46,12 +66,9 @@ export function initSearch() {
searchEl.classList.add("failure");
});
searchScript.addEventListener("load", () => {
searchEl.classList.remove("loading");
searchEl.classList.add("ready");
updateIndex(state, searchEl);
});
if (window.searchData) {
searchEl.classList.remove("loading");
}
updateIndex(state, searchEl);
}

const field = document.querySelector<HTMLInputElement>("#tsd-search input");
Expand All @@ -78,10 +95,6 @@ export function initSearch() {
}
});

const state: SearchState = {
base: searchEl.dataset["base"] + "/",
};

bindEvents(searchEl, results, field, state);
}

Expand Down Expand Up @@ -129,24 +142,12 @@ function bindEvents(
});
}

function checkIndex(state: SearchState, searchEl: HTMLElement) {
if (state.index) return;

if (window.searchData) {
searchEl.classList.remove("loading");
searchEl.classList.add("ready");
state.data = window.searchData;
state.index = Index.load(window.searchData.index);
}
}

function updateResults(
searchEl: HTMLElement,
results: HTMLElement,
query: HTMLInputElement,
state: SearchState,
) {
checkIndex(state, searchEl);
// Don't clear results if loading state is not ready,
// because loading or error message can be removed.
if (!state.index || !state.data) return;
Expand Down Expand Up @@ -189,25 +190,24 @@ function updateResults(

for (let i = 0, c = Math.min(10, res.length); i < c; i++) {
const row = state.data.rows[Number(res[i].ref)];
const icon = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" class="tsd-kind-icon"><use href="#icon-${row.kind}"></use></svg>`;

// Bold the matched part of the query in the search results
let name = boldMatches(row.name, searchText);
if (globalThis.DEBUG_SEARCH_WEIGHTS) {
name += ` (score: ${res[i].score.toFixed(2)})`;
}
if (row.parent) {
name = `<span class="parent">${boldMatches(
row.parent,
searchText,
)}.</span>${name}`;
name = `<span class="parent">
${boldMatches(row.parent, searchText)}.</span>${name}`;
}

const item = document.createElement("li");
item.classList.value = row.classes ?? "";

const anchor = document.createElement("a");
anchor.href = state.base + row.url;
anchor.innerHTML = name;
anchor.innerHTML = icon + name;
item.append(anchor);

results.appendChild(item);
Expand Down
15 changes: 1 addition & 14 deletions src/lib/utils/perf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,8 @@ export function Bench<T extends Function>(
};
}

const anon = { name: "measure()", calls: 0, time: 0 };
export function measure<T>(cb: () => T): T {
if (anon.calls === 0) {
benchmarks.unshift(anon);
}

anon.calls++;
const start = performance.now();
let result: T;
try {
result = cb();
} finally {
anon.time += performance.now() - start;
}
return result;
return bench(cb, "measure()")();
}

process.on("exit", () => {
Expand Down
8 changes: 6 additions & 2 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -910,8 +910,9 @@ a.tsd-index-link {
box-shadow: 0 0 4px rgba(0, 0, 0, 0.25);
}
#tsd-search .results li {
padding: 0 10px;
background-color: var(--color-background);
line-height: initial;
padding: 4px;
}
#tsd-search .results li:nth-child(even) {
background-color: var(--color-background-secondary);
Expand All @@ -924,7 +925,10 @@ a.tsd-index-link {
background-color: var(--color-accent);
}
#tsd-search .results a {
display: block;
display: flex;
align-items: center;
padding: 0.25rem;
box-sizing: border-box;
}
#tsd-search .results a:before {
top: 10px;
Expand Down

0 comments on commit 1f88a1f

Please sign in to comment.