Adding 2025 and 2026 incremental compiles options (#35850)

* feat: adding 2025 and 2026 incremental compiles options

* feat: remove dmtcp from compiler list and autoselect checkpointing version with labs inclusion

* feat: updating seccomp profile for checkpointing

* feat: disable non-premium dmtcp compiles, and align checkpoint version detection between clsi and web

* feat: adding in reminder of checkpoint experiment, and using session based split test retrieval

* Increase Packer build.sh timeout from 1h15m to 1h30m as build takes approx 1h

* fix: moving checkpointing check from tag name based to hasCheckpointing flag set as part of allowed_images, and removing uneeded seccomp rule

* feat: move variant/ image selection to CLSI, and have web control only what options to set for a compile. removing uneeded extra variants from webs list of allowed images

* feat: adding compile timer as a query string as well

---------

Co-authored-by: Tim Gibbon <tim.gibbon@overleaf.com>
GitOrigin-RevId: cdbdf76a62dcd5a585925f18d64b6cb9cd6ed373
This commit is contained in:
Jimmy Domagala-Tang
2026-09-17 12:00:46 +00:00
committed by Copybot
co-authored by Tim Gibbon
parent 35271cb33c
commit baa9cf78d9
12 changed files with 389 additions and 27 deletions
+44 -8
View File
@@ -170,7 +170,7 @@ async function doCompile(request, stats, timings) {
)
// set up environment variables for chktex
const env = {
let env = {
OVERLEAF_PROJECT_ID: request.project_id,
}
if (Settings.texliveOpenoutAny && Settings.texliveOpenoutAny !== '') {
@@ -194,10 +194,11 @@ async function doCompile(request, stats, timings) {
}
}
// Pass through checkpoint setting
if (request.enableCheckpoint) {
env.ENABLE_CHECKPOINT = '1'
}
// the requested compile options may need a variant of the requested image,
// which brings its own environment
const imageVariantSettings = _getImageVariantSettings(request)
request.imageName = imageVariantSettings.imageName
env = { ...env, ...imageVariantSettings.env }
const compileStart = Date.now()
@@ -769,10 +770,45 @@ function _parseWordcountFromOutput(output) {
return results
}
function _getAllowedImages() {
return Settings.clsi?.docker?.allowedImages
}
function _isImageNameAllowed(imageName) {
const ALLOWED_IMAGES =
Settings.clsi && Settings.clsi.docker && Settings.clsi.docker.allowedImages
return !ALLOWED_IMAGES || ALLOWED_IMAGES.includes(imageName)
const allowedImages = _getAllowedImages()
return !allowedImages || allowedImages.includes(imageName)
}
// Variants are images built on top of a base image, tagged with a suffix, eg
// texlive-full:2026.1.checkpointing is the checkpointing variant of
// texlive-full:2026.1. Returns the settings needed to compile the request on
// the variant its options call for: the image to run and the environment that
// image needs. Falls back to the requested image when no variant applies, or
// when the variant is not available on this host.
function _getImageVariantSettings(request) {
const { imageName } = request
// checkpointing compiles run on the checkpointing variant of the image
if (request.enableCheckpoint) {
const checkpointingImageName = `${imageName}.checkpointing`
if (imageName && _getAllowedImages()?.includes(checkpointingImageName)) {
return {
imageName: checkpointingImageName,
env: { ENABLE_CHECKPOINT: '1' },
}
}
// variant either doesnt exist when it should, or user was able to make a request they shouldnt be able to
logger.error(
{
projectId: request.project_id,
userId: request.user_id,
imageName,
},
'no checkpointing variant available for image, compiling without it'
)
}
return { imageName, env: {} }
}
function _emitMetrics(request, status, stats, timings) {
@@ -186,6 +186,16 @@ describe('CompileManager', () => {
default: ctx.Settings,
}))
ctx.logger = {
debug: sinon.stub(),
info: sinon.stub(),
warn: sinon.stub(),
error: sinon.stub(),
}
vi.doMock('@overleaf/logger', () => ({
default: ctx.logger,
}))
vi.doMock('@overleaf/metrics', () => ({
default: {
inc: sinon.stub(),
@@ -390,6 +400,77 @@ describe('CompileManager', () => {
})
})
describe('with the enableCheckpoint option', () => {
beforeEach(ctx => {
ctx.baseImage = 'example.com/texlive-full:2025.1'
ctx.checkpointingImage = `${ctx.baseImage}.checkpointing`
ctx.request.imageName = ctx.baseImage
ctx.request.enableCheckpoint = true
ctx.Settings.clsi.docker.allowedImages = [
ctx.baseImage,
ctx.checkpointingImage,
]
})
it('should compile on the checkpointing variant of the image', async ctx => {
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
const { image, environment } =
ctx.LatexRunner.promises.runLatex.args[0][1]
expect(image).to.equal(ctx.checkpointingImage)
expect(environment).to.have.property('ENABLE_CHECKPOINT', '1')
})
it('should log an error and compile on the base image when there is no checkpointing variant', async ctx => {
ctx.Settings.clsi.docker.allowedImages = [ctx.baseImage]
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
const { image, environment } =
ctx.LatexRunner.promises.runLatex.args[0][1]
expect(image).to.equal(ctx.baseImage)
expect(environment).to.not.have.property('ENABLE_CHECKPOINT')
expect(ctx.logger.error).to.have.been.calledWith(
sinon.match({ imageName: ctx.baseImage })
)
})
it('should log an error and compile on the base image when there is no allow list', async ctx => {
delete ctx.Settings.clsi.docker.allowedImages
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
const { image, environment } =
ctx.LatexRunner.promises.runLatex.args[0][1]
expect(image).to.equal(ctx.baseImage)
expect(environment).to.not.have.property('ENABLE_CHECKPOINT')
expect(ctx.logger.error).to.have.been.called
})
it('should log an error and compile without an image when none is requested', async ctx => {
delete ctx.request.imageName
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
const { image, environment } =
ctx.LatexRunner.promises.runLatex.args[0][1]
expect(image).to.be.undefined
expect(environment).to.not.have.property('ENABLE_CHECKPOINT')
expect(ctx.logger.error).to.have.been.called
})
})
describe('without the enableCheckpoint option', () => {
beforeEach(async ctx => {
ctx.request.imageName = 'example.com/texlive-full:2025.1'
ctx.Settings.clsi.docker.allowedImages = [
'example.com/texlive-full:2025.1',
'example.com/texlive-full:2025.1.checkpointing',
]
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
})
it('should not enable checkpointing or change the image', ctx => {
const { image, environment } =
ctx.LatexRunner.promises.runLatex.args[0][1]
expect(image).to.equal('example.com/texlive-full:2025.1')
expect(environment).to.not.have.property('ENABLE_CHECKPOINT')
})
})
describe('with a check option', () => {
beforeEach(async ctx => {
ctx.request.check = 'error'
@@ -194,6 +194,39 @@ describe('RequestParser', () => {
})
})
describe('with enableCheckpoint set', () => {
beforeEach(async ctx => {
await new Promise((resolve, reject) => {
ctx.validRequest.compile.options.enableCheckpoint = true
ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
if (error) return reject(error)
ctx.data = data
resolve()
})
})
})
it('should set the enableCheckpoint attribute', ctx => {
expect(ctx.data.enableCheckpoint).to.equal(true)
})
})
describe('with enableCheckpoint not specified', () => {
beforeEach(async ctx => {
await new Promise((resolve, reject) => {
ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
if (error) return reject(error)
ctx.data = data
resolve()
})
})
})
it('should default the enableCheckpoint attribute to false', ctx => {
expect(ctx.data.enableCheckpoint).to.equal(false)
})
})
describe('without a timeout specified', () => {
beforeEach(async ctx => {
await new Promise((resolve, reject) => {
@@ -1,3 +1,4 @@
import Path from 'node:path'
import { callbackify } from 'node:util'
import { callbackifyMultiResult } from '@overleaf/promise-utils'
import {
@@ -1184,7 +1185,6 @@ function _finaliseRequest(projectId, options, project, docs, files) {
const hasPremiumCompiles = ['alpha', 'priority'].includes(
options.compileGroup
)
return {
compile: {
options: {
@@ -1197,6 +1197,11 @@ function _finaliseRequest(projectId, options, project, docs, files) {
draft: Boolean(options.draft),
// enable for premium compiles only
png2pdf: Boolean(options.png2pdf) && hasPremiumCompiles,
// enable for premium compiles on an image that has a checkpointing build
enableCheckpoint:
Boolean(options.checkpointing) &&
hasPremiumCompiles &&
_imageHasCheckpointing(project.imageName),
stopOnFirstError: Boolean(options.stopOnFirstError),
check: options.check,
syncType: options.syncType,
@@ -1213,7 +1218,6 @@ function _finaliseRequest(projectId, options, project, docs, files) {
enablePdfCaching:
(Settings.enablePdfCaching && options.enablePdfCaching) || false,
pdfCachingMinChunkSize: options.pdfCachingMinChunkSize,
enableCheckpoint: Boolean(options.enableCheckpoint),
flags,
metricsMethod: options.compileGroup,
metricsPath: options.metricsPath,
@@ -1228,6 +1232,21 @@ function _finaliseRequest(projectId, options, project, docs, files) {
}
}
// checkpointing compiles run on a checkpointing build of the project's image,
// which is only available for some TeX Live years
function _imageHasCheckpointing(imageName) {
if (!imageName) {
return false
}
// allowedImageNames holds bare image names, so drop the hosting URL first
const bareImageName = Path.basename(imageName)
return Boolean(
Settings.allowedImageNames?.find(
allowedImage => allowedImage.imageName === bareImageName
)?.hasCheckpointing
)
}
async function buildDocumentConversionRequest(projectId, userId, options) {
return await _buildRequest(null, projectId, userId, {
...options,
@@ -81,13 +81,6 @@ async function _getSplitTestOptions(req, res) {
{ enablePdfCaching: false }
: { enablePdfCaching, pdfCachingMinChunkSize }
const enableCheckpoint = await SplitTestHandler.promises.featureFlagEnabled(
req,
res,
'compile-with-checkpoint',
{ includeReferer: true }
)
const enablePng2Pdf = await SplitTestHandler.promises.featureFlagEnabled(
req,
res,
@@ -100,12 +93,24 @@ async function _getSplitTestOptions(req, res) {
? { enablePng2Pdf: false }
: { enablePng2Pdf }
const checkpointingEnabled =
await SplitTestHandler.promises.featureFlagEnabled(
req,
res,
'compile-with-checkpoint',
{ includeReferer: true }
)
const checkpointCompilesOptions = {
enableCheckpointCompiles: checkpointingEnabled,
}
return {
compileFromHistory,
pdfDownloadDomain,
...pdfCachingOptions,
enableCheckpoint,
...png2PdfOptions,
...checkpointCompilesOptions,
}
}
@@ -454,19 +459,19 @@ const _CompileController = {
pdfCachingMinChunkSize,
pdfDownloadDomain,
compileFromHistory,
enableCheckpoint,
enablePng2Pdf,
enableCheckpointCompiles,
} = await _getSplitTestOptions(req, res)
if (Features.hasFeature('saas')) {
options.compileFromClsiCache = true
options.populateClsiCache = true
options.compileFromHistory = compileFromHistory
if (enableCheckpoint) {
options.enableCheckpoint = enableCheckpoint
}
if (enablePng2Pdf) {
options.png2pdf = enablePng2Pdf
}
if (enableCheckpointCompiles) {
options.checkpointing = true
}
}
options.enablePdfCaching = enablePdfCaching
if (enablePdfCaching) {
@@ -189,13 +189,13 @@ function _imageAllowed(
}
async function getAllowedImagesForUser(req, res, user) {
let images = Settings.allowedImageNames || []
const images = Settings.allowedImageNames || []
const alphaImagesAllowed = Boolean(user?.alphaProgram)
const monthlyExperimentalImagesAllowed =
await _monthlyExperimentalImageAllowed(req, res)
images = images.map(image => {
return images.map(image => {
return {
...image,
allowed: _imageAllowed(
@@ -206,6 +206,4 @@ async function getAllowedImagesForUser(req, res, user) {
rolling: image.monthlyExperimental,
}
})
return images
}
@@ -1013,6 +1013,8 @@
"increase_indent": "",
"increased_ai_allowance": "",
"increased_compile_timeout": "",
"incremental_compiles_are_on": "",
"incremental_compiles_reusing_last_compile": "",
"info": "",
"inline": "",
"inline_math": "",
@@ -0,0 +1,32 @@
import Notification from '@/shared/components/notification'
import { useTranslation, Trans } from 'react-i18next'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
const CheckpointCompilesEnabledReminder = () => {
const { t } = useTranslation()
if (!isSplitTestEnabled('compile-with-checkpoint')) {
return null
}
const content = (
<Trans
i18nKey="incremental_compiles_reusing_last_compile"
components={[
<a href="/labs/participate" target="_blank" rel="noreferrer" />, // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content
]}
/>
)
return (
<div className="notification-list">
<Notification
title={t('incremental_compiles_are_on')}
content={content}
type="info"
className="mb-0"
/>
</div>
)
}
export default CheckpointCompilesEnabledReminder
@@ -16,6 +16,7 @@ import getMeta from '@/utils/meta'
import PdfClearCacheButton from '@/features/pdf-preview/components/pdf-clear-cache-button'
import PdfDownloadFilesButton from '@/features/pdf-preview/components/pdf-download-files-button'
import RollingBuildSelectedReminder from './rolling-build-selected-reminder'
import CheckpointCompilesEnabledReminder from './checkpoint-compiles-enabled-reminder'
import ErrorAssistantAiPaywallNotification from './error-assistant-ai-paywall-notification'
type ErrorLogTab = {
@@ -78,6 +79,7 @@ function ErrorLogs({
<TabContent className="error-logs new-error-logs">
<div className="logs-pane-content">
<RollingBuildSelectedReminder />
<CheckpointCompilesEnabledReminder />
{stoppedOnFirstError && includeErrors && <StopOnFirstErrorPrompt />}
{loadingError && (
@@ -38,7 +38,9 @@ function PdfPreviewHybridToolbar() {
function PdfPreviewHybridToolbarInner() {
const { focusMode } = useLayoutContext()
const showCompileTimer = useFeatureFlag('compile-with-checkpoint')
const debugTimer =
new URLSearchParams(window.location.search).get('compile-timer') === 'true'
const showCompileTimer = useFeatureFlag('show-compile-timer') || debugTimer
return (
<>
+2
View File
@@ -1285,6 +1285,8 @@
"increased_ai_allowance": "Increased AI allowance",
"increased_ai_allowance_tooltip": "10 AI uses per day across all AI tools.",
"increased_compile_timeout": "Increased compile timeout",
"incremental_compiles_are_on": "Faster recompiles are on",
"incremental_compiles_reusing_last_compile": "We’re reusing part of your last compile to speed things up. It’s experimental, so if your PDF or logs look wrong, turn it off by <0>leaving the Labs experiment</0>, then recompile.",
"individuals": "Individuals",
"info": "Info",
"initiator": "Initiator",
@@ -198,6 +198,10 @@ describe('ClsiManager', function () {
clsiCookie: { key: 'clsiserver' },
safeCompilers: ['pdflatex', 'latex', 'xelatex', 'lualatex'],
defaultLatexCompiler: 'pdflatex',
allowedImageNames: [
{ imageName: 'mock-image-name', hasCheckpointing: true },
{ imageName: 'mock-image-name-no-checkpointing' },
],
}
ctx.ClsiCacheHandler = {
clearCache: sinon.stub().resolves(),
@@ -1117,6 +1121,152 @@ describe('ClsiManager', function () {
})
})
describe('with the checkpointing option', function () {
it('should ask the clsi to enable checkpointing, leaving the image alone', async function (ctx) {
await ctx.ClsiManager.promises.sendRequest(
null,
ctx.project._id,
ctx.user_id,
{
timeout: 100,
compileGroup: 'priority',
checkpointing: true,
}
)
expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith(
sinon.match.any,
sinon.match({
json: {
compile: {
options: {
imageName: ctx.project.imageName,
enableCheckpoint: true,
},
},
},
})
)
})
it('should not enable checkpointing for a standard compileGroup', async function (ctx) {
await ctx.ClsiManager.promises.sendRequest(
null,
ctx.project._id,
ctx.user_id,
{
timeout: 100,
compileGroup: 'standard',
checkpointing: true,
}
)
expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith(
sinon.match.any,
sinon.match({
json: {
compile: { options: { enableCheckpoint: false } },
},
})
)
})
it('should not enable checkpointing without the option', async function (ctx) {
await ctx.ClsiManager.promises.sendRequest(
null,
ctx.project._id,
ctx.user_id,
{
timeout: 100,
compileGroup: 'priority',
}
)
expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith(
sinon.match.any,
sinon.match({
json: {
compile: {
options: {
imageName: ctx.project.imageName,
enableCheckpoint: false,
},
},
},
})
)
})
it('should not enable checkpointing when the image has no checkpointing build', async function (ctx) {
ctx.project.imageName = 'mock-image-name-no-checkpointing'
await ctx.ClsiManager.promises.sendRequest(
null,
ctx.project._id,
ctx.user_id,
{
timeout: 100,
compileGroup: 'priority',
checkpointing: true,
}
)
expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith(
sinon.match.any,
sinon.match({
json: {
compile: { options: { enableCheckpoint: false } },
},
})
)
})
it('should not enable checkpointing when the project has no image', async function (ctx) {
delete ctx.project.imageName
await ctx.ClsiManager.promises.sendRequest(
null,
ctx.project._id,
ctx.user_id,
{
timeout: 100,
compileGroup: 'priority',
checkpointing: true,
}
)
expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith(
sinon.match.any,
sinon.match({
json: {
compile: { options: { enableCheckpoint: false } },
},
})
)
})
it('should look up the image ignoring the registry host', async function (ctx) {
ctx.project.imageName = 'quay.io/sharelatex/mock-image-name'
await ctx.ClsiManager.promises.sendRequest(
null,
ctx.project._id,
ctx.user_id,
{
timeout: 100,
compileGroup: 'priority',
checkpointing: true,
}
)
expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith(
sinon.match.any,
sinon.match({
json: {
compile: { options: { enableCheckpoint: true } },
},
})
)
})
})
describe('with the png2pdf option', function () {
beforeEach(async function (ctx) {
await ctx.ClsiManager.promises.sendRequest(