perf: optimize render cache

This commit is contained in:
pengzhanbo 2024-06-26 00:43:47 +08:00
parent babd6114c3
commit 787626e2e4

View File

@ -12,45 +12,46 @@
* vuepress shiki 0.5s
*/
import { createHash } from 'node:crypto'
import process from 'node:process'
import { fs, path } from 'vuepress/utils'
import type { App } from 'vuepress'
import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
import { fs } from 'vuepress/utils'
interface CacheContent {
export interface CacheData {
content: string
env: MarkdownEnv
}
const cacheDir = 'markdown/render'
const metaFile = '_metadata.json'
// { [filepath]: CacheDta }
export type Cache = Record<string, CacheData>
// { [filepath]: hash }
export type Metadata = Record<string, string>
const CACHE_DIR = 'markdown/rendered'
const META_FILE = '_metadata.json'
export async function extendsMarkdown(md: Markdown, app: App): Promise<void> {
// 如果是在 构建阶段,且缓存文件夹不存在,则不进行缓存
// 因为构建阶段仅一次性产物,生成缓存资源反而会带来额外的开销
if (app.env.isBuild && !fs.existsSync(app.dir.cache())) {
if (app.env.isBuild && !fs.existsSync(app.dir.cache(CACHE_DIR))) {
return
}
const basename = app.dir.cache(CACHE_DIR)
await fs.ensureDir(app.dir.cache(cacheDir))
const metadata = await readMetadata(app)
await fs.ensureDir(basename)
const writeCache = (filepath: string, cache: CacheContent) => {
const cachePath = app.dir.cache(cacheDir, filepath)
const content = JSON.stringify(cache)
fs.writeFileSync(cachePath, content, 'utf-8')
const speed = checkIOSpeed(basename)
const metaFilepath = `${basename}/${META_FILE}`
const metadata = (await readFile<Metadata>(metaFilepath)) || {}
let timer: ReturnType<typeof setTimeout> | null = null
const update = (filepath: string, data: CacheData): void => {
writeFile(`${basename}/${filepath}`, data)
timer && clearTimeout(timer)
timer = setTimeout(async () => writeFile(metaFilepath, metadata), 200)
}
const readCache = (filepath: string): CacheContent | null => {
const cachePath = app.dir.cache(cacheDir, filepath)
try {
const content = fs.readFileSync(cachePath, 'utf-8')
return JSON.parse(content) as CacheContent
}
catch {}
return null
}
const rawRender = md.render
md.render = (input, env: MarkdownEnv) => {
const filepath = env.filePathRelative
@ -59,53 +60,77 @@ export async function extendsMarkdown(md: Markdown, app: App): Promise<void> {
return rawRender(input, env)
}
const hash = getContentHash(input)
const cachePath = normalizePath(filepath)
const key = hash(input)
const filename = normalizeFilename(filepath)
if (metadata[filepath] === hash) {
const cache = readCache(cachePath)
if (cache) {
Object.assign(env, cache.env)
return cache.content
if (metadata[filepath] === key) {
const cached = readFileSync<CacheData>(`${basename}/${filename}`)
if (cached) {
Object.assign(env, cached.env)
return cached.content
}
else {
metadata[filepath] = ''
}
}
const start = performance.now()
const content = rawRender(input, env)
metadata[filepath] = hash
const renderedContent = rawRender(input, env)
writeCache(cachePath, { content: renderedContent, env })
updateMetadata(app, metadata)
return renderedContent
/**
* High-frequency I/O is also a time-consuming operation,
* therefore, for render operations with low overhead, caching is not performed.
*/
if (performance.now() - start > speed) {
metadata[filepath] = key
update(filename, { content, env })
}
return content
}
}
async function readMetadata(app: App): Promise<Record<string, string>> {
const filepath = app.dir.cache(cacheDir, metaFile)
function hash(data: string): string {
return createHash('md5').update(data).digest('hex')
}
function normalizeFilename(filename: string): string {
return hash(filename).slice(0, 10)
}
async function readFile<T = any>(filepath: string): Promise<T | null> {
try {
const content = await fs.readFile(filepath, 'utf-8')
return JSON.parse(content)
return JSON.parse(content) as T
}
catch {
return null
}
catch {}
return {}
}
let timer: NodeJS.Timeout | null = null
function updateMetadata(app: App, metadata: Record<string, string>) {
const filepath = app.dir.cache(cacheDir, metaFile)
timer && clearTimeout(timer)
timer = setTimeout(
async () => await fs.writeFile(filepath, JSON.stringify(metadata), 'utf-8'),
200,
)
function readFileSync<T = any>(filepath: string): T | null {
try {
const content = fs.readFileSync(filepath, 'utf-8')
return JSON.parse(content) as T
}
catch {
return null
}
}
function normalizePath(filepath: string) {
return getContentHash(filepath)
async function writeFile<T = any>(filepath: string, data: T): Promise<void> {
return await fs.writeFile(filepath, JSON.stringify(data), 'utf-8')
}
function getContentHash(content: string): string {
const hash = createHash('md5')
hash.update(content)
return hash.digest('hex')
export function checkIOSpeed(cwd = process.cwd()): number {
try {
const tmp = path.join(cwd, 'tmp')
fs.writeFileSync(tmp, '{}', 'utf-8')
const start = performance.now()
readFileSync(tmp)
const end = performance.now()
fs.unlinkSync(tmp)
return end - start
}
catch {
return 0.15
}
}