diff --git a/www/index.html b/www/index.html index 49316447..795831a1 100644 --- a/www/index.html +++ b/www/index.html @@ -884,6 +884,8 @@
+
+
diff --git a/www/js/app.js b/www/js/app.js index 0f831b3a..cfadbe9b 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1626,7 +1626,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett */ function refreshAPIStatus() { var apiStatusPanel = document.getElementById('apiStatusDiv'); - apiStatusPanel.classList.remove('panel-success', 'panel-warning'); + apiStatusPanel.classList.remove('panel-success', 'panel-warning', 'panel-danger'); var apiPanelClass = 'panel-success'; if (isMessageChannelAvailable()) { $('#messageChannelStatus').html("MessageChannel API available"); @@ -1655,7 +1655,28 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") .addClass("apiUnavailable"); } - apiStatusPanel.classList.add(apiPanelClass); + + // Update Settings Store section of API panel with API name + var settingsStoreStatusDiv = document.getElementById('settingsStoreStatus'); + var apiName = params.storeType === 'cookie' ? 'Cookie' : params.storeType === 'local_storage' ? 'Local Storage' : 'None'; + settingsStoreStatusDiv.innerHTML = 'Settings Storage API in use: ' + apiName; + settingsStoreStatusDiv.classList.remove('apiAvailable', 'apiUnavailable'); + settingsStoreStatusDiv.classList.add(params.storeType === 'none' ? 'apiUnavailable' : 'apiAvailable'); + apiPanelClass = params.storeType === 'none' ? 'panel-warning' : apiPanelClass; + + // Update Decompressor API section of panel + var decompAPIStatusDiv = document.getElementById('decompressorAPIStatus'); + apiName = params.decompressorAPI.assemblerMachineType; + if (apiName && params.decompressorAPI.decompressorLastUsed) { + apiName += ' [ ' + params.decompressorAPI.decompressorLastUsed + ' ]'; + } + apiPanelClass = params.decompressorAPI.errorStatus ? 'panel-danger' : apiName ? apiPanelClass : 'panel-warning'; + decompAPIStatusDiv.className = apiName ? params.decompressorAPI.errorStatus ? 'apiBroken' : 'apiAvailable' : 'apiUnavailable'; + apiName = params.decompressorAPI.errorStatus || apiName || 'Not initialized'; + decompAPIStatusDiv.innerHTML = 'Decompressor API: ' + apiName; + + // Add a warning colour to the API Status Panel if any of the above tests failed + apiStatusPanel.classList.add(apiPanelClass); } var keepAliveServiceWorkerHandle; @@ -2230,6 +2251,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett } } if (fileHandle) { + // Deal with split archives if (/\.zim\w\w$/i.test(fileHandle.name)) { var genericFileName = fileHandle.name.replace(/(\.zim)\w\w$/i, '$1'); var testFileName = new RegExp(genericFileName + '\\w\\w$'); @@ -2242,11 +2264,13 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett } } } else { + // Deal with single unslpit archive fileset.push(fileHandle.getFile().then(function(file) { return file; })); } if (fileset.length) { + // Wait for all getFile Promises to resolve Promise.all(fileset).then(function (resolvedFiles) { setLocalArchiveFromFileList(resolvedFiles); }); diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index 104a8b95..a75963ae 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -728,7 +728,20 @@ define(rqDef, function() { appstate.sessionScale = appstate.windowScale; } - // If global variable webpMachine was defined, then we need to initialize the WebP Polyfill + + // Reports an error in loading one of the ASM or WASM machines to the UI API Status Panel + // This can't be done in aoo.js because the error occurs after the API panel is first displayed + function reportAssemblerErrorToAPIStatusPanel(decoderType, error) { + // Report error to API panel because error is produced asynchronously after panel is first displayed + console.error('Could not instantiate any ' + decoderType + ' decoder!', error); + params.decompressorAPI.errorStatus = 'Error loading ' + decoderType + ' decompressor!'; + var decompAPI = document.getElementById('decompressorAPIStatus'); + decompAPI.innerHTML = 'Decompressor API: ' + params.decompressorAPI.errorStatus; + decompAPI.className = 'apiBroken'; + document.getElementById('apiStatusDiv').className = 'panel panel-danger'; + } + + // If global variable webpMachine is true (set in init.js), then we need to initialize the WebP Polyfill if (webpMachine) webpMachine = new webpHero.WebpMachine(); /** @@ -754,6 +767,7 @@ define(rqDef, function() { systemAlert: systemAlert, checkServerIsAccessible: checkServerIsAccessible, htmlEscapeChars: htmlEscapeChars, - initTouchZoom: initTouchZoom + initTouchZoom: initTouchZoom, + reportAssemblerErrorToAPIStatusPanel: reportAssemblerErrorToAPIStatusPanel }; }); diff --git a/www/js/lib/xzdec_wrapper.js b/www/js/lib/xzdec_wrapper.js index 3b258649..658d9c95 100644 --- a/www/js/lib/xzdec_wrapper.js +++ b/www/js/lib/xzdec_wrapper.js @@ -24,18 +24,20 @@ // DEV: Put your RequireJS definition in the rqDefXZ array below, and any function exports in the function parenthesis of the define statement // We need to do it this way in order to load the wasm or asm versions of xzdec conditionally. Older browsers can only use the asm version // because they cannot interpret WebAssembly. -var rqDefXZ = []; +var rqDefXZ = ['uiUtil']; // Select asm or wasm conditionally if ('WebAssembly' in self) { - console.debug('Using WASM xz decoder'); + console.debug('Instantiating WASM xz decoder'); + params.decompressorAPI.assemblerMachineType = 'WASM'; rqDefXZ.push('xzdec-wasm'); } else { - console.debug('Using ASM xz decoder'); + console.debug('Instantiating ASM xz decoder'); + params.decompressorAPI.assemblerMachineType = 'ASM'; rqDefXZ.push('xzdec-asm'); } -define(rqDefXZ, function() { +define(rqDefXZ, function(uiUtil) { // DEV: xzdec.js has been compiled with `-s EXPORT_NAME="XZ" -s MODULARIZE=1` to avoid a clash with zstddec.js // Note that we include xzdec-asm or xzdec-wasm above in requireJS definition, but we cannot change the name in the function list // There is no longer any need to load it in index.html @@ -49,27 +51,32 @@ define(rqDefXZ, function() { * The XZ Decoder instance * @type EMSInstance */ - var xzdec; + var xzdec; - var instantiateDecoder = function (instance) { - xzdec = instance; - }; - - XZ().then(instantiateDecoder) - .catch(function (err) { - console.debug(err); - if (/CompileError.+?WASM/i.test(err.message)) { - console.log("WASM failed to load, falling back to ASM...", err); - XZ = null; - require(['xzdec-asm'], function() { - XZ().then(instantiateDecoder) - .catch(function (err) { - console.error('Could not instantiate any decoder!', err); - }); - }); - } - }); - + XZ().then(function (instance) { + // TEST ERROR CODE: UNCOMMENT TO TEST AND REMOVE BEFORE MERGE + // throw params.decompressorAPI.assemblerMachineType + ' broken!'; + xzdec = instance; + }).catch(function (err) { + if (params.decompressorAPI.assemblerMachineType === 'ASM') { + // There is no fallback, because we were attempting to load the ASM machine, so report error immediately + uiUtil.reportAssemblerErrorToAPIStatusPanel('XZ', err); + } else { + console.warn('WASM failed to load, falling back to ASM...', err); + params.decompressorAPI.assemblerMachineType = 'ASM'; + XZ = null; + require(['xzdec-asm'], function () { + XZ().then(function (instance) { + // TEST ERROR CODE: UNCOMMENT TO TEST AND REMOVE BEFORE MERGE + // throw params.decompressorAPI.assemblerMachineType + ' broken!'; + xzdec = instance; + }).catch(function (err) { + uiUtil.reportAssemblerErrorToAPIStatusPanel('XZ', err); + }); + }); + } + }); + /** * Number of milliseconds to wait for the decompressor to be available for another chunk * @type Integer @@ -99,9 +106,11 @@ define(rqDefXZ, function() { * @returns {Decompressor} */ function Decompressor(reader, chunkSize) { + params.decompressorAPI.decompressorLastUsed = 'XZ'; this._chunkSize = chunkSize || 1024 * 5; this._reader = reader; }; + /** * Read length bytes, offset into the decompressed stream. Consecutive calls may only * advance in the stream and may not overlap. @@ -122,7 +131,7 @@ define(rqDefXZ, function() { return data; }); }; - + /** * Reads stream of data from file offset for length of bytes to send to the decompresor * This function ensures that only one decompression runs at a time @@ -130,7 +139,7 @@ define(rqDefXZ, function() { * @param {Integer} length The amount of data to read * @returns {Promise} A Promise for the read data */ - Decompressor.prototype.readSliceSingleThread = function(offset, length) { + Decompressor.prototype.readSliceSingleThread = function (offset, length) { // Tests whether the decompressor is ready (initiated) and not busy if (xzdec && !busy) { return this.readSlice(offset, length); @@ -140,9 +149,9 @@ define(rqDefXZ, function() { // before using it for another decompression var that = this; return new Promise(function (resolve, reject) { - setTimeout(function(){ + setTimeout(function () { that.readSliceSingleThread(offset, length).then(resolve, reject); - }, DELAY_WAITING_IDLE_DECOMPRESSOR); + }, DELAY_WAITING_IDLE_DECOMPRESSOR); }); } }; diff --git a/www/js/lib/zimfile.js b/www/js/lib/zimfile.js index a38d3422..a75bfa33 100644 --- a/www/js/lib/zimfile.js +++ b/www/js/lib/zimfile.js @@ -25,6 +25,7 @@ * Add Polyfill currently required by IE11 to run zstddec-asm and xzdec-asm * See https://github.com/emscripten-core/emscripten/issues/14700 * If this is resolved upstream, remove this polyfill + * Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith */ if (!String.prototype.startsWith) { Object.defineProperty(String.prototype, 'startsWith', { @@ -35,6 +36,17 @@ }); } +/** + * A global variable to track the assembler machine type and the last used decompressor (for reporting to the API panel) + * This is populated in the Emscripten wrappers + * @type {Object} + */ +params.decompressorAPI = { + assemblerMachineType: null, + decompressorLastUsed: null, + errorStatus: null +}; + define(['xzdec_wrapper', 'zstddec_wrapper', 'util', 'utf8', 'zimDirEntry', 'filecache'], function(xz, zstd, util, utf8, zimDirEntry, FileCache) { /** diff --git a/www/js/lib/zstddec_wrapper.js b/www/js/lib/zstddec_wrapper.js index 794710d7..8b257cbe 100644 --- a/www/js/lib/zstddec_wrapper.js +++ b/www/js/lib/zstddec_wrapper.js @@ -24,22 +24,24 @@ // DEV: Put your RequireJS definition in the rqDefZD array below, and any function exports in the function parenthesis of the define statement // We need to do it this way in order to load the wasm or asm versions of zstddec conditionally. Older browsers can only use the asm version // because they cannot interpret WebAssembly. -var rqDefZD = []; +var rqDefZD = ['uiUtil']; // Select asm or wasm conditionally if ('WebAssembly' in self) { - console.debug('Using WASM zstandard decoder'); + console.debug('Instantiating WASM zstandard decoder'); + params.decompressorAPI.assemblerMachineType = 'WASM'; rqDefZD.push('zstddec-wasm'); } else { - console.debug('Using ASM zstandard decoder'); + console.debug('Instantiating ASM zstandard decoder'); + params.decompressorAPI.assemblerMachineType = 'ASM'; rqDefZD.push('zstddec-asm'); } -define(rqDefZD, function() { +define(rqDefZD, function(uiUtil) { // DEV: zstddec.js has been compiled with `-s EXPORT_NAME="ZD" -s MODULARIZE=1` to avoid a clash with xzdec.js // Note that we include zstddec-wasm or zstddec-asm above in requireJS definition, but we cannot change the name in the function list // For explanation of loading method below to avoid conflicts, see https://github.com/emscripten-core/emscripten/blob/master/src/settings.js - + /** * @typedef EMSInstanceExt An object type representing an Emscripten instance with extended properties * @property {Integer} _decHandle The decoder stream context object in asm memory (to be re-used for each decoder operation) @@ -47,7 +49,7 @@ define(rqDefZD, function() { * @property {Object} _outBuffer A JS copy of the outBuffer structure to be set in asm memory (malloc) * @property {Integer} _chunkSize The number of compressed bytes to feed to the decompressor in any one read loop */ - + /** * The ZSTD Decoder instance * @type EMSInstanceExt @@ -68,7 +70,7 @@ define(rqDefZD, function() { // Change _chunkSize if you need a more conservative memory environment, but you may need to experiment with INITIAL_MEMORY // in zstddec.js (see below) for this to make any difference // zd._chunkSize = 5 * 1024; - + // Initialize inBuffer zd._inBuffer = { ptr: null, /* pointer to this inBuffer structure in w/asm memory */ @@ -80,7 +82,7 @@ define(rqDefZD, function() { zd._inBuffer.ptr = mallocOrDie(3 << 2); // 3 x 32bit bytes // Reserve w/asm memory for the inBuffer data stream zd._inBuffer.src = mallocOrDie(zd._inBuffer.size); - + // DEV: Size of outBuffer is currently set as recommended by zd._ZSTD_DStreamOutSize() below; if you are running into // memory issues, it may be possible to reduce memory consumption by setting a smaller outBuffer size here and // reompiling zstddec.js with lower TOTAL_MEMORY (or just search for INITIAL_MEMORY in zstddec.js and change it) @@ -99,33 +101,42 @@ define(rqDefZD, function() { zd._outBuffer.dst = mallocOrDie(zd._outBuffer.size); }; - ZD().then(instantiateDecoder) - .catch(function (err) { - console.debug(err); - if (/CompileError.+?WASM/i.test(err.message)) { - console.log("WASM failed to load, falling back to ASM...", err); + ZD().then(function (inst) { + // TEST ERROR CODE: UNCOMMENT TO TEST AND REMOVE BEFORE MERGE + // throw params.decompressorAPI.assemblerMachineType + ' broken!'; + instantiateDecoder(inst); + }).catch(function (err) { + if (params.decompressorAPI.assemblerMachineType === 'ASM') { + // There is no fallback, because we were attempting to load the ASM machine, so report error immediately + uiUtil.reportAssemblerErrorToAPIStatusPanel('ZSTD', err); + } else { + console.warn('WASM failed to load, falling back to ASM...', err); + params.decompressorAPI.assemblerMachineType = 'ASM'; ZD = null; - require(['zstddec-asm'], function() { - ZD().then(instantiateDecoder) - .catch(function (err) { - console.error('Could not instantiate any decoder!', err); + require(['zstddec-asm'], function () { + ZD().then(function (inst) { + // TEST ERROR CODE: UNCOMMENT TO TEST AND REMOVE BEFORE MERGE + // throw params.decompressorAPI.assemblerMachineType + ' broken!'; + instantiateDecoder(inst); + }).catch(function (err) { + uiUtil.reportAssemblerErrorToAPIStatusPanel('ZSTD', err); }); }); } }); - + /** * Number of milliseconds to wait for the decompressor to be available for another chunk * @type Integer */ var DELAY_WAITING_IDLE_DECOMPRESSOR = 50; - + /** * Is the decompressor already working? * @type Boolean */ var busy = false; - + /** * @typedef Decompressor * @property {FileReader} _reader The filereader to use (uses plain blob reader defined in zimfile.js) @@ -135,12 +146,13 @@ define(rqDefZD, function() { * @property {Array} _outDataBuf The buffer that stores decoded bytes (it is set to the requested blob's length, and when full, the data are returned) * @property {Integer} _outDataBufPos The number of bytes of the requested blob decoded so far */ - + /** * @constructor * @param {FileReader} reader The reader used to extract file slices (defined in zimfile.js) */ function Decompressor(reader) { + params.decompressorAPI.decompressorLastUsed = 'ZSTD'; this._reader = reader; } @@ -151,7 +163,7 @@ define(rqDefZD, function() { * @param {Integer} length Number of decompressed bytes to read * @returns {Promise} Promise for an ArrayBuffer with decoded data */ - Decompressor.prototype.readSlice = function(offset, length) { + Decompressor.prototype.readSlice = function (offset, length) { busy = true; this._inStreamPos = 0; this._inStreamChunkedPos = 0; @@ -163,7 +175,7 @@ define(rqDefZD, function() { return Promise.reject('Failed to initialize ZSTD decompression'); } - return this._readLoop(offset, length).then(function(data) { + return this._readLoop(offset, length).then(function (data) { // DEV: We are re-using all the allocated w/asm memory, so we do not need to free any of structures assigned wiht _malloc // However, should you need to free assigned structures use, e.g., zd._free(zd._inBuffer.src); // Additionally, freeing zd._decHandle is not needed, and actually increases memory consumption (crashing zstddeclib) @@ -217,7 +229,7 @@ define(rqDefZD, function() { // Get updated outbuffer values var obxPtr32Bit = zd._outBuffer.ptr >> 2; var outPos = zd.HEAP32[obxPtr32Bit + 2]; - + // If data have been decompressed, check to see whether the data are in the offset range we need if (outPos > 0 && that._outStreamPos + outPos >= offset) { var copyStart = offset - that._outStreamPos; @@ -247,14 +259,14 @@ define(rqDefZD, function() { console.error(err); }); }; - + /** * Fills in the instream buffer * @returns {Promise<0>} A Promise for 0 when all data have been added to the stream */ - Decompressor.prototype._fillInBuffer = function() { + Decompressor.prototype._fillInBuffer = function () { var that = this; - return this._reader(this._inStreamPos, zd._chunkSize).then(function(data) { + return this._reader(this._inStreamPos, zd._chunkSize).then(function (data) { // Populate inBuffer and assign asm/wasm memory if not already assigned zd._inBuffer.size = data.length; // Reset inBuffer @@ -265,7 +277,7 @@ define(rqDefZD, function() { var outBufferStruct = new Int32Array([zd._outBuffer.dst, zd._outBuffer.size, zd._outBuffer.pos]); // Write outBuffer structure to w/asm memory zd.HEAP32.set(outBufferStruct, zd._outBuffer.ptr >> 2); - + // Transfer the (new) data to be read to the inBuffer zd.HEAPU8.set(data, zd._inBuffer.src); that._inStreamChunkedPos += data.length;