Merge pull request #11069 from Snuffleupagus/getoplist-stream

Use streams for OperatorList chunking (issue 10023)
This commit is contained in:
Tim van der Meij 2019-08-24 19:31:00 +02:00 committed by GitHub
commit 56ae7a6690
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 140 additions and 94 deletions

View File

@ -195,7 +195,7 @@ class Page {
}); });
} }
getOperatorList({ handler, task, intent, renderInteractiveForms, }) { getOperatorList({ handler, sink, task, intent, renderInteractiveForms, }) {
const contentStreamPromise = this.pdfManager.ensure(this, const contentStreamPromise = this.pdfManager.ensure(this,
'getContentStream'); 'getContentStream');
const resourcesPromise = this.loadResources([ const resourcesPromise = this.loadResources([
@ -220,7 +220,7 @@ class Page {
const dataPromises = Promise.all([contentStreamPromise, resourcesPromise]); const dataPromises = Promise.all([contentStreamPromise, resourcesPromise]);
const pageListPromise = dataPromises.then(([contentStream]) => { const pageListPromise = dataPromises.then(([contentStream]) => {
const opList = new OperatorList(intent, handler, this.pageIndex); const opList = new OperatorList(intent, sink, this.pageIndex);
handler.send('StartRenderPage', { handler.send('StartRenderPage', {
transparency: partialEvaluator.hasBlendModes(this.resources), transparency: partialEvaluator.hasBlendModes(this.resources),
@ -244,7 +244,7 @@ class Page {
function([pageOpList, annotations]) { function([pageOpList, annotations]) {
if (annotations.length === 0) { if (annotations.length === 0) {
pageOpList.flush(true); pageOpList.flush(true);
return pageOpList; return { length: pageOpList.totalLength, };
} }
// Collect the operator list promises for the annotations. Each promise // Collect the operator list promises for the annotations. Each promise
@ -264,7 +264,7 @@ class Page {
} }
pageOpList.addOp(OPS.endAnnotations, []); pageOpList.addOp(OPS.endAnnotations, []);
pageOpList.flush(true); pageOpList.flush(true);
return pageOpList; return { length: pageOpList.totalLength, };
}); });
}); });
} }

View File

@ -541,6 +541,9 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
operatorList.addDependencies(tilingOpList.dependencies); operatorList.addDependencies(tilingOpList.dependencies);
operatorList.addOp(fn, tilingPatternIR); operatorList.addOp(fn, tilingPatternIR);
}, (reason) => { }, (reason) => {
if (reason instanceof AbortException) {
return;
}
if (this.options.ignoreErrors) { if (this.options.ignoreErrors) {
// Error(s) in the TilingPattern -- sending unsupported feature // Error(s) in the TilingPattern -- sending unsupported feature
// notification and allow rendering to continue. // notification and allow rendering to continue.
@ -918,8 +921,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
} }
return new Promise(function promiseBody(resolve, reject) { return new Promise(function promiseBody(resolve, reject) {
var next = function (promise) { let next = function(promise) {
promise.then(function () { Promise.all([promise, operatorList.ready]).then(function () {
try { try {
promiseBody(resolve, reject); promiseBody(resolve, reject);
} catch (ex) { } catch (ex) {
@ -1000,6 +1003,9 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
} }
resolveXObject(); resolveXObject();
}).catch(function(reason) { }).catch(function(reason) {
if (reason instanceof AbortException) {
return;
}
if (self.options.ignoreErrors) { if (self.options.ignoreErrors) {
// Error(s) in the XObject -- sending unsupported feature // Error(s) in the XObject -- sending unsupported feature
// notification and allow rendering to continue. // notification and allow rendering to continue.
@ -1230,6 +1236,9 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
closePendingRestoreOPS(); closePendingRestoreOPS();
resolve(); resolve();
}).catch((reason) => { }).catch((reason) => {
if (reason instanceof AbortException) {
return;
}
if (this.options.ignoreErrors) { if (this.options.ignoreErrors) {
// Error(s) in the OperatorList -- sending unsupported feature // Error(s) in the OperatorList -- sending unsupported feature
// notification and allow rendering to continue. // notification and allow rendering to continue.

View File

@ -541,11 +541,11 @@ var OperatorList = (function OperatorListClosure() {
var CHUNK_SIZE = 1000; var CHUNK_SIZE = 1000;
var CHUNK_SIZE_ABOUT = CHUNK_SIZE - 5; // close to chunk size var CHUNK_SIZE_ABOUT = CHUNK_SIZE - 5; // close to chunk size
function OperatorList(intent, messageHandler, pageIndex) { function OperatorList(intent, streamSink, pageIndex) {
this.messageHandler = messageHandler; this._streamSink = streamSink;
this.fnArray = []; this.fnArray = [];
this.argsArray = []; this.argsArray = [];
if (messageHandler && intent !== 'oplist') { if (streamSink && intent !== 'oplist') {
this.optimizer = new QueueOptimizer(this); this.optimizer = new QueueOptimizer(this);
} else { } else {
this.optimizer = new NullOptimizer(this); this.optimizer = new NullOptimizer(this);
@ -555,6 +555,7 @@ var OperatorList = (function OperatorListClosure() {
this.pageIndex = pageIndex; this.pageIndex = pageIndex;
this.intent = intent; this.intent = intent;
this.weight = 0; this.weight = 0;
this._resolved = streamSink ? null : Promise.resolve();
} }
OperatorList.prototype = { OperatorList.prototype = {
@ -562,6 +563,10 @@ var OperatorList = (function OperatorListClosure() {
return this.argsArray.length; return this.argsArray.length;
}, },
get ready() {
return this._resolved || this._streamSink.ready;
},
/** /**
* @returns {number} The total length of the entire operator list, * @returns {number} The total length of the entire operator list,
* since `this.length === 0` after flushing. * since `this.length === 0` after flushing.
@ -573,7 +578,7 @@ var OperatorList = (function OperatorListClosure() {
addOp(fn, args) { addOp(fn, args) {
this.optimizer.push(fn, args); this.optimizer.push(fn, args);
this.weight++; this.weight++;
if (this.messageHandler) { if (this._streamSink) {
if (this.weight >= CHUNK_SIZE) { if (this.weight >= CHUNK_SIZE) {
this.flush(); this.flush();
} else if (this.weight >= CHUNK_SIZE_ABOUT && } else if (this.weight >= CHUNK_SIZE_ABOUT &&
@ -642,7 +647,7 @@ var OperatorList = (function OperatorListClosure() {
const length = this.length; const length = this.length;
this._totalLength += length; this._totalLength += length;
this.messageHandler.send('RenderPageChunk', { this._streamSink.enqueue({
operatorList: { operatorList: {
fnArray: this.fnArray, fnArray: this.fnArray,
argsArray: this.argsArray, argsArray: this.argsArray,
@ -651,7 +656,7 @@ var OperatorList = (function OperatorListClosure() {
}, },
pageIndex: this.pageIndex, pageIndex: this.pageIndex,
intent: this.intent, intent: this.intent,
}, this._transfers); }, 1, this._transfers);
this.dependencies = Object.create(null); this.dependencies = Object.create(null);
this.fnArray.length = 0; this.fnArray.length = 0;

View File

@ -466,10 +466,10 @@ var WorkerMessageHandler = {
}); });
}); });
handler.on('RenderPageRequest', function wphSetupRenderPage(data) { handler.on('GetOperatorList', function wphSetupRenderPage(data, sink) {
var pageIndex = data.pageIndex; var pageIndex = data.pageIndex;
pdfManager.getPage(pageIndex).then(function(page) { pdfManager.getPage(pageIndex).then(function(page) {
var task = new WorkerTask('RenderPageRequest: page ' + pageIndex); var task = new WorkerTask(`GetOperatorList: page ${pageIndex}`);
startWorkerTask(task); startWorkerTask(task);
// NOTE: Keep this condition in sync with the `info` helper function. // NOTE: Keep this condition in sync with the `info` helper function.
@ -478,55 +478,32 @@ var WorkerMessageHandler = {
// Pre compile the pdf page and fetch the fonts/images. // Pre compile the pdf page and fetch the fonts/images.
page.getOperatorList({ page.getOperatorList({
handler, handler,
sink,
task, task,
intent: data.intent, intent: data.intent,
renderInteractiveForms: data.renderInteractiveForms, renderInteractiveForms: data.renderInteractiveForms,
}).then(function(operatorList) { }).then(function(operatorListInfo) {
finishWorkerTask(task); finishWorkerTask(task);
if (start) { if (start) {
info(`page=${pageIndex + 1} - getOperatorList: time=` + info(`page=${pageIndex + 1} - getOperatorList: time=` +
`${Date.now() - start}ms, len=${operatorList.totalLength}`); `${Date.now() - start}ms, len=${operatorListInfo.length}`);
} }
}, function(e) { sink.close();
}, function(reason) {
finishWorkerTask(task); finishWorkerTask(task);
if (task.terminated) { if (task.terminated) {
return; // ignoring errors from the terminated thread return; // ignoring errors from the terminated thread
} }
// For compatibility with older behavior, generating unknown // For compatibility with older behavior, generating unknown
// unsupported feature notification on errors. // unsupported feature notification on errors.
handler.send('UnsupportedFeature', handler.send('UnsupportedFeature',
{ featureId: UNSUPPORTED_FEATURES.unknown, }); { featureId: UNSUPPORTED_FEATURES.unknown, });
var minimumStackMessage = sink.error(reason);
'worker.js: while trying to getPage() and getOperatorList()';
var wrappedException; // TODO: Should `reason` be re-thrown here (currently that casues
// "Uncaught exception: ..." messages in the console)?
// Turn the error into an obj that can be serialized
if (typeof e === 'string') {
wrappedException = {
message: e,
stack: minimumStackMessage,
};
} else if (typeof e === 'object') {
wrappedException = {
message: e.message || e.toString(),
stack: e.stack || minimumStackMessage,
};
} else {
wrappedException = {
message: 'Unknown exception type: ' + (typeof e),
stack: minimumStackMessage,
};
}
handler.send('PageError', {
pageIndex,
error: wrappedException,
intent: data.intent,
});
}); });
}); });
}, this); }, this);
@ -563,7 +540,9 @@ var WorkerMessageHandler = {
return; // ignoring errors from the terminated thread return; // ignoring errors from the terminated thread
} }
sink.error(reason); sink.error(reason);
throw reason;
// TODO: Should `reason` be re-thrown here (currently that casues
// "Uncaught exception: ..." messages in the console)?
}); });
}); });
}); });

View File

@ -1032,7 +1032,7 @@ class PDFPageProxy {
}; };
stats.time('Page Request'); stats.time('Page Request');
this._transport.messageHandler.send('RenderPageRequest', { this._pumpOperatorList({
pageIndex: this.pageNumber - 1, pageIndex: this.pageNumber - 1,
intent: renderingIntent, intent: renderingIntent,
renderInteractiveForms: renderInteractiveForms === true, renderInteractiveForms: renderInteractiveForms === true,
@ -1054,6 +1054,11 @@ class PDFPageProxy {
if (error) { if (error) {
internalRenderTask.capability.reject(error); internalRenderTask.capability.reject(error);
this._abortOperatorList({
intentState,
reason: error,
});
} else { } else {
internalRenderTask.capability.resolve(); internalRenderTask.capability.resolve();
} }
@ -1135,7 +1140,7 @@ class PDFPageProxy {
}; };
this._stats.time('Page Request'); this._stats.time('Page Request');
this._transport.messageHandler.send('RenderPageRequest', { this._pumpOperatorList({
pageIndex: this.pageIndex, pageIndex: this.pageIndex,
intent: renderingIntent, intent: renderingIntent,
}); });
@ -1201,19 +1206,25 @@ class PDFPageProxy {
this._transport.pageCache[this.pageIndex] = null; this._transport.pageCache[this.pageIndex] = null;
const waitOn = []; const waitOn = [];
Object.keys(this.intentStates).forEach(function(intent) { Object.keys(this.intentStates).forEach((intent) => {
const intentState = this.intentStates[intent];
this._abortOperatorList({
intentState,
reason: new Error('Page was destroyed.'),
force: true,
});
if (intent === 'oplist') { if (intent === 'oplist') {
// Avoid errors below, since the renderTasks are just stubs. // Avoid errors below, since the renderTasks are just stubs.
return; return;
} }
const intentState = this.intentStates[intent];
intentState.renderTasks.forEach(function(renderTask) { intentState.renderTasks.forEach(function(renderTask) {
const renderCompleted = renderTask.capability.promise. const renderCompleted = renderTask.capability.promise.
catch(function() {}); // ignoring failures catch(function() {}); // ignoring failures
waitOn.push(renderCompleted); waitOn.push(renderCompleted);
renderTask.cancel(); renderTask.cancel();
}); });
}, this); });
this.objs.clear(); this.objs.clear();
this.annotationsPromise = null; this.annotationsPromise = null;
this.pendingCleanup = false; this.pendingCleanup = false;
@ -1273,8 +1284,7 @@ class PDFPageProxy {
* For internal use only. * For internal use only.
* @ignore * @ignore
*/ */
_renderPageChunk(operatorListChunk, intent) { _renderPageChunk(operatorListChunk, intentState) {
const intentState = this.intentStates[intent];
// Add the new chunk to the current operator list. // Add the new chunk to the current operator list.
for (let i = 0, ii = operatorListChunk.length; i < ii; i++) { for (let i = 0, ii = operatorListChunk.length; i < ii; i++) {
intentState.operatorList.fnArray.push(operatorListChunk.fnArray[i]); intentState.operatorList.fnArray.push(operatorListChunk.fnArray[i]);
@ -1293,6 +1303,86 @@ class PDFPageProxy {
} }
} }
/**
* For internal use only.
* @ignore
*/
_pumpOperatorList(args) {
assert(args.intent,
'PDFPageProxy._pumpOperatorList: Expected "intent" argument.');
const readableStream =
this._transport.messageHandler.sendWithStream('GetOperatorList', args);
const reader = readableStream.getReader();
const intentState = this.intentStates[args.intent];
intentState.streamReader = reader;
const pump = () => {
reader.read().then(({ value, done, }) => {
if (done) {
intentState.streamReader = null;
return;
}
if (this._transport.destroyed) {
return; // Ignore any pending requests if the worker was terminated.
}
this._renderPageChunk(value.operatorList, intentState);
pump();
}, (reason) => {
intentState.streamReader = null;
if (this._transport.destroyed) {
return; // Ignore any pending requests if the worker was terminated.
}
if (intentState.operatorList) {
// Mark operator list as complete.
intentState.operatorList.lastChunk = true;
for (let i = 0; i < intentState.renderTasks.length; i++) {
intentState.renderTasks[i].operatorListChanged();
}
this._tryCleanup();
}
if (intentState.displayReadyCapability) {
intentState.displayReadyCapability.reject(reason);
} else if (intentState.opListReadCapability) {
intentState.opListReadCapability.reject(reason);
} else {
throw reason;
}
});
};
pump();
}
/**
* For internal use only.
* @ignore
*/
_abortOperatorList({ intentState, reason, force = false, }) {
assert(reason instanceof Error,
'PDFPageProxy._abortOperatorList: Expected "reason" argument.');
if (!intentState.streamReader) {
return;
}
if (!force && intentState.renderTasks.length !== 0) {
// Ensure that an Error occuring in *only* one `InternalRenderTask`, e.g.
// multiple render() calls on the same canvas, won't break all rendering.
return;
}
if (reason instanceof RenderingCancelledException) {
// Aborting parsing on the worker-thread when rendering is cancelled will
// break subsequent rendering operations. TODO: Remove this restriction.
return;
}
intentState.streamReader.cancel(
new AbortException(reason && reason.message));
intentState.streamReader = null;
}
/** /**
* @return {Object} Returns page stats, if enabled. * @return {Object} Returns page stats, if enabled.
*/ */
@ -1955,15 +2045,6 @@ class WorkerTransport {
page._startRenderPage(data.transparency, data.intent); page._startRenderPage(data.transparency, data.intent);
}, this); }, this);
messageHandler.on('RenderPageChunk', function(data) {
if (this.destroyed) {
return; // Ignore any pending requests if the worker was terminated.
}
const page = this.pageCache[data.pageIndex];
page._renderPageChunk(data.operatorList, data.intent);
}, this);
messageHandler.on('commonobj', function(data) { messageHandler.on('commonobj', function(data) {
if (this.destroyed) { if (this.destroyed) {
return; // Ignore any pending requests if the worker was terminated. return; // Ignore any pending requests if the worker was terminated.
@ -2083,33 +2164,6 @@ class WorkerTransport {
} }
}, this); }, this);
messageHandler.on('PageError', function(data) {
if (this.destroyed) {
return; // Ignore any pending requests if the worker was terminated.
}
const page = this.pageCache[data.pageIndex];
const intentState = page.intentStates[data.intent];
if (intentState.operatorList) {
// Mark operator list as complete.
intentState.operatorList.lastChunk = true;
for (let i = 0; i < intentState.renderTasks.length; i++) {
intentState.renderTasks[i].operatorListChanged();
}
page._tryCleanup();
}
if (intentState.displayReadyCapability) {
intentState.displayReadyCapability.reject(new Error(data.error));
} else if (intentState.opListReadCapability) {
intentState.opListReadCapability.reject(new Error(data.error));
} else {
throw new Error(data.error);
}
}, this);
messageHandler.on('UnsupportedFeature', this._onUnsupportedFeature, this); messageHandler.on('UnsupportedFeature', this._onUnsupportedFeature, this);
messageHandler.on('JpegDecode', function(data) { messageHandler.on('JpegDecode', function(data) {

View File

@ -320,13 +320,12 @@ describe('evaluator', function() {
}); });
describe('operator list', function () { describe('operator list', function () {
function MessageHandlerMock() { } class StreamSinkMock {
MessageHandlerMock.prototype = { enqueue() { }
send() { }, }
};
it('should get correct total length after flushing', function () { it('should get correct total length after flushing', function () {
var operatorList = new OperatorList(null, new MessageHandlerMock()); var operatorList = new OperatorList(null, new StreamSinkMock());
operatorList.addOp(OPS.save, null); operatorList.addOp(OPS.save, null);
operatorList.addOp(OPS.restore, null); operatorList.addOp(OPS.restore, null);