You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
5.1 KiB
TypeScript

import { log } from './log';
import { performance } from 'perf_hooks';
import { normalizePath } from 'vite';
import { VitePluginSvelteCache } from './vite-plugin-svelte-cache';
interface Stat {
file: string;
pkg?: string;
start: number;
end: number;
}
export interface StatCollection {
name: string;
options: CollectionOptions;
//eslint-disable-next-line no-unused-vars
start: (file: string) => () => void;
stats: Stat[];
packageStats?: PackageStats[];
collectionStart: number;
duration?: number;
finish: () => Promise<void> | void;
finished: boolean;
}
interface PackageStats {
pkg: string;
files: number;
duration: number;
}
export interface CollectionOptions {
//eslint-disable-next-line no-unused-vars
logInProgress: (collection: StatCollection, now: number) => boolean;
//eslint-disable-next-line no-unused-vars
logResult: (collection: StatCollection) => boolean;
}
const defaultCollectionOptions: CollectionOptions = {
// log after 500ms and more than one file processed
logInProgress: (c, now) => now - c.collectionStart > 500 && c.stats.length > 1,
// always log results
logResult: () => true
};
function humanDuration(n: number) {
// 99.9ms 0.10s
return n < 100 ? `${n.toFixed(1)}ms` : `${(n / 1000).toFixed(2)}s`;
}
function formatPackageStats(pkgStats: PackageStats[]) {
const statLines = pkgStats.map((pkgStat) => {
const duration = pkgStat.duration;
const avg = duration / pkgStat.files;
return [pkgStat.pkg, `${pkgStat.files}`, humanDuration(duration), humanDuration(avg)];
});
statLines.unshift(['package', 'files', 'time', 'avg']);
const columnWidths = statLines.reduce(
(widths: number[], row) => {
for (let i = 0; i < row.length; i++) {
const cell = row[i];
if (widths[i] < cell.length) {
widths[i] = cell.length;
}
}
return widths;
},
statLines[0].map(() => 0)
);
const table = statLines
.map((row: string[]) =>
row
.map((cell: string, i: number) => {
if (i === 0) {
return cell.padEnd(columnWidths[i], ' ');
} else {
return cell.padStart(columnWidths[i], ' ');
}
})
.join('\t')
)
.join('\n');
return table;
}
export class VitePluginSvelteStats {
// package directory -> package name
private _cache: VitePluginSvelteCache;
private _collections: StatCollection[] = [];
constructor(cache: VitePluginSvelteCache) {
this._cache = cache;
}
startCollection(name: string, opts?: Partial<CollectionOptions>) {
const options = {
...defaultCollectionOptions,
...opts
};
const stats: Stat[] = [];
const collectionStart = performance.now();
const _this = this;
let hasLoggedProgress = false;
const collection: StatCollection = {
name,
options,
stats,
collectionStart,
finished: false,
start(file) {
if (collection.finished) {
throw new Error('called after finish() has been used');
}
file = normalizePath(file);
const start = performance.now();
const stat: Stat = { file, start, end: start };
return () => {
const now = performance.now();
stat.end = now;
stats.push(stat);
if (!hasLoggedProgress && options.logInProgress(collection, now)) {
hasLoggedProgress = true;
log.debug(`${name} in progress ...`, undefined, 'stats');
}
};
},
async finish() {
await _this._finish(collection);
}
};
_this._collections.push(collection);
return collection;
}
public async finishAll() {
await Promise.all(this._collections.map((c) => c.finish()));
}
private async _finish(collection: StatCollection) {
try {
collection.finished = true;
const now = performance.now();
collection.duration = now - collection.collectionStart;
const logResult = collection.options.logResult(collection);
if (logResult) {
await this._aggregateStatsResult(collection);
log.debug(
`${collection.name} done.\n${formatPackageStats(collection.packageStats!)}`,
undefined,
'stats'
);
}
// cut some ties to free it for garbage collection
const index = this._collections.indexOf(collection);
this._collections.splice(index, 1);
collection.stats.length = 0;
collection.stats = [];
if (collection.packageStats) {
collection.packageStats.length = 0;
collection.packageStats = [];
}
collection.start = () => () => {};
collection.finish = () => {};
} catch (e) {
// this should not happen, but stats taking also should not break the process
log.debug.once(`failed to finish stats for ${collection.name}\n`, e, 'stats');
}
}
private async _aggregateStatsResult(collection: StatCollection) {
const stats = collection.stats;
for (const stat of stats) {
stat.pkg = (await this._cache.getPackageInfo(stat.file)).name;
}
// group stats
const grouped: { [key: string]: PackageStats } = {};
stats.forEach((stat) => {
const pkg = stat.pkg!;
let group = grouped[pkg];
if (!group) {
group = grouped[pkg] = {
files: 0,
duration: 0,
pkg
};
}
group.files += 1;
group.duration += stat.end - stat.start;
});
const groups = Object.values(grouped);
groups.sort((a, b) => b.duration - a.duration);
collection.packageStats = groups;
}
}