[web] emit event when synctex mapping was downloaded from clsi-cache (#25424)

* [clsi] tell frontend when synctex mapping was downloaded from clsi-cache

* [web] emit event when synctex mapping was downloaded from clsi-cache

GitOrigin-RevId: 1f6b7e0faaa7dd76449aad566802da971a4cf9ed
This commit is contained in:
Jakob Ackermann
2025-05-09 08:06:00 +00:00
committed by Copybot
parent 28bb8ed2ab
commit 35adaf19b6
7 changed files with 160 additions and 31 deletions
+4 -2
View File
@@ -204,12 +204,13 @@ function syncFromCode(req, res, next) {
line,
column,
{ imageName, editorId, buildId, compileFromClsiCache },
function (error, pdfPositions) {
function (error, pdfPositions, downloadedFromCache) {
if (error) {
return next(error)
}
res.json({
pdf: pdfPositions,
downloadedFromCache,
})
}
)
@@ -229,12 +230,13 @@ function syncFromPdf(req, res, next) {
h,
v,
{ imageName, editorId, buildId, compileFromClsiCache },
function (error, codePositions) {
function (error, codePositions, downloadedFromCache) {
if (error) {
return next(error)
}
res.json({
code: codePositions,
downloadedFromCache,
})
}
)
+44 -16
View File
@@ -23,6 +23,7 @@ const {
downloadLatestCompileCache,
downloadOutputDotSynctexFromCompileCache,
} = require('./CLSICacheHandler')
const { callbackifyMultiResult } = require('@overleaf/promise-utils')
const COMPILE_TIME_BUCKETS = [
// NOTE: These buckets are locked in per metric name.
@@ -447,12 +448,20 @@ async function syncFromCode(projectId, userId, filename, line, column, opts) {
'-o',
outputFilePath,
]
const stdout = await _runSynctex(projectId, userId, command, opts)
const { stdout, downloadedFromCache } = await _runSynctex(
projectId,
userId,
command,
opts
)
logger.debug(
{ projectId, userId, filename, line, column, command, stdout },
'synctex code output'
)
return SynctexOutputParser.parseViewOutput(stdout)
return {
codePositions: SynctexOutputParser.parseViewOutput(stdout),
downloadedFromCache,
}
}
async function syncFromPdf(projectId, userId, page, h, v, opts) {
@@ -465,9 +474,17 @@ async function syncFromPdf(projectId, userId, page, h, v, opts) {
'-o',
`${page}:${h}:${v}:${outputFilePath}`,
]
const stdout = await _runSynctex(projectId, userId, command, opts)
const { stdout, downloadedFromCache } = await _runSynctex(
projectId,
userId,
command,
opts
)
logger.debug({ projectId, userId, page, h, v, stdout }, 'synctex pdf output')
return SynctexOutputParser.parseEditOutput(stdout, baseDir)
return {
pdfPositions: SynctexOutputParser.parseEditOutput(stdout, baseDir),
downloadedFromCache,
}
}
async function _checkFileExists(dir, filename) {
@@ -522,9 +539,10 @@ async function _runSynctex(projectId, userId, command, opts) {
return await OutputCacheManager.promises.queueDirOperation(
outputDir,
/**
* @return {Promise<string>}
* @return {Promise<{stdout: string, downloadedFromCache: boolean}>}
*/
async () => {
let downloadedFromCache = false
try {
await _checkFileExists(directory, 'output.synctex.gz')
} catch (err) {
@@ -535,13 +553,14 @@ async function _runSynctex(projectId, userId, command, opts) {
buildId
) {
try {
await downloadOutputDotSynctexFromCompileCache(
projectId,
userId,
editorId,
buildId,
directory
)
downloadedFromCache =
await downloadOutputDotSynctexFromCompileCache(
projectId,
userId,
editorId,
buildId,
directory
)
} catch (err) {
logger.warn(
{ err, projectId, userId, editorId, buildId },
@@ -554,7 +573,7 @@ async function _runSynctex(projectId, userId, command, opts) {
}
}
try {
const output = await CommandRunner.promises.run(
const { stdout } = await CommandRunner.promises.run(
compileName,
command,
directory,
@@ -563,7 +582,10 @@ async function _runSynctex(projectId, userId, command, opts) {
{},
compileGroup
)
return output.stdout
return {
stdout,
downloadedFromCache,
}
} catch (error) {
throw OError.tag(error, 'error running synctex', {
command,
@@ -686,8 +708,14 @@ module.exports = {
stopCompile: callbackify(stopCompile),
clearProject: callbackify(clearProject),
clearExpiredProjects: callbackify(clearExpiredProjects),
syncFromCode: callbackify(syncFromCode),
syncFromPdf: callbackify(syncFromPdf),
syncFromCode: callbackifyMultiResult(syncFromCode, [
'codePositions',
'downloadedFromCache',
]),
syncFromPdf: callbackifyMultiResult(syncFromPdf, [
'pdfPositions',
'downloadedFromCache',
]),
wordcount: callbackify(wordcount),
promises: {
doCompileWithLock,
@@ -109,6 +109,7 @@ Hello world
width: 343.71106,
},
],
downloadedFromCache: false,
})
done()
}
@@ -146,6 +147,7 @@ Hello world
expect(error).to.not.exist
expect(result).to.deep.equal({
code: [{ file: 'main.tex', line: 3, column: -1 }],
downloadedFromCache: false,
})
done()
}
@@ -67,6 +67,7 @@ Hello world
width: 343.71106,
},
],
downloadedFromCache: false,
})
return done()
}
@@ -87,6 +88,7 @@ Hello world
}
expect(codePositions).to.deep.equal({
code: [{ file: 'main.tex', line: 3, column: -1 }],
downloadedFromCache: false,
})
return done()
}
@@ -410,7 +410,7 @@ describe('CompileController', function () {
this.CompileManager.syncFromCode = sinon
.stub()
.yields(null, (this.pdfPositions = ['mock-positions']))
.yields(null, (this.pdfPositions = ['mock-positions']), true)
this.CompileController.syncFromCode(this.req, this.res, this.next)
})
@@ -430,6 +430,7 @@ describe('CompileController', function () {
this.res.json
.calledWith({
pdf: this.pdfPositions,
downloadedFromCache: true,
})
.should.equal(true)
})
@@ -451,7 +452,7 @@ describe('CompileController', function () {
this.CompileManager.syncFromPdf = sinon
.stub()
.yields(null, (this.codePositions = ['mock-positions']))
.yields(null, (this.codePositions = ['mock-positions']), true)
this.CompileController.syncFromPdf(this.req, this.res, this.next)
})
@@ -465,6 +466,7 @@ describe('CompileController', function () {
this.res.json
.calledWith({
code: this.codePositions,
downloadedFromCache: true,
})
.should.equal(true)
})
@@ -35,7 +35,7 @@ describe('CompileManager', function () {
build: 1234,
},
]
this.buildId = 'build-id-123'
this.buildId = '00000000000-0000000000000000'
this.commandOutput = 'Dummy output'
this.compileBaseDir = '/compile/dir'
this.outputBaseDir = '/output/dir'
@@ -61,6 +61,8 @@ describe('CompileManager', function () {
},
}
this.OutputCacheManager = {
BUILD_REGEX: /^[0-9a-f]+-[0-9a-f]+$/,
CACHE_SUBDIR: 'generated-files',
promises: {
queueDirOperation: sinon.stub().callsArg(1),
saveOutputFiles: sinon
@@ -88,9 +90,10 @@ describe('CompileManager', function () {
execFile: sinon.stub().yields(),
}
this.CommandRunner = {
canRunSyncTeXInOutputDir: sinon.stub().returns(false),
promises: {
run: sinon.stub().callsFake((_1, _2, _3, _4, _5, _6, compileGroup) => {
if (compileGroup === 'synctex') {
if (compileGroup === 'synctex' || compileGroup === 'synctex-output') {
return Promise.resolve({ stdout: this.commandOutput })
} else {
return Promise.resolve({
@@ -141,6 +144,12 @@ describe('CompileManager', function () {
.withArgs(Path.join(this.compileDir, 'output.synctex.gz'))
.resolves(this.fileStats)
this.CLSICacheHandler = {
notifyCLSICacheAboutBuild: sinon.stub(),
downloadLatestCompileCache: sinon.stub().resolves(),
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
}
this.CompileManager = SandboxedModule.require(MODULE_PATH, {
requires: {
'./LatexRunner': this.LatexRunner,
@@ -161,11 +170,7 @@ describe('CompileManager', function () {
'./LockManager': this.LockManager,
'./SynctexOutputParser': this.SynctexOutputParser,
'fs/promises': this.fsPromises,
'./CLSICacheHandler': {
notifyCLSICacheAboutBuild: sinon.stub(),
downloadLatestCompileCache: sinon.stub().resolves(),
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
},
'./CLSICacheHandler': this.CLSICacheHandler,
},
})
})
@@ -462,12 +467,83 @@ describe('CompileManager', function () {
this.compileDir,
this.Settings.clsi.docker.image,
60000,
{}
{},
'synctex'
)
})
it('should return the parsed output', function () {
expect(this.result).to.deep.equal(this.records)
expect(this.result).to.deep.equal({
codePositions: this.records,
downloadedFromCache: false,
})
})
})
describe('from cache in docker', function () {
beforeEach(async function () {
this.CommandRunner.canRunSyncTeXInOutputDir.returns(true)
this.Settings.path.synctexBaseDir
.withArgs(`${this.projectId}-${this.userId}`)
.returns('/compile')
const errNotFound = new Error()
errNotFound.code = 'ENOENT'
this.outputDir = `${this.outputBaseDir}/${this.projectId}-${this.userId}/${this.OutputCacheManager.CACHE_SUBDIR}/${this.buildId}`
const filename = Path.join(this.outputDir, 'output.synctex.gz')
this.fsPromises.stat
.withArgs(this.outputDir)
.onFirstCall()
.rejects(errNotFound)
this.fsPromises.stat
.withArgs(this.outputDir)
.onSecondCall()
.resolves(this.dirStats)
this.fsPromises.stat.withArgs(filename).resolves(this.fileStats)
this.CLSICacheHandler.downloadOutputDotSynctexFromCompileCache.resolves(
true
)
this.result = await this.CompileManager.promises.syncFromCode(
this.projectId,
this.userId,
this.filename,
this.line,
this.column,
{
imageName: 'image',
editorId: '00000000-0000-0000-0000-000000000000',
buildId: this.buildId,
compileFromClsiCache: true,
}
)
})
it('should run in output dir', function () {
const outputFilePath = '/compile/output.pdf'
const inputFilePath = `/compile/${this.filename}`
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
`${this.projectId}-${this.userId}`,
[
'synctex',
'view',
'-i',
`${this.line}:${this.column}:${inputFilePath}`,
'-o',
outputFilePath,
],
this.outputDir,
'image',
60000,
{},
'synctex-output'
)
})
it('should return the parsed output', function () {
expect(this.result).to.deep.equal({
codePositions: this.records,
downloadedFromCache: true,
})
})
})
@@ -500,7 +576,8 @@ describe('CompileManager', function () {
this.compileDir,
customImageName,
60000,
{}
{},
'synctex'
)
})
})
@@ -544,7 +621,10 @@ describe('CompileManager', function () {
})
it('should return the parsed output', function () {
expect(this.result).to.deep.equal(this.records)
expect(this.result).to.deep.equal({
pdfPositions: this.records,
downloadedFromCache: false,
})
})
})
@@ -18,6 +18,7 @@ import { CursorPosition } from '@/features/ide-react/types/cursor-position'
import { isValidTeXFile } from '@/main/is-valid-tex-file'
import { PdfScrollPosition } from '@/shared/hooks/use-pdf-scroll-position'
import { showFileErrorToast } from '@/features/pdf-preview/components/synctex-toasts'
import { sendMB } from '@/infrastructure/event-tracking'
export default function useSynctex(): {
syncToPdf: () => void
@@ -115,6 +116,12 @@ export default function useSynctex(): {
.then(data => {
setShowLogs(false)
setHighlights(data.pdf)
if (data.downloadedFromCache) {
sendMB('synctex-downloaded-from-cache', {
projectId,
method: 'code',
})
}
})
.catch(debugConsole.error)
.finally(() => {
@@ -223,6 +230,12 @@ export default function useSynctex(): {
.then(data => {
const [{ file, line }] = data.code
goToCodeLine(file, line)
if (data.downloadedFromCache) {
sendMB('synctex-downloaded-from-cache', {
projectId,
method: 'pdf',
})
}
})
.catch(debugConsole.error)
.finally(() => {