UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

236 lines (235 loc) • 6.91 kB
import { AsyncLocalStorage } from "node:async_hooks"; export const ACTIVITY_BUCKETS = 22; // Comfortably more than the report prints, so the cap never decides what is shown. const MAX_ROWS = 50; /** Wall-clock time covered by possibly overlapping intervals, which summing would count twice. */ class IntervalUnion { intervals = []; add(start, end) { this.intervals.push([start, end]); } merged() { const sorted = [...this.intervals].sort((a, b) => a[0] - b[0]); const merged = []; for (const [start, end] of sorted) { const last = merged.at(-1); if (last && start <= last[1]) { last[1] = Math.max(last[1], end); } else { merged.push([start, end]); } } return merged; } get duration() { return this.merged().reduce((total, [start, end]) => total + (end - start), 0); } activity(window) { const slots = new Array(ACTIVITY_BUCKETS).fill(0); const width = window / ACTIVITY_BUCKETS; if (width <= 0) return slots; for (const [start, end] of this.merged()) { const first = Math.max(0, Math.floor(start / width)); const last = Math.min(ACTIVITY_BUCKETS - 1, Math.floor(end / width)); for (let slot = first; slot <= last; slot++) { const overlap = Math.min(end, (slot + 1) * width) - Math.max(start, slot * width); if (overlap > 0) slots[slot] += overlap / width; } } return slots.map((value) => Math.min(value, 1)); } } class Tally { calls = 0; wall = new IntervalUnion(); add(start, end) { this.calls++; this.wall.add(start, end); } toStat(name, window) { return { name, total: this.wall.duration, calls: this.calls, activity: this.wall.activity(window) }; } } class GroupedTally extends Tally { children = new Map(); addChild(child, start, end) { this.add(start, end); tallyOf(this.children, child, Tally).add(start, end); } toGroupedStat(name, window) { return { ...this.toStat(name, window), children: toSortedStats(this.children, window) }; } } function tallyOf(map, key, Kind) { let tally = map.get(key); if (!tally) { tally = new Kind(); map.set(key, tally); } return tally; } function toSortedStats(map, window) { return [...map].map(([name, tally]) => tally.toStat(name, window)).sort((a, b) => b.total - a.total); } function toSortedGroupedStats(map, window) { return [...map].map(([name, tally]) => tally.toGroupedStat(name, window)).sort((a, b) => b.total - a.total); } function section(entries, wall) { return { entries: entries.slice(0, MAX_ROWS), count: entries.length, wallDuration: wall.duration }; } function byDuration(records) { return [...records].sort((a, b) => b.duration - a.duration); } function percentile(sorted, fraction) { if (sorted.length === 0) return 0; return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * fraction))]; } export class BuildTimingsCollector { origin = performance.now(); spans = []; currentSpan = new AsyncLocalStorage(); plugins = new Map(); modules = new Map(); integrations = new Map(); markdownPlugins = new Map(); mdxPlugins = new Map(); languages = new Map(); pages = []; images = []; compiledFiles = []; markdownFiles = 0; pluginWall = new IntervalUnion(); moduleWall = new IntervalUnion(); pageWall = new IntervalUnion(); pageRenderWall = new IntervalUnion(); imageWall = new IntervalUnion(); markdownWall = new IntervalUnion(); compilerWall = new IntervalUnion(); highlightWall = new IntervalUnion(); async span(name, fn) { const span = { id: this.spans.length, parentId: this.currentSpan.getStore() ?? null, name, start: performance.now() - this.origin, duration: 0 }; this.spans.push(span); try { return await this.currentSpan.run(span.id, fn); } finally { span.duration = performance.now() - this.origin - span.start; } } record(kind, name, duration, meta) { const end = performance.now() - this.origin; const start = end - duration; switch (kind) { case "vite-hook": this.pluginWall.add(start, end); tallyOf(this.plugins, name, GroupedTally).addChild(meta?.hook, start, end); if (meta?.module) { this.moduleWall.add(start, end); tallyOf(this.modules, meta.module, GroupedTally).addChild(name, start, end); } break; case "integration": tallyOf(this.integrations, name, GroupedTally).addChild(meta?.hook, start, end); break; case "page": this.pageWall.add(start, end); this.pages.push({ name, route: meta?.route, duration, cached: Boolean(meta?.cached) }); break; case "page-render": this.pageRenderWall.add(start, end); break; case "image": this.imageWall.add(start, end); this.images.push({ name, duration, cached: Boolean(meta?.cached) }); break; case "markdown-plugin": tallyOf(this.markdownPlugins, name, Tally).add(start, end); break; case "mdx-plugin": tallyOf(this.mdxPlugins, name, Tally).add(start, end); break; case "markdown-file": this.markdownWall.add(start, end); this.markdownFiles++; break; case "highlight": this.highlightWall.add(start, end); tallyOf(this.languages, name, Tally).add(start, end); break; case "astro-compile": this.compilerWall.add(start, end); this.compiledFiles.push({ name, duration }); break; } } toData(context) { const window = performance.now() - this.origin; const sortedPageDurations = this.pages.map((page) => page.duration).sort((a, b) => a - b); const languages = toSortedStats(this.languages, window); return { ...context, totalDuration: window, spans: this.spans, plugins: section(toSortedGroupedStats(this.plugins, window), this.pluginWall), modules: section(toSortedGroupedStats(this.modules, window), this.moduleWall), integrations: toSortedGroupedStats(this.integrations, window), pages: { ...section(byDuration(this.pages), this.pageWall), cached: this.pages.filter((page) => page.cached).length, median: percentile(sortedPageDurations, .5), p95: percentile(sortedPageDurations, .95), renderDuration: this.pageRenderWall.duration, renderActivity: this.pageRenderWall.activity(window) }, images: { ...section(byDuration(this.images), this.imageWall), cached: this.images.filter((image) => image.cached).length }, markdown: { plugins: [...toSortedStats(this.markdownPlugins, window).map((stat) => ({ ...stat, pipeline: "markdown" })), ...toSortedStats(this.mdxPlugins, window).map((stat) => ({ ...stat, pipeline: "mdx" }))].sort((a, b) => b.total - a.total), files: this.markdownFiles, wallDuration: this.markdownWall.duration }, highlight: { ...section(languages, this.highlightWall), blocks: languages.reduce((sum, language) => sum + language.calls, 0) }, compiler: section(byDuration(this.compiledFiles), this.compilerWall) }; } }