From 58c3ea08202becf007c304512c44726719acb508 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Tue, 20 Oct 2015 20:50:32 -0500 Subject: [PATCH] Adds thread abort capabilities. --- src/core/annotation.js | 17 ++++---- src/core/core.js | 11 ++--- src/core/evaluator.js | 63 ++++++++++++++++------------- src/core/worker.js | 81 +++++++++++++++++++++++++++++++++---- src/display/api.js | 15 ++++++- test/unit/evaluator_spec.js | 52 ++++++++++++++++++++++-- 6 files changed, 186 insertions(+), 53 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 5844f2e4d..f08d22d3b 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -281,7 +281,7 @@ var Annotation = (function AnnotationClosure() { }.bind(this)); }, - getOperatorList: function Annotation_getOperatorList(evaluator) { + getOperatorList: function Annotation_getOperatorList(evaluator, task) { if (!this.appearance) { return Promise.resolve(new OperatorList()); @@ -308,7 +308,8 @@ var Annotation = (function AnnotationClosure() { return resourcesPromise.then(function(resources) { var opList = new OperatorList(); opList.addOp(OPS.beginAnnotation, [data.rect, transform, matrix]); - return evaluator.getOperatorList(self.appearance, resources, opList). + return evaluator.getOperatorList(self.appearance, task, + resources, opList). then(function () { opList.addOp(OPS.endAnnotation, []); self.appearance.reset(); @@ -319,7 +320,7 @@ var Annotation = (function AnnotationClosure() { }; Annotation.appendToOperatorList = function Annotation_appendToOperatorList( - annotations, opList, pdfManager, partialEvaluator, intent) { + annotations, opList, pdfManager, partialEvaluator, task, intent) { function reject(e) { annotationsReadyCapability.reject(e); @@ -332,7 +333,7 @@ var Annotation = (function AnnotationClosure() { if (intent === 'display' && annotations[i].isViewable() || intent === 'print' && annotations[i].isPrintable()) { annotationPromises.push( - annotations[i].getOperatorList(partialEvaluator)); + annotations[i].getOperatorList(partialEvaluator, task)); } } Promise.all(annotationPromises).then(function(datas) { @@ -564,9 +565,10 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { } Util.inherit(TextWidgetAnnotation, WidgetAnnotation, { - getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator) { + getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator, + task) { if (this.appearance) { - return Annotation.prototype.getOperatorList.call(this, evaluator); + return Annotation.prototype.getOperatorList.call(this, evaluator, task); } var opList = new OperatorList(); @@ -579,7 +581,8 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { } var stream = new Stream(stringToBytes(data.defaultAppearance)); - return evaluator.getOperatorList(stream, this.fieldResources, opList). + return evaluator.getOperatorList(stream, task, + this.fieldResources, opList). then(function () { return opList; }); diff --git a/src/core/core.js b/src/core/core.js index e18824877..2342e3199 100644 --- a/src/core/core.js +++ b/src/core/core.js @@ -161,7 +161,7 @@ var Page = (function PageClosure() { }.bind(this)); }, - getOperatorList: function Page_getOperatorList(handler, intent) { + getOperatorList: function Page_getOperatorList(handler, task, intent) { var self = this; var pdfManager = this.pdfManager; @@ -194,8 +194,8 @@ var Page = (function PageClosure() { pageIndex: self.pageIndex, intent: intent }); - return partialEvaluator.getOperatorList(contentStream, self.resources, - opList).then(function () { + return partialEvaluator.getOperatorList(contentStream, task, + self.resources, opList).then(function () { return opList; }); }); @@ -212,7 +212,7 @@ var Page = (function PageClosure() { } var annotationsReadyPromise = Annotation.appendToOperatorList( - annotations, pageOpList, pdfManager, partialEvaluator, intent); + annotations, pageOpList, pdfManager, partialEvaluator, task, intent); return annotationsReadyPromise.then(function () { pageOpList.flush(true); return pageOpList; @@ -220,7 +220,7 @@ var Page = (function PageClosure() { }); }, - extractTextContent: function Page_extractTextContent() { + extractTextContent: function Page_extractTextContent(task) { var handler = { on: function nullHandlerOn() {}, send: function nullHandlerSend() {} @@ -249,6 +249,7 @@ var Page = (function PageClosure() { self.fontCache); return partialEvaluator.getTextContent(contentStream, + task, self.resources); }); }, diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 973f93eea..41f6571a7 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -125,6 +125,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { buildFormXObject: function PartialEvaluator_buildFormXObject(resources, xobj, smask, operatorList, + task, initialState) { var matrix = xobj.dict.getArray('Matrix'); var bbox = xobj.dict.getArray('BBox'); @@ -157,7 +158,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { operatorList.addOp(OPS.paintFormXObjectBegin, [matrix, bbox]); - return this.getOperatorList(xobj, + return this.getOperatorList(xobj, task, (xobj.dict.get('Resources') || resources), operatorList, initialState). then(function () { operatorList.addOp(OPS.paintFormXObjectEnd, []); @@ -269,7 +270,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, handleSMask: function PartialEvaluator_handleSmask(smask, resources, - operatorList, + operatorList, task, stateManager) { var smaskContent = smask.get('G'); var smaskOptions = { @@ -277,13 +278,13 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { backdrop: smask.get('BC') }; return this.buildFormXObject(resources, smaskContent, smaskOptions, - operatorList, stateManager.state.clone()); + operatorList, task, stateManager.state.clone()); }, handleTilingType: function PartialEvaluator_handleTilingType(fn, args, resources, pattern, patternDict, - operatorList) { + operatorList, task) { // Create an IR of the pattern code. var tilingOpList = new OperatorList(); // Merge the available resources, to prevent issues when the patternDict @@ -291,8 +292,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var resourcesArray = [patternDict.get('Resources'), resources]; var patternResources = Dict.merge(this.xref, resourcesArray); - return this.getOperatorList(pattern, patternResources, tilingOpList).then( - function () { + return this.getOperatorList(pattern, task, patternResources, + tilingOpList).then(function () { // Add the dependencies to the parent operator list so they are // resolved before sub operator list is executed synchronously. operatorList.addDependencies(tilingOpList.dependencies); @@ -305,7 +306,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { handleSetFont: function PartialEvaluator_handleSetFont(resources, fontArgs, fontRef, - operatorList, state) { + operatorList, task, state) { // TODO(mack): Not needed? var fontName; if (fontArgs) { @@ -319,8 +320,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (!translated.font.isType3Font) { return translated; } - return translated.loadType3Data(self, resources, operatorList).then( - function () { + return translated.loadType3Data(self, resources, operatorList, task). + then(function () { return translated; }); }).then(function (translated) { @@ -367,8 +368,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, setGState: function PartialEvaluator_setGState(resources, gState, - operatorList, xref, - stateManager) { + operatorList, task, + xref, stateManager) { // This array holds the converted/processed state data. var gStateObj = []; var gStateMap = gState.map; @@ -392,8 +393,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { break; case 'Font': promise = promise.then(function () { - return self.handleSetFont(resources, null, value[0], - operatorList, stateManager.state). + return self.handleSetFont(resources, null, value[0], operatorList, + task, stateManager.state). then(function (loadedName) { operatorList.addDependency(loadedName); gStateObj.push([key, [loadedName, value[1]]]); @@ -412,7 +413,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (isDict(dict)) { promise = promise.then(function () { return self.handleSMask(dict, resources, operatorList, - stateManager); + task, stateManager); }); gStateObj.push([key, true]); } else { @@ -593,7 +594,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, handleColorN: function PartialEvaluator_handleColorN(operatorList, fn, args, - cs, patterns, resources, xref) { + cs, patterns, resources, task, xref) { // compile tiling patterns var patternName = args[args.length - 1]; // SCN/scn applies patterns along with normal colors @@ -606,7 +607,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (typeNum === TILING_PATTERN) { var color = cs.base ? cs.base.getRgb(args, 0) : null; return this.handleTilingType(fn, color, resources, pattern, - dict, operatorList); + dict, operatorList, task); } else if (typeNum === SHADING_PATTERN) { var shading = dict.get('Shading'); var matrix = dict.get('Matrix'); @@ -623,6 +624,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, getOperatorList: function PartialEvaluator_getOperatorList(stream, + task, resources, operatorList, initialState) { @@ -641,6 +643,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var timeSlotManager = new TimeSlotManager(); return new Promise(function next(resolve, reject) { + task.ensureNotTerminated(); timeSlotManager.reset(); var stop, operation = {}, i, ii, cs; while (!(stop = timeSlotManager.check())) { @@ -683,7 +686,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (type.name === 'Form') { stateManager.save(); return self.buildFormXObject(resources, xobj, null, - operatorList, + operatorList, task, stateManager.state.clone()). then(function () { stateManager.restore(); @@ -707,8 +710,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { case OPS.setFont: var fontSize = args[1]; // eagerly collect all fonts - return self.handleSetFont(resources, args, null, - operatorList, stateManager.state). + return self.handleSetFont(resources, args, null, operatorList, + task, stateManager.state). then(function (loadedName) { operatorList.addDependency(loadedName); operatorList.addOp(OPS.setFont, [loadedName, fontSize]); @@ -814,7 +817,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { cs = stateManager.state.fillColorSpace; if (cs.name === 'Pattern') { return self.handleColorN(operatorList, OPS.setFillColorN, - args, cs, patterns, resources, xref).then(function() { + args, cs, patterns, resources, task, xref).then(function() { next(resolve, reject); }, reject); } @@ -825,7 +828,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { cs = stateManager.state.strokeColorSpace; if (cs.name === 'Pattern') { return self.handleColorN(operatorList, OPS.setStrokeColorN, - args, cs, patterns, resources, xref).then(function() { + args, cs, patterns, resources, task, xref).then(function() { next(resolve, reject); }, reject); } @@ -859,8 +862,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } var gState = extGState.get(dictName.name); - return self.setGState(resources, gState, operatorList, xref, - stateManager).then(function() { + return self.setGState(resources, gState, operatorList, task, + xref, stateManager).then(function() { next(resolve, reject); }, reject); case OPS.moveTo: @@ -898,7 +901,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (stop) { deferred.then(function () { next(resolve, reject); - }); + }, reject); return; } // Some PDFs don't close all restores inside object/form. @@ -910,7 +913,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }); }, - getTextContent: function PartialEvaluator_getTextContent(stream, resources, + getTextContent: function PartialEvaluator_getTextContent(stream, task, + resources, stateManager) { stateManager = (stateManager || new StateManager(new TextState())); @@ -1088,6 +1092,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var timeSlotManager = new TimeSlotManager(); return new Promise(function next(resolve, reject) { + task.ensureNotTerminated(); timeSlotManager.reset(); var stop, operation = {}, args = []; while (!(stop = timeSlotManager.check())) { @@ -1243,7 +1248,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { stateManager.transform(matrix); } - return self.getTextContent(xobj, + return self.getTextContent(xobj, task, xobj.dict.get('Resources') || resources, stateManager). then(function (formTextContent) { Util.appendToArray(bidiTexts, formTextContent.items); @@ -1283,7 +1288,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (stop) { deferred.then(function () { next(resolve, reject); - }); + }, reject); return; } resolve(textContent); @@ -1848,7 +1853,7 @@ var TranslatedFont = (function TranslatedFontClosure() { ]); this.sent = true; }, - loadType3Data: function (evaluator, resources, parentOperatorList) { + loadType3Data: function (evaluator, resources, parentOperatorList, task) { assert(this.font.isType3Font); if (this.type3Loaded) { @@ -1865,7 +1870,7 @@ var TranslatedFont = (function TranslatedFontClosure() { loadCharProcsPromise = loadCharProcsPromise.then(function (key) { var glyphStream = charProcs[key]; var operatorList = new OperatorList(); - return evaluator.getOperatorList(glyphStream, fontResources, + return evaluator.getOperatorList(glyphStream, task, fontResources, operatorList).then(function () { charProcOperatorList[key] = operatorList.getIR(); diff --git a/src/core/worker.js b/src/core/worker.js index 9fc3ea440..7440534e7 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -22,11 +22,42 @@ 'use strict'; +var WorkerTask = (function WorkerTaskClosure() { + function WorkerTask(name) { + this.name = name; + this.terminated = false; + this._capability = createPromiseCapability(); + } + + WorkerTask.prototype = { + get finished() { + return this._capability.promise; + }, + + finish: function () { + this._capability.resolve(); + }, + + terminate: function () { + this.terminated = true; + }, + + ensureNotTerminated: function () { + if (this.terminated) { + throw new Error('Worker task was terminated'); + } + } + }; + + return WorkerTask; +})(); + var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { setup: function wphSetup(handler) { var pdfManager; var terminated = false; var cancelXHRs = null; + var WorkerTasks = []; function ensureNotTerminated() { if (terminated) { @@ -34,6 +65,16 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { } } + function startWorkerTask(task) { + WorkerTasks.push(task); + } + + function finishWorkerTask(task) { + task.finish(); + var i = WorkerTasks.indexOf(task); + WorkerTasks.splice(i, 1); + } + function loadDocument(recoveryMode) { var loadDocumentCapability = createPromiseCapability(); @@ -413,17 +454,25 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { }); handler.on('RenderPageRequest', function wphSetupRenderPage(data) { - pdfManager.getPage(data.pageIndex).then(function(page) { + var pageIndex = data.pageIndex; + pdfManager.getPage(pageIndex).then(function(page) { + var task = new WorkerTask('RenderPageRequest: page ' + pageIndex); + startWorkerTask(task); - var pageNum = data.pageIndex + 1; + var pageNum = pageIndex + 1; var start = Date.now(); // Pre compile the pdf page and fetch the fonts/images. - page.getOperatorList(handler, data.intent).then(function(operatorList) { + page.getOperatorList(handler, task, data.intent).then( + function(operatorList) { + finishWorkerTask(task); info('page=' + pageNum + ' - getOperatorList: time=' + (Date.now() - start) + 'ms, len=' + operatorList.fnArray.length); - }, function(e) { + finishWorkerTask(task); + if (task.terminated) { + return; // ignoring errors from the terminated thread + } var minimumStackMessage = 'worker.js: while trying to getPage() and getOperatorList()'; @@ -458,13 +507,23 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { }, this); handler.on('GetTextContent', function wphExtractText(data) { - return pdfManager.getPage(data.pageIndex).then(function(page) { - var pageNum = data.pageIndex + 1; + var pageIndex = data.pageIndex; + return pdfManager.getPage(pageIndex).then(function(page) { + var task = new WorkerTask('GetTextContent: page ' + pageIndex); + startWorkerTask(task); + var pageNum = pageIndex + 1; var start = Date.now(); - return page.extractTextContent().then(function(textContent) { + return page.extractTextContent(task).then(function(textContent) { + finishWorkerTask(task); info('text indexing: page=' + pageNum + ' - time=' + (Date.now() - start) + 'ms'); return textContent; + }, function (reason) { + finishWorkerTask(task); + if (task.terminated) { + return; // ignoring errors from the terminated thread + } + throw reason; }); }); }); @@ -482,6 +541,14 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { if (cancelXHRs) { cancelXHRs(); } + + var waitOn = []; + WorkerTasks.forEach(function (task) { + waitOn.push(task.finished); + task.terminate(); + }); + + return Promise.all(waitOn).then(function () {}); }); } }; diff --git a/src/display/api.js b/src/display/api.js index 08b02caf1..59efee3ba 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -883,15 +883,20 @@ var PDFPageProxy = (function PDFPageProxyClosure() { this.destroyed = true; this.transport.pageCache[this.pageIndex] = null; + var waitOn = []; Object.keys(this.intentStates).forEach(function(intent) { var intentState = this.intentStates[intent]; intentState.renderTasks.forEach(function(renderTask) { + var renderCompleted = renderTask.capability.promise. + catch(function () {}); // ignoring failures + waitOn.push(renderCompleted); renderTask.cancel(); }); }, this); this.objs.clear(); this.annotationsPromise = null; this.pendingCleanup = false; + return Promise.all(waitOn); }, /** @@ -1054,15 +1059,21 @@ var WorkerTransport = (function WorkerTransportClosure() { this.destroyed = true; this.destroyCapability = createPromiseCapability(); + var waitOn = []; + // We need to wait for all renderings to be completed, e.g. + // timeout/rAF can take a long time. this.pageCache.forEach(function (page) { if (page) { - page._destroy(); + waitOn.push(page._destroy()); } }); this.pageCache = []; this.pagePromises = []; var self = this; - this.messageHandler.sendWithPromise('Terminate', null).then(function () { + // We also need to wait for the worker to finish its long running tasks. + var terminated = this.messageHandler.sendWithPromise('Terminate', null); + waitOn.push(terminated); + Promise.all(waitOn).then(function () { FontLoader.clear(); if (self.worker) { self.worker.terminate(); diff --git a/test/unit/evaluator_spec.js b/test/unit/evaluator_spec.js index 5bf007d0b..5e21122c5 100644 --- a/test/unit/evaluator_spec.js +++ b/test/unit/evaluator_spec.js @@ -1,13 +1,13 @@ /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* globals expect, it, describe, PartialEvaluator, StringStream, OPS, - OperatorList, waitsFor, runs, Dict, Name, Stream */ + OperatorList, waitsFor, runs, Dict, Name, Stream, WorkerTask */ 'use strict'; describe('evaluator', function() { function XrefMock(queue) { - this.queue = queue; + this.queue = queue || []; } XrefMock.prototype = { fetchIfRef: function() { @@ -35,7 +35,9 @@ describe('evaluator', function() { var done = false; runs(function () { var result = new OperatorList(); - evaluator.getOperatorList(stream, resources, result).then(function () { + var task = new WorkerTask('OperatorListCheck'); + evaluator.getOperatorList(stream, task, resources, result).then( + function () { check(result); done = true; }); @@ -259,4 +261,48 @@ describe('evaluator', function() { }); }); }); + + describe('thread control', function() { + it('should abort operator list parsing', function () { + var evaluator = new PartialEvaluator(new PdfManagerMock(), + new XrefMock(), new HandlerMock(), + 'prefix'); + var stream = new StringStream('qqQQ'); + var resources = new ResourcesMock(); + var done = false; + runs(function () { + var result = new OperatorList(); + var task = new WorkerTask('OperatorListAbort'); + task.terminate(); + evaluator.getOperatorList(stream, task, resources, result).catch( + function () { + done = true; + expect(!!result.fnArray && !!result.argsArray).toEqual(true); + expect(result.fnArray.length).toEqual(0); + }); + }); + waitsFor(function () { + return done; + }); + }); + it('should abort text parsing parsing', function () { + var resources = new ResourcesMock(); + var evaluator = new PartialEvaluator(new PdfManagerMock(), + new XrefMock(), new HandlerMock(), + 'prefix'); + var stream = new StringStream('qqQQ'); + var done = false; + runs(function () { + var task = new WorkerTask('TextContentAbort'); + task.terminate(); + evaluator.getTextContent(stream, task, resources).catch( + function () { + done = true; + }); + }); + waitsFor(function () { + return done; + }); + }); + }); });