|
@@ -10,21 +10,26 @@ const Settings = require('settings-sharelatex')
|
|
|
const OError = require('@overleaf/o-error')
|
|
const OError = require('@overleaf/o-error')
|
|
|
const pLimit = require('p-limit')
|
|
const pLimit = require('p-limit')
|
|
|
const { parseXrefTable } = require('../lib/pdfjs/parseXrefTable')
|
|
const { parseXrefTable } = require('../lib/pdfjs/parseXrefTable')
|
|
|
|
|
+const { TimedOutError } = require('./Errors')
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
*
|
|
*
|
|
|
* @param {String} contentDir path to directory where content hash files are cached
|
|
* @param {String} contentDir path to directory where content hash files are cached
|
|
|
* @param {String} filePath the pdf file to scan for streams
|
|
* @param {String} filePath the pdf file to scan for streams
|
|
|
* @param {number} size the pdf size
|
|
* @param {number} size the pdf size
|
|
|
|
|
+ * @param {number} compileTime
|
|
|
*/
|
|
*/
|
|
|
-async function update(contentDir, filePath, size) {
|
|
|
|
|
|
|
+async function update(contentDir, filePath, size, compileTime) {
|
|
|
|
|
+ const checkDeadline = getDeadlineChecker(compileTime)
|
|
|
const ranges = []
|
|
const ranges = []
|
|
|
const newRanges = []
|
|
const newRanges = []
|
|
|
// keep track of hashes expire old ones when they reach a generation > N.
|
|
// keep track of hashes expire old ones when they reach a generation > N.
|
|
|
const tracker = await HashFileTracker.from(contentDir)
|
|
const tracker = await HashFileTracker.from(contentDir)
|
|
|
tracker.updateAge()
|
|
tracker.updateAge()
|
|
|
|
|
|
|
|
- const rawTable = await parseXrefTable(filePath, size)
|
|
|
|
|
|
|
+ checkDeadline('after init HashFileTracker')
|
|
|
|
|
+
|
|
|
|
|
+ const rawTable = await parseXrefTable(filePath, size, checkDeadline)
|
|
|
rawTable.sort((a, b) => {
|
|
rawTable.sort((a, b) => {
|
|
|
return a.offset - b.offset
|
|
return a.offset - b.offset
|
|
|
})
|
|
})
|
|
@@ -32,6 +37,8 @@ async function update(contentDir, filePath, size) {
|
|
|
obj.idx = idx
|
|
obj.idx = idx
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ checkDeadline('after parsing')
|
|
|
|
|
+
|
|
|
const uncompressedObjects = []
|
|
const uncompressedObjects = []
|
|
|
for (const object of rawTable) {
|
|
for (const object of rawTable) {
|
|
|
if (!object.uncompressed) {
|
|
if (!object.uncompressed) {
|
|
@@ -50,12 +57,14 @@ async function update(contentDir, filePath, size) {
|
|
|
if (size < Settings.pdfCachingMinChunkSize) {
|
|
if (size < Settings.pdfCachingMinChunkSize) {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
- uncompressedObjects.push(object)
|
|
|
|
|
|
|
+ uncompressedObjects.push({ object, idx: uncompressedObjects.length })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ checkDeadline('after finding uncompressed')
|
|
|
|
|
+
|
|
|
const handle = await fs.promises.open(filePath)
|
|
const handle = await fs.promises.open(filePath)
|
|
|
try {
|
|
try {
|
|
|
- for (const object of uncompressedObjects) {
|
|
|
|
|
|
|
+ for (const { object, idx } of uncompressedObjects) {
|
|
|
let buffer = Buffer.alloc(object.size, 0)
|
|
let buffer = Buffer.alloc(object.size, 0)
|
|
|
const { bytesRead } = await handle.read(
|
|
const { bytesRead } = await handle.read(
|
|
|
buffer,
|
|
buffer,
|
|
@@ -63,6 +72,7 @@ async function update(contentDir, filePath, size) {
|
|
|
object.size,
|
|
object.size,
|
|
|
object.offset
|
|
object.offset
|
|
|
)
|
|
)
|
|
|
|
|
+ checkDeadline('after read ' + idx)
|
|
|
if (bytesRead !== object.size) {
|
|
if (bytesRead !== object.size) {
|
|
|
throw new OError('could not read full chunk', {
|
|
throw new OError('could not read full chunk', {
|
|
|
object,
|
|
object,
|
|
@@ -80,6 +90,7 @@ async function update(contentDir, filePath, size) {
|
|
|
buffer = buffer.subarray(objectIdRaw.byteLength)
|
|
buffer = buffer.subarray(objectIdRaw.byteLength)
|
|
|
|
|
|
|
|
const hash = pdfStreamHash(buffer)
|
|
const hash = pdfStreamHash(buffer)
|
|
|
|
|
+ checkDeadline('after hash ' + idx)
|
|
|
const range = {
|
|
const range = {
|
|
|
objectId: objectIdRaw.toString(),
|
|
objectId: objectIdRaw.toString(),
|
|
|
start: object.offset + objectIdRaw.byteLength,
|
|
start: object.offset + objectIdRaw.byteLength,
|
|
@@ -92,12 +103,15 @@ async function update(contentDir, filePath, size) {
|
|
|
if (tracker.track(range)) continue
|
|
if (tracker.track(range)) continue
|
|
|
|
|
|
|
|
await writePdfStream(contentDir, hash, buffer)
|
|
await writePdfStream(contentDir, hash, buffer)
|
|
|
|
|
+ checkDeadline('after write ' + idx)
|
|
|
newRanges.push(range)
|
|
newRanges.push(range)
|
|
|
}
|
|
}
|
|
|
} finally {
|
|
} finally {
|
|
|
await handle.close()
|
|
await handle.close()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // NOTE: Bailing out below does not make sense.
|
|
|
|
|
+ // Let the next compile use the already written ranges.
|
|
|
const reclaimedSpace = await tracker.deleteStaleHashes(5)
|
|
const reclaimedSpace = await tracker.deleteStaleHashes(5)
|
|
|
await tracker.flush()
|
|
await tracker.flush()
|
|
|
return [ranges, newRanges, reclaimedSpace]
|
|
return [ranges, newRanges, reclaimedSpace]
|
|
@@ -219,6 +233,32 @@ async function writePdfStream(dir, hash, buffer) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function getDeadlineChecker(compileTime) {
|
|
|
|
|
+ const maxOverhead = Math.min(
|
|
|
|
|
+ // Adding 10s to a 40s compile time is OK.
|
|
|
|
|
+ // Adding 1s to a 3s compile time is OK.
|
|
|
|
|
+ Math.max(compileTime / 4, 1000),
|
|
|
|
|
+ // Adding 30s to a 120s compile time is not OK, limit to 10s.
|
|
|
|
|
+ Settings.pdfCachingMaxProcessingTime
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const deadline = Date.now() + maxOverhead
|
|
|
|
|
+ let lastStage = { stage: 'start', now: Date.now() }
|
|
|
|
|
+ let completedStages = 0
|
|
|
|
|
+ return function (stage) {
|
|
|
|
|
+ const now = Date.now()
|
|
|
|
|
+ if (now > deadline) {
|
|
|
|
|
+ throw new TimedOutError(stage, {
|
|
|
|
|
+ completedStages,
|
|
|
|
|
+ lastStage: lastStage.stage,
|
|
|
|
|
+ diffToLastStage: now - lastStage.now
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ completedStages++
|
|
|
|
|
+ lastStage = { stage, now }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function promiseMapWithLimit(concurrency, array, fn) {
|
|
function promiseMapWithLimit(concurrency, array, fn) {
|
|
|
const limit = pLimit(concurrency)
|
|
const limit = pLimit(concurrency)
|
|
|
return Promise.all(array.map((x) => limit(() => fn(x))))
|
|
return Promise.all(array.map((x) => limit(() => fn(x))))
|