From fd87cf1ddd54d83a9a2821a4af899e5e8285de84 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 31 Jul 2026 14:35:17 +0100 Subject: [PATCH] Merge pull request #35966 from overleaf/bg-return-optimised-image-stats-and-timings return image stats and timings in compile response GitOrigin-RevId: ec700b8cb6680bfd8f28ad0f1f2bbdb0479398bc --- services/clsi/app/js/LatexMetrics.js | 34 ++++++++- services/clsi/app/js/LatexRunner.js | 2 +- .../acceptance/js/SimpleLatexFileTests.js | 4 ++ .../clsi/test/unit/js/LatexMetrics.test.js | 71 +++++++++++++------ 4 files changed, 87 insertions(+), 24 deletions(-) diff --git a/services/clsi/app/js/LatexMetrics.js b/services/clsi/app/js/LatexMetrics.js index 9f5274fd3a..b692d86184 100644 --- a/services/clsi/app/js/LatexMetrics.js +++ b/services/clsi/app/js/LatexMetrics.js @@ -138,6 +138,10 @@ const LATEX_MK_METRICS_STDERR = [ ], [ 'latexmk-img-times', + /** + * @param {string} s + * @returns {Array<{type: string, count: number, time_ms: number}>} + */ s => { const pngCategoriesByFile = new Map() const pngCopyMatches = s.matchAll(/^PNG copy: (.*)$/gm) @@ -157,6 +161,7 @@ const LATEX_MK_METRICS_STDERR = [ /^Image written \((PNG|JPG|JBIG2|PDF), (\d+) ms\): (.*)$/gm ) const timingsByType = new Map() + const countByType = new Map() for (const match of timingMatches) { let type = match[1] const timeMs = parseInt(match[2], 10) @@ -174,9 +179,12 @@ const LATEX_MK_METRICS_STDERR = [ const accumulatedTime = timingsByType.get(type) ?? 0 timingsByType.set(type, accumulatedTime + timeMs) + const accumulatedCount = countByType.get(type) ?? 0 + countByType.set(type, accumulatedCount + 1) } return Array.from(timingsByType.entries()).map(([type, timeMs]) => ({ type, + count: countByType.get(type) || 0, time_ms: timeMs, })) }, @@ -190,9 +198,10 @@ const LATEX_MK_METRICS_STDERR = [ * `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. + * @param {object} stats - The statistics object to update. This object is mutated. + * @param {object} [timings] - The timings object to update. This object is mutated. */ -function addLatexMkMetrics(output, stats) { +function addLatexMkMetrics(output, stats, timings) { for (const [stat, matcher] of LATEX_MK_METRICS_STDOUT) { const match = matcher(output?.stdout || '', stats.latexmk) if (match) { @@ -205,6 +214,27 @@ function addLatexMkMetrics(output, stats) { stats.latexmk[stat] = match } } + const imgTimings = stats.latexmk['latexmk-img-times'] + if (imgTimings) { + let totalImageIncludeTime = 0 + let optimisedImageIncludeTime = 0 + let totalImageIncludeCount = 0 + let optimisedImageIncludeCount = 0 + for (const { type, count, time_ms } of imgTimings) { + totalImageIncludeCount += count + totalImageIncludeTime += time_ms + if (type === 'PNG-png2pdf') { + optimisedImageIncludeCount += count + optimisedImageIncludeTime += time_ms + } + } + stats['include-image-all'] = totalImageIncludeCount + stats['include-image-optimised'] = optimisedImageIncludeCount + if (timings) { + timings['include-image-optimised'] = optimisedImageIncludeTime + timings['include-image-all'] = totalImageIncludeTime + } + } } /** diff --git a/services/clsi/app/js/LatexRunner.js b/services/clsi/app/js/LatexRunner.js index 2fd3afa279..ed5d1f8b4b 100644 --- a/services/clsi/app/js/LatexRunner.js +++ b/services/clsi/app/js/LatexRunner.js @@ -81,7 +81,7 @@ function runLatex(projectId, options, callback) { } if (stats.latexmk) { try { - addLatexMkMetrics(output, stats) + addLatexMkMetrics(output, stats, timings) } catch (err) { logger.error({ err, projectId }, 'error adding latexmk metrics') } diff --git a/services/clsi/test/acceptance/js/SimpleLatexFileTests.js b/services/clsi/test/acceptance/js/SimpleLatexFileTests.js index 3ee26adbab..7fdd103b6c 100644 --- a/services/clsi/test/acceptance/js/SimpleLatexFileTests.js +++ b/services/clsi/test/acceptance/js/SimpleLatexFileTests.js @@ -134,6 +134,8 @@ Hello world const expectedRuns = scenario.expectedRuns ?? 1 // Note: chai's all.keys assertion rejects extra keys stats.should.have.all.keys( + 'include-image-all', + 'include-image-optimised', 'isInitialCompile', 'latexmk-errors', 'latex-runs', @@ -148,6 +150,8 @@ Hello world 'compile', 'output', 'compileE2E', + 'include-image-all', + 'include-image-optimised', ...pdfCachingTimings ) }) diff --git a/services/clsi/test/unit/js/LatexMetrics.test.js b/services/clsi/test/unit/js/LatexMetrics.test.js index 26fc496875..88c410b9de 100644 --- a/services/clsi/test/unit/js/LatexMetrics.test.js +++ b/services/clsi/test/unit/js/LatexMetrics.test.js @@ -115,6 +115,7 @@ describe('LatexMetrics', function () { describe('addLatexMkMetrics', function () { beforeEach(function (ctx) { ctx.stats = {} + ctx.timings = {} Object.defineProperty(ctx.stats, 'latexmk', { value: {}, enumerable: false, @@ -128,34 +129,46 @@ describe('LatexMetrics', function () { 'Image written (PNG, 100 ms): output/image1.png\n' + 'Image written (JPG, 50 ms): output/image2.jpg\n', } - addLatexMkMetrics(output, ctx.stats) + addLatexMkMetrics(output, ctx.stats, ctx.timings) expect(ctx.stats.latexmk['latexmk-img-times']).to.deep.equal([ - { type: 'PNG', time_ms: 100 }, - { type: 'JPG', time_ms: 50 }, + { type: 'PNG', count: 1, time_ms: 100 }, + { type: 'JPG', count: 1, time_ms: 50 }, ]) + expect(ctx.stats['include-image-all']).to.equal(2) + expect(ctx.stats['include-image-optimised']).to.equal(0) + expect(ctx.timings['include-image-all']).to.equal(150) + expect(ctx.timings['include-image-optimised']).to.equal(0) }) it('should convert PDF type with .png extension to PNG-png2pdf', function (ctx) { const output = { stderr: 'Image written (PDF, 150 ms): image.png\n', } - addLatexMkMetrics(output, ctx.stats) + addLatexMkMetrics(output, ctx.stats, ctx.timings) expect(ctx.stats.latexmk['latexmk-img-times']).to.deep.equal([ - { type: 'PNG-png2pdf', time_ms: 150 }, + { type: 'PNG-png2pdf', count: 1, time_ms: 150 }, ]) + expect(ctx.stats['include-image-all']).to.equal(1) + expect(ctx.stats['include-image-optimised']).to.equal(1) + expect(ctx.timings['include-image-all']).to.equal(150) + expect(ctx.timings['include-image-optimised']).to.equal(150) }) it('should handle uppercase .PNG extension', function (ctx) { const output = { stderr: 'Image written (PDF, 200 ms): image.PNG\n', } - addLatexMkMetrics(output, ctx.stats) + addLatexMkMetrics(output, ctx.stats, ctx.timings) expect(ctx.stats.latexmk['latexmk-img-times']).to.deep.equal([ - { type: 'PNG-png2pdf', time_ms: 200 }, + { type: 'PNG-png2pdf', count: 1, time_ms: 200 }, ]) + expect(ctx.stats['include-image-all']).to.equal(1) + expect(ctx.stats['include-image-optimised']).to.equal(1) + expect(ctx.timings['include-image-all']).to.equal(200) + expect(ctx.timings['include-image-optimised']).to.equal(200) }) it('should keep PDF type for non-png extensions', function (ctx) { @@ -164,11 +177,15 @@ describe('LatexMetrics', function () { 'Image written (PDF, 100 ms): document.pdf\n' + 'Image written (PDF, 80 ms): image.jpg\n', } - addLatexMkMetrics(output, ctx.stats) + addLatexMkMetrics(output, ctx.stats, ctx.timings) expect(ctx.stats.latexmk['latexmk-img-times']).to.deep.equal([ - { type: 'PDF', time_ms: 180 }, + { type: 'PDF', count: 2, time_ms: 180 }, ]) + expect(ctx.stats['include-image-all']).to.equal(2) + expect(ctx.stats['include-image-optimised']).to.equal(0) + expect(ctx.timings['include-image-all']).to.equal(180) + expect(ctx.timings['include-image-optimised']).to.equal(0) }) it('should accumulate timing for the same type', function (ctx) { @@ -178,15 +195,19 @@ describe('LatexMetrics', function () { 'Image written (PDF, 150 ms): image2.png\n' + 'Image written (PNG, 50 ms): image3.png\n', } - addLatexMkMetrics(output, ctx.stats) + addLatexMkMetrics(output, ctx.stats, ctx.timings) const result = ctx.stats.latexmk['latexmk-img-times'].sort((a, b) => a.type.localeCompare(b.type) ) expect(result).to.deep.equal([ - { type: 'PNG', time_ms: 50 }, - { type: 'PNG-png2pdf', time_ms: 250 }, + { type: 'PNG', count: 1, time_ms: 50 }, + { type: 'PNG-png2pdf', count: 2, time_ms: 250 }, ]) + expect(ctx.stats['include-image-all']).to.equal(3) + expect(ctx.stats['include-image-optimised']).to.equal(2) + expect(ctx.timings['include-image-all']).to.equal(300) + expect(ctx.timings['include-image-optimised']).to.equal(250) }) it('should handle PNG with category from PNG copy skipped', function (ctx) { @@ -195,11 +216,15 @@ describe('LatexMetrics', function () { 'PNG copy skipped (alpha): image1.png\n' + 'Image written (PNG, 75 ms): image1.png\n', } - addLatexMkMetrics(output, ctx.stats) + addLatexMkMetrics(output, ctx.stats, ctx.timings) expect(ctx.stats.latexmk['latexmk-img-times']).to.deep.equal([ - { type: 'PNG-alpha', time_ms: 75 }, + { type: 'PNG-alpha', count: 1, time_ms: 75 }, ]) + expect(ctx.stats['include-image-all']).to.equal(1) + expect(ctx.stats['include-image-optimised']).to.equal(0) + expect(ctx.timings['include-image-all']).to.equal(75) + expect(ctx.timings['include-image-optimised']).to.equal(0) }) it('should mix PDF-to-png2pdf and PNG types', function (ctx) { @@ -209,16 +234,20 @@ describe('LatexMetrics', function () { 'Image written (PNG, 50 ms): native.png\n' + 'Image written (JPG, 30 ms): photo.jpg\n', } - addLatexMkMetrics(output, ctx.stats) + addLatexMkMetrics(output, ctx.stats, ctx.timings) const result = ctx.stats.latexmk['latexmk-img-times'].sort((a, b) => a.type.localeCompare(b.type) ) expect(result).to.deep.equal([ - { type: 'JPG', time_ms: 30 }, - { type: 'PNG', time_ms: 50 }, - { type: 'PNG-png2pdf', time_ms: 100 }, + { type: 'JPG', count: 1, time_ms: 30 }, + { type: 'PNG', count: 1, time_ms: 50 }, + { type: 'PNG-png2pdf', count: 1, time_ms: 100 }, ]) + expect(ctx.stats['include-image-all']).to.equal(3) + expect(ctx.stats['include-image-optimised']).to.equal(1) + expect(ctx.timings['include-image-all']).to.equal(180) + expect(ctx.timings['include-image-optimised']).to.equal(100) }) }) @@ -231,7 +260,7 @@ describe('LatexMetrics', function () { 'PNG copy skipped (palette): palette.png\n' + 'PNG copy skipped (other): other.png\n', } - addLatexMkMetrics(output, ctx.stats) + addLatexMkMetrics(output, ctx.stats, ctx.timings) expect(ctx.stats.latexmk['latexmk-png-slow']).to.deep.equal([ 'images/alpha.png', @@ -242,7 +271,7 @@ describe('LatexMetrics', function () { it('should not include fast PNG copy lines', function (ctx) { const output = { stderr: 'PNG copy: fast.png\n' } - addLatexMkMetrics(output, ctx.stats) + addLatexMkMetrics(output, ctx.stats, ctx.timings) expect(ctx.stats.latexmk).to.not.have.property('latexmk-png-slow') }) @@ -253,7 +282,7 @@ describe('LatexMetrics', function () { 'PNG copy skipped (gamma): /compile/deep/img.png\n' + 'PNG copy skipped (interlaced): ./local.png\n', } - addLatexMkMetrics(output, ctx.stats) + addLatexMkMetrics(output, ctx.stats, ctx.timings) expect(ctx.stats.latexmk['latexmk-png-slow']).to.deep.equal([ 'deep/img.png',