Skip to content

Commit

Permalink
optimize dev edit-refresh loop by caching the syntax highlighting (#1229
Browse files Browse the repository at this point in the history
)

Result is 11.3s edit-refresh time (from 16.7s edit-refresh time) on my M2 silicon mac.
Or, 34.4s edit-refresh time (from 57.3s ) on intel mac.

Co-authored-by: Augustine Kim <[email protected]>
  • Loading branch information
AndrewJakubowicz and augustjk authored Oct 19, 2023
1 parent 671bc83 commit eece220
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ test-results
*.tsbuildinfo

.wireit
.highlights_cache
1 change: 1 addition & 0 deletions packages/lit-dev-content/.eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ module.exports = function (eleventyConfig) {
eleventyConfig.addPlugin(eleventyNavigationPlugin);
eleventyConfig.addPlugin(playgroundPlugin, {
sandboxUrl: ENV.playgroundSandboxUrl,
isDevMode: DEV,
});
if (!DEV) {
// In dev mode, we symlink these directly to source.
Expand Down
101 changes: 100 additions & 1 deletion packages/lit-dev-tools-cjs/src/playground-plugin/blocking-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

import * as workerthreads from 'worker_threads';
import * as pathlib from 'path';
import * as fs from 'fs';

const cachedHighlightsDir = pathlib.resolve(
__dirname,
'../../.highlights_cache/'
);

export type WorkerMessage = HandshakeMessage | Render | Shutdown;

Expand All @@ -32,6 +38,38 @@ export interface Shutdown {
type: 'shutdown';
}

// Create a cache key for the highlighted strings. This is a
// simple digest build from a DJB2-ish hash modified from:
// https://github.com/darkskyapp/string-hash/blob/master/index.js
// This is modified from @lit-labs/ssr-client.
// Goals:
// - Extremely low collision rate. We may not be able to detect collisions.
// - Extremely fast.
// - Extremely small code size.
// - Safe to include in HTML comment text or attribute value.
// - Easily specifiable and implementable in multiple languages.
// We don't care about cryptographic suitability.
const digestToFileName = (stringToDigest: string) => {
// Number of 32 bit elements to use to create template digests
const digestSize = 5;
const hashes = new Uint32Array(digestSize).fill(5381);
for (let i = 0; i < stringToDigest.length; i++) {
hashes[i % digestSize] =
(hashes[i % digestSize] * 33) ^ stringToDigest.charCodeAt(i);
}
const str = String.fromCharCode(...new Uint8Array(hashes.buffer));
return (
Buffer.from(str, 'binary')
.toString('base64')
// These characters do not play well in file names. Replace with
// underscores.
.replace(/[<>:"'/\\|?*]/g, '_')
);
};

const createUniqueFileNameKey = (lang: string, code: string) =>
digestToFileName(`[${lang}]:${code}`);

export class BlockingRenderer {
/** Worker that performs rendering. */
private worker: workerthreads.Worker;
Expand All @@ -45,7 +83,20 @@ export class BlockingRenderer {
private exited = false;
private renderTimeout: number;

constructor({renderTimeout = 60_000, maxHtmlBytes = 1024 * 1024} = {}) {
/**
* Spawning a headless browser to syntax highlight code is expensive and slows
* down the edit/refresh loop during development. When developing, cache the
* syntax highlighted DOM in the filesystem so it can be retrieved if
* previously seen.
*/
private isDevMode = false;

constructor({
renderTimeout = 60_000,
maxHtmlBytes = 1024 * 1024,
isDevMode = false,
} = {}) {
this.isDevMode = isDevMode;
this.renderTimeout = renderTimeout;
this.sharedHtml = new Uint8Array(new SharedArrayBuffer(maxHtmlBytes));
this.worker = new workerthreads.Worker(
Expand All @@ -70,6 +121,15 @@ export class BlockingRenderer {
htmlBuffer: this.sharedHtml,
notify: this.sharedNotify,
});
try {
fs.mkdirSync(cachedHighlightsDir);
} catch (error) {
if ((error as {code: string}).code === 'EEXIST') {
// Directory already exists.
} else {
throw error;
}
}
}

async stop(): Promise<void> {
Expand All @@ -82,7 +142,46 @@ export class BlockingRenderer {
});
}

private getCachedRender(cachedFileName: string): string | null {
const absoluteFilePath = pathlib.resolve(
cachedHighlightsDir,
cachedFileName
);
if (fs.existsSync(absoluteFilePath)) {
return fs.readFileSync(absoluteFilePath, {encoding: 'utf8'});
}
return null;
}

private writeCachedRender(cachedFileName: string, html: string) {
const absoluteFilePath = pathlib.resolve(
cachedHighlightsDir,
cachedFileName
);
fs.writeFileSync(absoluteFilePath, html);
}

render(lang: 'js' | 'ts' | 'html' | 'css', code: string): {html: string} {
if (!this.isDevMode) {
// In production, skip all caching.
return this.renderWithWorker(lang, code);
}
// In dev mode, speed up the edit-refresh loop by caching the syntax
// highlighted code.
const cachedFileName = createUniqueFileNameKey(lang, code);
const cachedResult = this.getCachedRender(cachedFileName);
if (cachedResult !== null) {
return {html: cachedResult};
}
const {html} = this.renderWithWorker(lang, code);
this.writeCachedRender(cachedFileName, html);
return {html};
}

private renderWithWorker(
lang: 'js' | 'ts' | 'html' | 'css',
code: string
): {html: string} {
if (this.exited) {
throw new Error('BlockingRenderer worker has already exited');
}
Expand Down
4 changes: 2 additions & 2 deletions packages/lit-dev-tools-cjs/src/playground-plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ const countVisibleLines = (filename: string, code: string): number => {
*/
export const playgroundPlugin = (
eleventyConfig: EleventyConfig,
{sandboxUrl}: {sandboxUrl: string}
{sandboxUrl, isDevMode}: {sandboxUrl: string; isDevMode: boolean}
) => {
let renderer: BlockingRenderer | undefined;

eleventyConfig.on('eleventy.before', () => {
renderer = new BlockingRenderer();
renderer = new BlockingRenderer({isDevMode});
});

eleventyConfig.on('eleventy.after', async () => {
Expand Down

0 comments on commit eece220

Please sign in to comment.