UNPKG

mc-benchmark

Version:

Build charts about load time of Minecraft modpack.

160 lines 6.78 kB
/** * @file Collect information about load time from Debug.log * and output it in .MD file * * @author Krutoy242 * @link https://github.com/Krutoy242 */ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import process from 'node:process'; import chalkWeak from 'chalk'; import { compose } from './hbs.js'; import logger from './log.js'; import { getFmlStuff, getJeiPlugins, getMcLoadTime, getMods, getTimeline, loaderSteps } from './parse.js'; const chalk = chalkWeak.constructor({ level: process.stderr.isTTY ? 3 : 0 }); //############################################################################ //############################################################################ function secondsToMinutes(sec) { const min = Math.floor(sec / 60); return `${min}:${String(Math.floor(sec) - min * 60).padStart(2, '0')}`; } export function sum(arr) { return arr.reduce((a, v) => a + v, 0); } export default async function parseDebugLog(_options) { const options = { mkdirSync, readFileSync, writeFileSync, ..._options, }; const log = options.defaultLogger ?? logger; async function loadText(fpath, onError) { await log.begin(`Opening file "${fpath}"`); let fileContent; try { fileContent = options.readFileSync(resolve(options.cwd ?? './', fpath), 'utf8'); } catch (err) { await onError(err, fpath); return undefined; } return fileContent; } function saveText(txt, filename) { options.mkdirSync(dirname(filename), { recursive: true }); options.writeFileSync(filename, txt); } //############################################################################ // Numbers const debug_log = await loadText(options.input, (err, fpath) => log.error(`Can't open file "${fpath}".\n${err}`)); if (!debug_log) return; const crafttweaker_log = await loadText(options.ctlog, (_err, fpath) => log.warn(`Can't open file "${fpath}". Use option "--ctlog=path/to/crafttweker.log"`)); const mods = await getMods(debug_log, crafttweaker_log, log); if (!Object.keys(mods)) { if (!debug_log.match(/\[main\/DEBUG\] \[FML\]/)) { return await log.error(`The file "${options.input}" does not contain` + ` rich debugging information. It is most likely not actual debug.log.` + `\n\nHint:\nSome MC launchers disable generating of debug.log file by default.` + ` Find out how to enable it.`); } else { return await log.error(`The file "${options.input}" not full.` + ` Your Minecraft not loaded completely or file is corrupted.`); } } const pie = []; for (const [name, mod] of Object.entries(mods).slice(0, options.detailed)) { const modSlice = { name, color: mod.color, time: sum(mod.steps) }; pie.push(modSlice); (mod.parts ?? []).forEach((part) => { pie.push(part); }); } const totalTimes = Object.values(mods).map(m => sum(m.steps)).slice(options.detailed); async function piePush(color, text, filter) { const otherMods = totalTimes.filter(filter); if (otherMods.length) { pie.push({ name: `${otherMods.length} ${text}`, color, time: sum(otherMods), }); } else { await log.info(`Cannot make pie section ` + `"${chalk.hex('007777')(text)}" since no mods found at all`); } } await piePush('444444', `Other mods`, t => t > 1.0); await piePush('333333', `'Fast' mods (1.0s - 0.1s)`, t => t >= 0.1 && t <= 1.0); await piePush('222222', `'Instant' mods (%3C 0.1s)`, t => t < 0.1); const mcLoadTime = getMcLoadTime(debug_log); const modsTime = sum(pie.map(o => o.time)); const fmlStuff = getFmlStuff(debug_log); const loaderStuffTime = mcLoadTime - modsTime; // Destinct "Other" section of chart const otherFmlStuffTime = loaderStuffTime - sum(fmlStuff.map(part => part.time)); if (otherFmlStuffTime > 0) fmlStuff.push({ color: '444444', name: 'Other', time: otherFmlStuffTime }); const timeline = getTimeline(debug_log); // Remove FML steps without time const filteredLoaderSteps = Object.entries(mods) .filter(([name]) => !name.match(/Just Enough Items|Had Enough Items/)); const sumLoaderSteps = columnSumm(filteredLoaderSteps.map(([, { steps }]) => steps)); const fmlStepFilter = sumLoaderSteps.map(n => n > 0); const removeUnusedSteps = (arr) => arr.filter((_, i) => fmlStepFilter[i]); filteredLoaderSteps.forEach(([, mod]) => mod.steps = removeUnusedSteps(mod.steps)); const data = { modpackName: options.modpack, mcLoadTime, mcLoadTimeMin: secondsToMinutes(mcLoadTime), loadingTimeline: timeline, modLoadingTime: pie, loaderStepNames: removeUnusedSteps(Object.keys(loaderSteps)), loaderSteps: Object.fromEntries(filteredLoaderSteps.map(([n, m]) => [n, m.steps]) .concat([[ '[Mod Average]', sumLoaderSteps .map(n => n / filteredLoaderSteps.filter(([, m]) => sum(m.steps)).length) .filter(Boolean), ]])), jeiPlugins: await getJeiPlugins(debug_log, log), fmlStuff: { total: loaderStuffTime, list: fmlStuff, }, }; await log.begin('Composing output'); if (options.data) { await log.begin('Writing file'); const dataJson = JSON.stringify(data, null, 2) // prettify numerical arrays // eslint-disable-next-line regexp/no-super-linear-backtracking .replace(/\[(\s*\d+(\.\d+)?,?\s+)+\]/g, m => m.replace(/\s+/g, '')); try { saveText(dataJson, options.data); } catch (error) { await log.error(`Can't save output file "${options.data}". Use option "--data=path/to/data.json"\n\n${error}`); } } let composed = await compose(data, log, options.template); if (options.nospaces) { composed = composed .replace(/(<img src="https:\/\/quickchart\.io\/chart.+?=\{)([\s\S]+?)(\}\s*"\s*\/\s*>)/g, (_, a, b, c) => a + b .replace(/\s*\n\s*/g, ' ') .replace(/\s+/g, '%20') + c); } process.stdout.write(composed); log.result(`Load Time total: ${mcLoadTime}`); } function columnSumm(arr) { return arr.reduce((r, a) => { a.forEach((b, i) => r[i] = (r[i] || 0) + b); return r; }, []); } //# sourceMappingURL=index.js.map