| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- /**
- * Converts a time string in seconds to an integer number of milliseconds.
- *
- * @param {string} timeStr - The time duration in seconds, represented as a string.
- * @returns {number} The equivalent time in milliseconds, rounded down to the nearest integer.
- */
- function convertToMs(timeStr) {
- return Math.floor(parseFloat(timeStr, 10) * 1000)
- }
- const NOTEWORTHY_DEPENDENCIES_REGEXP =
- /\/(beamer\.cls|tikz\.sty|microtype\.sty|minted\.sty)$/
- /* An array of metric parsers for `latexmk` time output (`-time` flag).
- * Each entry is a tuple containing a metric name and a function to parse that
- * metric from the `latexmk` log output.
- *
- * The parser functions generally take the log string as their first argument.
- * Some may take additional arguments, such as the already computed stats,
- * to derive new metrics.
- *
- * There are different formats of `latexmk` output depending on the version.
- * The parsers attempt to handle these variations gracefully.
- */
- const LATEX_MK_METRICS_STDOUT = [
- // Extract individual latexmk rule times as an array of objects, each with 'rule'
- // and 'time_ms' properties
- [
- 'latexmk-rule-times',
- s => {
- // Each line looks like: 'pdflatex ... options ...': time = 12.34
- // Take the command up to the first space as the rule name
- const matches = s.matchAll(/^'([^' ]+).*': time = (\d+\.\d+)/gm)
- return Array.from(matches, match => ({
- rule: match[1],
- time_ms: convertToMs(match[2]),
- }))
- },
- ],
- // Extract a comma-separated signature of rule names from the rule times above
- [
- 'latexmk-rule-signature',
- (s, latexmkStats) => {
- const times = latexmkStats['latexmk-rule-times']
- if (!times) return null
- // Example output: 'pdflatex,bibtex,pdflatex,pdflatex'
- return times.map(t => t.rule).join(',')
- },
- ],
- // Total latexmk processing time, invoked processes time, and other time in ms
- [
- 'latexmk-time',
- s => {
- const match = s.match(
- /^Processing time = (\d+\.\d+), of which invoked processes = (\d+\.\d+), other = (\d+\.\d+)/m
- )
- if (match) {
- return {
- total: convertToMs(match[1]),
- invoked: convertToMs(match[2]),
- other: convertToMs(match[3]),
- }
- }
- // Older format
- const fallbackMatch = s.match(
- /^Accumulated processing time = (\d+\.\d+)/m
- )
- if (fallbackMatch) {
- return { total: convertToMs(fallbackMatch[1]) }
- }
- return null
- },
- ],
- // Total elapsed clock time in ms for latexmk
- [
- 'latexmk-clock-time',
- s => {
- const match = s.match(/^Elapsed clock time = (\d+\.\d+)/m)
- if (match) {
- return convertToMs(match[1])
- }
- // not present in older versions
- return null
- },
- ],
- // Number of rules run by latexmk
- [
- 'latexmk-rules-run',
- (s, latexmkStats) => {
- const match = s.match(/^Number of rules run = (\d+)/m)
- if (match) {
- return parseInt(match[1], 10)
- }
- // Fallback: count number of entries in rule times if available
- if (latexmkStats['latexmk-rule-times']) {
- return latexmkStats['latexmk-rule-times'].length
- }
- return null
- },
- ],
- ]
- const LATEX_MK_METRICS_STDERR = [
- [
- 'latexmk-img-times',
- s => {
- const pngCategoriesByFile = new Map()
- const pngCopyMatches = s.matchAll(/^PNG copy: (.*)$/gm)
- for (const match of pngCopyMatches) {
- const filename = match[1]
- pngCategoriesByFile.set(filename, 'fast-copy')
- }
- const pngCopySkipMatches = s.matchAll(
- /^PNG copy skipped \((alpha|gamma|palette|interlaced)\): (.*)$/gm
- )
- for (const match of pngCopySkipMatches) {
- const category = match[1]
- const filename = match[2]
- pngCategoriesByFile.set(filename, category)
- }
- const timingMatches = s.matchAll(
- /^Image written \((PNG|JPG|JBIG2|PDF), (\d+) ms\): (.*)$/gm
- )
- const timingsByType = new Map()
- for (const match of timingMatches) {
- let type = match[1]
- const timeMs = parseInt(match[2], 10)
- const filename = match[3]
- if (type === 'PNG') {
- const pngCategory = pngCategoriesByFile.get(filename)
- if (pngCategory != null) {
- type = `PNG-${pngCategory}`
- }
- }
- const accumulatedTime = timingsByType.get(type) ?? 0
- timingsByType.set(type, accumulatedTime + timeMs)
- }
- return Array.from(timingsByType.entries()).map(([type, timeMs]) => ({
- type,
- time_ms: timeMs,
- }))
- },
- ],
- ]
- /**
- * Parses latexmk stdout for metrics and adds them to the stats object.
- * It iterates through a predefined list of metric matchers (LATEX_MK_METRICS),
- * applies them to the stdout, and adds any successful matches to the
- * `stats.latexmk` object.
- *
- * @param {{stdout?: string, stderr?: string}} output - The output from the latexmk process.
- * @param {{latexmk: object}} stats - The statistics object to update. This object is mutated.
- */
- function addLatexMkMetrics(output, stats) {
- for (const [stat, matcher] of LATEX_MK_METRICS_STDOUT) {
- const match = matcher(output?.stdout || '', stats.latexmk)
- if (match) {
- stats.latexmk[stat] = match
- }
- }
- for (const [stat, matcher] of LATEX_MK_METRICS_STDERR) {
- const match = matcher(output?.stderr || '', stats.latexmk)
- if (match) {
- stats.latexmk[stat] = match
- }
- }
- }
- /**
- * Adds a non-enumerable `latexmk` property to the stats object.
- *
- * This property is used to store statistics from the latexmk compilation
- * process. It is made non-enumerable to prevent it from being serialized by
- * `JSON.stringify()`, which means it is not sent in the compile response
- * to web where it would added to the analytics events.
- *
- * @param {object} stats - The compile stats object to be modified.
- */
- function enableLatexMkMetrics(stats) {
- Object.defineProperty(stats, 'latexmk', {
- value: {},
- enumerable: false,
- })
- }
- /**
- * Parses the content of a latexmk .fdb file to extract and record metrics
- * about the files used during compilation.
- *
- * It categorizes files into 'system' (paths starting with '/') and 'user'
- * files. For each category, it aggregates the count and total size of files
- * grouped by their extension. The results are added to the provided stats
- * object under `stats.latexmk['fdb-file-types']`.
- *
- * Each file is counted only once, even if it appears multiple times in the
- * .fdb content. Files without a valid extension are counted as 'other'.
- *
- * @param {string | null | undefined} fdbContent - The content of the .fdb_latexmk file.
- * @param {object} stats - The statistics object to be populated. The metrics will be added to `stats.latexmk`.
- * @returns {void}
- */
- function addLatexFdbMetrics(fdbContent, stats) {
- if (!fdbContent) {
- return
- }
- const { systemFileTypes, userFileTypes, dependencies } =
- parseFdbContent(fdbContent)
- if (
- Object.keys(systemFileTypes).length > 0 ||
- Object.keys(userFileTypes).length > 0
- ) {
- const userSummary = summarizeFileTypes(userFileTypes)
- const systemSummary = summarizeFileTypes(systemFileTypes)
- stats.latexmk['fdb-file-types'] = {
- total: {
- systemFileCount: systemSummary.total.count,
- systemFileSize: systemSummary.total.size,
- imageFileCount: userSummary.image.count,
- imageFileSize: userSummary.image.size,
- textFileCount: userSummary.text.count,
- textFileSize: userSummary.text.size,
- fontFileCount: userSummary.font.count,
- fontFileSize: userSummary.font.size,
- otherFileCount: userSummary.other.count,
- otherFileSize: userSummary.other.size,
- },
- system: convertToArray(systemFileTypes),
- user: convertToArray(userFileTypes),
- }
- }
- if (dependencies.length > 0) {
- stats.latexmk['fdb-dependencies'] = dependencies
- }
- }
- function parseFdbContent(fdbContent) {
- const systemFileTypes = {}
- const userFileTypes = {}
- const seenFiles = new Set()
- const dependencies = new Set()
- // Extract the file path and size from lines like:
- // FILENAME TIMESTAMP SIZE CHECKSUM ...
- // "main.tex" 1760016467 6147 9da336eb1132ecb1d61cd1f5a70cfa62 ""
- // The timestamp can be an integer or a float
- const lineRegex = /^\s*"([^"]+)"\s+\d+(?:\.\d*)?\s+(\d+)/gm
- for (const match of fdbContent.matchAll(lineRegex)) {
- // strip leading /compile/ from paths
- const filePath = match[1].replace(/^\/compile\//, '')
- if (seenFiles.has(filePath)) {
- continue
- }
- const fileSize = parseInt(match[2], 10)
- const isSystemFile = filePath.startsWith('/')
- const extMatch = filePath.match(/\.([a-z]{1,4})$/i)
- const ext = extMatch ? extMatch[1].toLowerCase() : 'other'
- const fileTypes = isSystemFile ? systemFileTypes : userFileTypes
- if (!fileTypes[ext]) {
- fileTypes[ext] = { count: 0, size: 0 }
- }
- fileTypes[ext].count++
- fileTypes[ext].size += fileSize
- const depMatch = filePath.match(NOTEWORTHY_DEPENDENCIES_REGEXP)
- if (depMatch) {
- dependencies.add(depMatch[1])
- }
- seenFiles.add(filePath)
- }
- return {
- systemFileTypes,
- userFileTypes,
- dependencies: Array.from(dependencies),
- }
- }
- function getFileTypeCategory(ext) {
- switch (ext) {
- case 'png':
- case 'jpg':
- case 'jpeg':
- case 'tif':
- case 'tiff':
- case 'bmp':
- case 'gif':
- case 'svg':
- case 'eps':
- return 'image'
- case 'tex':
- case 'sty':
- case 'cls':
- case 'bib':
- return 'text'
- case 'ttf':
- case 'otf':
- case 'pfb':
- return 'font'
- default:
- return 'other'
- }
- }
- function summarizeFileTypes(fileTypes) {
- const summary = {
- image: { count: 0, size: 0 },
- text: { count: 0, size: 0 },
- font: { count: 0, size: 0 },
- other: { count: 0, size: 0 },
- total: { count: 0, size: 0 },
- }
- for (const [ext, info] of Object.entries(fileTypes)) {
- const category = getFileTypeCategory(ext)
- summary[category].count += info.count
- summary[category].size += info.size
- summary.total.count += info.count
- summary.total.size += info.size
- }
- return summary
- }
- function convertToArray(object) {
- return Object.entries(object)
- .map(([ext, value]) => ({
- ext,
- count: value.count,
- size: value.size,
- }))
- .sort((a, b) => b.size - a.size) // sort by size descending
- }
- export default { enableLatexMkMetrics, addLatexMkMetrics, addLatexFdbMetrics }
|