diff --git a/.eslintrc.json b/.eslintrc.json index 4b5a62ac3..29a86ef1a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -79,6 +79,10 @@ { "message": "Avoid using Response.json(), prefer readResponseJson.", "selector": "MemberExpression[property.name=json]" + }, + { + "message": "Avoid using performance, prefer safePerformance.", + "selector": "MemberExpression[object.name=performance]" } ], "no-self-compare": "error", @@ -585,6 +589,7 @@ "ext/js/core/extension-error.js", "ext/js/core/json.js", "ext/js/core/log.js", + "ext/js/core/safe-performance.js", "ext/js/core/to-error.js", "ext/js/core/utilities.js", "ext/js/data/database.js", @@ -622,6 +627,7 @@ "ext/js/core/log-utilities.js", "ext/js/core/log.js", "ext/js/core/object-utilities.js", + "ext/js/core/safe-performance.js", "ext/js/core/to-error.js", "ext/js/core/utilities.js", "ext/js/data/anki-util.js", diff --git a/dev/bin/schema-validate.js b/dev/bin/schema-validate.js index 4baeb8aba..dc40e55f4 100644 --- a/dev/bin/schema-validate.js +++ b/dev/bin/schema-validate.js @@ -43,15 +43,18 @@ function main() { const schema = parseJson(schemaSource); for (const dataFileName of args.slice(1)) { + // eslint-disable-next-line no-restricted-syntax const start = performance.now(); try { console.log(`Validating ${dataFileName}...`); const dataSource = fs.readFileSync(dataFileName, {encoding: 'utf8'}); const data = parseJson(dataSource); createJsonSchema(mode, schema).validate(data); + // eslint-disable-next-line no-restricted-syntax const end = performance.now(); console.log(`No issues detected (${((end - start) / 1000).toFixed(2)}s)`); } catch (e) { + // eslint-disable-next-line no-restricted-syntax const end = performance.now(); console.log(`Encountered an error (${((end - start) / 1000).toFixed(2)}s)`); console.warn(e); diff --git a/dev/dictionary-validate.js b/dev/dictionary-validate.js index 18532ad69..77fa0fca3 100644 --- a/dev/dictionary-validate.js +++ b/dev/dictionary-validate.js @@ -131,14 +131,17 @@ export async function testDictionaryFiles(mode, dictionaryFileNames) { const schemas = getSchemas(); for (const dictionaryFileName of dictionaryFileNames) { + // eslint-disable-next-line no-restricted-syntax const start = performance.now(); try { console.log(`Validating ${dictionaryFileName}...`); const source = fs.readFileSync(dictionaryFileName); await validateDictionary(mode, source.buffer, schemas); + // eslint-disable-next-line no-restricted-syntax const end = performance.now(); console.log(`No issues detected (${((end - start) / 1000).toFixed(2)}s)`); } catch (e) { + // eslint-disable-next-line no-restricted-syntax const end = performance.now(); console.log(`Encountered an error (${((end - start) / 1000).toFixed(2)}s)`); console.warn(e); diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index d532cc9b6..08d62a081 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -20,6 +20,7 @@ import {createApiMap, invokeApiMapHandler} from '../core/api-map.js'; import {EventListenerCollection} from '../core/event-listener-collection.js'; import {log} from '../core/log.js'; import {promiseAnimationFrame} from '../core/promise-animation-frame.js'; +import {safePerformance} from '../core/safe-performance.js'; import {setProfile} from '../data/profiles-util.js'; import {addFullscreenChangeEventListener, getFullscreenElement} from '../dom/document-util.js'; import {TextSourceElement} from '../dom/text-source-element.js'; @@ -952,13 +953,13 @@ export class Frontend { * @returns {Promise} */ async _scanSelectedText(allowEmptyRange, disallowExpandSelection, showEmpty = false) { - performance.mark('frontend:scanSelectedText:start'); + safePerformance.mark('frontend:scanSelectedText:start'); const range = this._getFirstSelectionRange(allowEmptyRange); if (range === null) { return false; } const source = disallowExpandSelection ? TextSourceRange.createLazy(range) : TextSourceRange.create(range); await this._textScanner.search(source, {focus: true, restoreSelection: true}, showEmpty); - performance.mark('frontend:scanSelectedText:end'); - performance.measure('frontend:scanSelectedText', 'frontend:scanSelectedText:start', 'frontend:scanSelectedText:end'); + safePerformance.mark('frontend:scanSelectedText:end'); + safePerformance.measure('frontend:scanSelectedText', 'frontend:scanSelectedText:start', 'frontend:scanSelectedText:end'); return true; } diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js index cc7abb9e5..c8ecdf687 100644 --- a/ext/js/app/popup.js +++ b/ext/js/app/popup.js @@ -21,6 +21,7 @@ import {DynamicProperty} from '../core/dynamic-property.js'; import {EventDispatcher} from '../core/event-dispatcher.js'; import {EventListenerCollection} from '../core/event-listener-collection.js'; import {ExtensionError} from '../core/extension-error.js'; +import {safePerformance} from '../core/safe-performance.js'; import {deepEqual} from '../core/utilities.js'; import {addFullscreenChangeEventListener, computeZoomScale, convertRectZoomCoordinates, getFullscreenElement} from '../dom/document-util.js'; import {loadStyle} from '../dom/style-util.js'; @@ -302,7 +303,7 @@ export class Popup extends EventDispatcher { await this._show(sourceRects, writingMode); if (displayDetails !== null) { - performance.mark('invokeDisplaySetContent:start'); + safePerformance.mark('invokeDisplaySetContent:start'); void this._invokeSafe('displaySetContent', {details: displayDetails}); } } diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index c242cfb4a..647b22e99 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -1372,7 +1372,20 @@ export class Backend { this._clipboardMonitor.stop(); } + this._setupContextMenu(options); + + void this._accessibilityController.update(this._getOptionsFull(false)); + + this._sendMessageAllTabsIgnoreResponse({action: 'applicationOptionsUpdated', params: {source}}); + } + + /** + * @param {import('settings').ProfileOptions} options + */ + _setupContextMenu(options) { try { + if (!chrome.contextMenus) { return; } + if (options.general.enableContextMenuScanSelected) { chrome.contextMenus.create({ id: 'yomitan_lookup', @@ -1390,10 +1403,6 @@ export class Backend { } catch (e) { log.error(e); } - - void this._accessibilityController.update(this._getOptionsFull(false)); - - this._sendMessageAllTabsIgnoreResponse({action: 'applicationOptionsUpdated', params: {source}}); } /** diff --git a/ext/js/comm/cross-frame-api.js b/ext/js/comm/cross-frame-api.js index a55bbfdd4..1b7d3f69e 100644 --- a/ext/js/comm/cross-frame-api.js +++ b/ext/js/comm/cross-frame-api.js @@ -22,6 +22,7 @@ import {EventListenerCollection} from '../core/event-listener-collection.js'; import {ExtensionError} from '../core/extension-error.js'; import {parseJson} from '../core/json.js'; import {log} from '../core/log.js'; +import {safePerformance} from '../core/safe-performance.js'; /** * @augments EventDispatcher @@ -106,7 +107,7 @@ export class CrossFrameAPIPort extends EventDispatcher { return; } } - performance.mark(`cross-frame-api:invoke:${action}`); + safePerformance.mark(`cross-frame-api:invoke:${action}`); try { this._port.postMessage(/** @type {import('cross-frame-api').InvokeMessage} */ ({type: 'invoke', id, data: {action, params}})); } catch (e) { diff --git a/ext/js/core/promise-animation-frame.js b/ext/js/core/promise-animation-frame.js index 0bcd6970d..23f643ace 100644 --- a/ext/js/core/promise-animation-frame.js +++ b/ext/js/core/promise-animation-frame.js @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import {safePerformance} from './safe-performance.js'; + /** * Creates a promise that will resolve after the next animation frame, using `requestAnimationFrame`. * @param {number} [timeout] A maximum duration (in milliseconds) to wait until the promise resolves. If null or omitted, no timeout is used. @@ -51,7 +53,7 @@ export function promiseAnimationFrame(timeout) { cancelAnimationFrame(frameRequest); frameRequest = null; } - resolve({time: performance.now(), timeout: true}); + resolve({time: safePerformance.now(), timeout: true}); }; frameRequest = requestAnimationFrame(onFrame); diff --git a/ext/js/core/safe-performance.js b/ext/js/core/safe-performance.js new file mode 100644 index 000000000..0bf5534ac --- /dev/null +++ b/ext/js/core/safe-performance.js @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import {log} from './log.js'; + +/** + * This class safely handles performance methods. + */ +class SafePerformance { + constructor() {} + + /** + * @param {string} markName + * @param {PerformanceMarkOptions} [markOptions] + * @returns {PerformanceMark | undefined} + */ + mark(markName, markOptions) { + try { + // eslint-disable-next-line no-restricted-syntax + return performance.mark(markName, markOptions); + } catch (e) { + log.error(e); + } + } + + /** + * + * @param {string} measureName + * @param {string | PerformanceMeasureOptions} [startOrMeasureOptions] + * @param {string} [endMark] + * @returns {PerformanceMeasure | undefined} + */ + measure(measureName, startOrMeasureOptions, endMark) { + try { + // eslint-disable-next-line no-restricted-syntax + return performance.measure(measureName, startOrMeasureOptions, endMark); + } catch (e) { + log.error(e); + } + } + + /** + * @returns {DOMHighResTimeStamp} + */ + now() { + // eslint-disable-next-line no-restricted-syntax + return performance.now(); + } +} + +/** + * This object is the default performance measurer used by the runtime. + */ +export const safePerformance = new SafePerformance(); diff --git a/ext/js/dictionary/dictionary-database.js b/ext/js/dictionary/dictionary-database.js index 22aa98445..98347de52 100644 --- a/ext/js/dictionary/dictionary-database.js +++ b/ext/js/dictionary/dictionary-database.js @@ -19,6 +19,7 @@ import {initWasm, Resvg} from '../../lib/resvg-wasm.js'; import {createApiMap, invokeApiMapHandler} from '../core/api-map.js'; import {log} from '../core/log.js'; +import {safePerformance} from '../core/safe-performance.js'; import {stringReverse} from '../core/utilities.js'; import {Database} from '../data/database.js'; @@ -401,7 +402,7 @@ export class DictionaryDatabase { return; } // otherwise, you are the worker, so do the work - performance.mark('drawMedia:start'); + safePerformance.mark('drawMedia:start'); // merge items with the same path to reduce the number of database queries. collects the canvases into a single array for each path. /** @type {Map} */ @@ -423,10 +424,10 @@ export class DictionaryDatabase { // move all svgs to front to have a hotter loop results.sort((a, _b) => (a.mediaType === 'image/svg+xml' ? -1 : 1)); - performance.mark('drawMedia:draw:start'); + safePerformance.mark('drawMedia:draw:start'); for (const m of results) { if (m.mediaType === 'image/svg+xml') { - performance.mark('drawMedia:draw:svg:start'); + safePerformance.mark('drawMedia:draw:svg:start'); /** @type {import('@resvg/resvg-wasm').ResvgRenderOptions} */ const opts = { fitTo: { @@ -440,10 +441,10 @@ export class DictionaryDatabase { const resvgJS = new Resvg(new Uint8Array(m.content), opts); const render = resvgJS.render(); source.postMessage({action: 'drawBufferToCanvases', params: {buffer: render.pixels.buffer, width: render.width, height: render.height, canvasIndexes: m.canvasIndexes, generation: m.generation}}, [render.pixels.buffer]); - performance.mark('drawMedia:draw:svg:end'); - performance.measure('drawMedia:draw:svg', 'drawMedia:draw:svg:start', 'drawMedia:draw:svg:end'); + safePerformance.mark('drawMedia:draw:svg:end'); + safePerformance.measure('drawMedia:draw:svg', 'drawMedia:draw:svg:start', 'drawMedia:draw:svg:end'); } else { - performance.mark('drawMedia:draw:raster:start'); + safePerformance.mark('drawMedia:draw:raster:start'); // ImageDecoder is slightly faster than Blob/createImageBitmap, but // 1) it is not available in Firefox <133 @@ -472,15 +473,15 @@ export class DictionaryDatabase { } }); } - performance.mark('drawMedia:draw:raster:end'); - performance.measure('drawMedia:draw:raster', 'drawMedia:draw:raster:start', 'drawMedia:draw:raster:end'); + safePerformance.mark('drawMedia:draw:raster:end'); + safePerformance.measure('drawMedia:draw:raster', 'drawMedia:draw:raster:start', 'drawMedia:draw:raster:end'); } } - performance.mark('drawMedia:draw:end'); - performance.measure('drawMedia:draw', 'drawMedia:draw:start', 'drawMedia:draw:end'); + safePerformance.mark('drawMedia:draw:end'); + safePerformance.measure('drawMedia:draw', 'drawMedia:draw:start', 'drawMedia:draw:end'); - performance.mark('drawMedia:end'); - performance.measure('drawMedia', 'drawMedia:start', 'drawMedia:end'); + safePerformance.mark('drawMedia:end'); + safePerformance.measure('drawMedia', 'drawMedia:start', 'drawMedia:end'); } /** @@ -592,7 +593,7 @@ export class DictionaryDatabase { * @returns {Promise} */ _findMultiBulk(objectStoreName, indexNames, items, createQuery, predicate, createResult) { - performance.mark('findMultiBulk:start'); + safePerformance.mark('findMultiBulk:start'); return new Promise((resolve, reject) => { const itemCount = items.length; const indexCount = indexNames.length; @@ -600,8 +601,8 @@ export class DictionaryDatabase { const results = []; if (itemCount === 0 || indexCount === 0) { resolve(results); - performance.mark('findMultiBulk:end'); - performance.measure('findMultiBulk', 'findMultiBulk:start', 'findMultiBulk:end'); + safePerformance.mark('findMultiBulk:end'); + safePerformance.measure('findMultiBulk', 'findMultiBulk:start', 'findMultiBulk:end'); return; } @@ -619,8 +620,8 @@ export class DictionaryDatabase { */ const onGetAll = (item) => (rows, data) => { if (typeof item === 'object' && item !== null && 'path' in item) { - performance.mark(`findMultiBulk:onGetAll:${item.path}:end`); - performance.measure(`findMultiBulk:onGetAll:${item.path}`, `findMultiBulk:onGetAll:${item.path}:start`, `findMultiBulk:onGetAll:${item.path}:end`); + safePerformance.mark(`findMultiBulk:onGetAll:${item.path}:end`); + safePerformance.measure(`findMultiBulk:onGetAll:${item.path}`, `findMultiBulk:onGetAll:${item.path}:start`, `findMultiBulk:onGetAll:${item.path}:end`); } for (const row of rows) { if (predicate(row, data.item)) { @@ -629,11 +630,11 @@ export class DictionaryDatabase { } if (++completeCount >= requiredCompleteCount) { resolve(results); - performance.mark('findMultiBulk:end'); - performance.measure('findMultiBulk', 'findMultiBulk:start', 'findMultiBulk:end'); + safePerformance.mark('findMultiBulk:end'); + safePerformance.measure('findMultiBulk', 'findMultiBulk:start', 'findMultiBulk:end'); } }; - performance.mark('findMultiBulk:getAll:start'); + safePerformance.mark('findMultiBulk:getAll:start'); for (let i = 0; i < itemCount; ++i) { const item = items[i]; const query = createQuery(item); @@ -641,13 +642,13 @@ export class DictionaryDatabase { /** @type {import('dictionary-database').FindMultiBulkData} */ const data = {item, itemIndex: i, indexIndex: j}; if (typeof item === 'object' && item !== null && 'path' in item) { - performance.mark(`findMultiBulk:onGetAll:${item.path}:start`); + safePerformance.mark(`findMultiBulk:onGetAll:${item.path}:start`); } this._db.getAll(indexList[j], query, onGetAll(item), reject, data); } } - performance.mark('findMultiBulk:getAll:end'); - performance.measure('findMultiBulk:getAll', 'findMultiBulk:getAll:start', 'findMultiBulk:getAll:end'); + safePerformance.mark('findMultiBulk:getAll:end'); + safePerformance.measure('findMultiBulk:getAll', 'findMultiBulk:getAll:start', 'findMultiBulk:getAll:end'); }); } diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js index 4e5d2c50e..af56af02c 100644 --- a/ext/js/display/display-generator.js +++ b/ext/js/display/display-generator.js @@ -17,6 +17,7 @@ */ import {ExtensionError} from '../core/extension-error.js'; +import {safePerformance} from '../core/safe-performance.js'; import {getDisambiguations, getGroupedPronunciations, getTermFrequency, groupKanjiFrequencies, groupTermFrequencies, groupTermTags, isNonNounVerbOrAdjective} from '../dictionary/dictionary-data-util.js'; import {HtmlTemplateCollection} from '../dom/html-template-collection.js'; import {distributeFurigana, getKanaMorae, getPitchCategory, isCodePointKanji} from '../language/ja/japanese.js'; @@ -118,23 +119,23 @@ export class DisplayGenerator { node.dataset.groupedFrequencyCount = `${groupedFrequencies.length}`; node.dataset.primaryMatchTypes = [...primaryMatchTypes].join(' '); - performance.mark('displayGenerator:createTermEntry:createTermHeadword:start'); + safePerformance.mark('displayGenerator:createTermEntry:createTermHeadword:start'); for (let i = 0, ii = headwords.length; i < ii; ++i) { const node2 = this._createTermHeadword(headwords[i], i, pronunciations); node2.dataset.index = `${i}`; headwordsContainer.appendChild(node2); } headwordsContainer.dataset.count = `${headwords.length}`; - performance.mark('displayGenerator:createTermEntry:createTermHeadword:end'); - performance.measure('displayGenerator:createTermEntry:createTermHeadword', 'displayGenerator:createTermEntry:createTermHeadword:start', 'displayGenerator:createTermEntry:createTermHeadword:end'); + safePerformance.mark('displayGenerator:createTermEntry:createTermHeadword:end'); + safePerformance.measure('displayGenerator:createTermEntry:createTermHeadword', 'displayGenerator:createTermEntry:createTermHeadword:start', 'displayGenerator:createTermEntry:createTermHeadword:end'); - performance.mark('displayGenerator:createTermEntry:promises:start'); + safePerformance.mark('displayGenerator:createTermEntry:promises:start'); this._appendMultiple(inflectionRuleChainsContainer, this._createInflectionRuleChain.bind(this), inflectionRuleChainCandidates); this._appendMultiple(frequencyGroupListContainer, this._createFrequencyGroup.bind(this), groupedFrequencies, false); this._appendMultiple(groupedPronunciationsContainer, this._createGroupedPronunciation.bind(this), groupedPronunciations); this._appendMultiple(headwordTagsContainer, this._createTermTag.bind(this), termTags, headwords.length); - performance.mark('displayGenerator:createTermEntry:promises:end'); - performance.measure('displayGenerator:createTermEntry:promises', 'displayGenerator:createTermEntry:promises:start', 'displayGenerator:createTermEntry:promises:end'); + safePerformance.mark('displayGenerator:createTermEntry:promises:end'); + safePerformance.measure('displayGenerator:createTermEntry:promises', 'displayGenerator:createTermEntry:promises:start', 'displayGenerator:createTermEntry:promises:end'); for (const term of uniqueTerms) { headwordTagsContainer.appendChild(this._createSearchTag(term)); diff --git a/ext/js/display/display.js b/ext/js/display/display.js index fe744406c..0b4bf6b0f 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -24,6 +24,7 @@ import {EventDispatcher} from '../core/event-dispatcher.js'; import {EventListenerCollection} from '../core/event-listener-collection.js'; import {ExtensionError} from '../core/extension-error.js'; import {log} from '../core/log.js'; +import {safePerformance} from '../core/safe-performance.js'; import {toError} from '../core/to-error.js'; import {clone, deepEqual, promiseTimeout} from '../core/utilities.js'; import {setProfile} from '../data/profiles-util.js'; @@ -733,7 +734,7 @@ export class Display extends EventDispatcher { /** @type {import('display').DirectApiHandler<'displaySetContent'>} */ _onMessageSetContent({details}) { - performance.mark('invokeDisplaySetContent:end'); + safePerformance.mark('invokeDisplaySetContent:end'); this.setContent(details); } @@ -787,14 +788,14 @@ export class Display extends EventDispatcher { async _onStateChanged() { if (this._historyChangeIgnore) { return; } - performance.mark('display:_onStateChanged:start'); + safePerformance.mark('display:_onStateChanged:start'); /** @type {?import('core').TokenObject} */ const token = {}; // Unique identifier token this._setContentToken = token; try { // Clear - performance.mark('display:_onStateChanged:clear:start'); + safePerformance.mark('display:_onStateChanged:clear:start'); this._closePopups(); this._closeAllPopupMenus(); this._eventListeners.removeAllEventListeners(); @@ -804,11 +805,11 @@ export class Display extends EventDispatcher { this._dictionaryEntries = []; this._dictionaryEntryNodes = []; this._elementOverflowController.clearElements(); - performance.mark('display:_onStateChanged:clear:end'); - performance.measure('display:_onStateChanged:clear', 'display:_onStateChanged:clear:start', 'display:_onStateChanged:clear:end'); + safePerformance.mark('display:_onStateChanged:clear:end'); + safePerformance.measure('display:_onStateChanged:clear', 'display:_onStateChanged:clear:start', 'display:_onStateChanged:clear:end'); // Prepare - performance.mark('display:_onStateChanged:prepare:start'); + safePerformance.mark('display:_onStateChanged:prepare:start'); const urlSearchParams = new URLSearchParams(location.search); let type = urlSearchParams.get('type'); if (type === null && urlSearchParams.get('query') !== null) { type = 'terms'; } @@ -817,10 +818,10 @@ export class Display extends EventDispatcher { this._queryParserVisibleOverride = (fullVisible === null ? null : (fullVisible !== 'false')); this._historyHasChanged = true; - performance.mark('display:_onStateChanged:prepare:end'); - performance.measure('display:_onStateChanged:prepare', 'display:_onStateChanged:prepare:start', 'display:_onStateChanged:prepare:end'); + safePerformance.mark('display:_onStateChanged:prepare:end'); + safePerformance.measure('display:_onStateChanged:prepare', 'display:_onStateChanged:prepare:start', 'display:_onStateChanged:prepare:end'); - performance.mark('display:_onStateChanged:setContent:start'); + safePerformance.mark('display:_onStateChanged:setContent:start'); // Set content switch (type) { case 'terms': @@ -837,13 +838,13 @@ export class Display extends EventDispatcher { this._clearContent(); break; } - performance.mark('display:_onStateChanged:setContent:end'); - performance.measure('display:_onStateChanged:setContent', 'display:_onStateChanged:setContent:start', 'display:_onStateChanged:setContent:end'); + safePerformance.mark('display:_onStateChanged:setContent:end'); + safePerformance.measure('display:_onStateChanged:setContent', 'display:_onStateChanged:setContent:start', 'display:_onStateChanged:setContent:end'); } catch (e) { this.onError(toError(e)); } - performance.mark('display:_onStateChanged:end'); - performance.measure('display:_onStateChanged', 'display:_onStateChanged:start', 'display:_onStateChanged:end'); + safePerformance.mark('display:_onStateChanged:end'); + safePerformance.measure('display:_onStateChanged', 'display:_onStateChanged:start', 'display:_onStateChanged:end'); } /** @@ -1326,7 +1327,7 @@ export class Display extends EventDispatcher { const hasEnabledDictionaries = this._options ? this._options.dictionaries.some(({enabled}) => enabled) : false; // Set query - performance.mark('display:setQuery:start'); + safePerformance.mark('display:setQuery:start'); let query = urlSearchParams.get('query'); if (query === null) { query = ''; } let queryFull = urlSearchParams.get('full'); @@ -1339,8 +1340,8 @@ export class Display extends EventDispatcher { queryOffset = Number.isFinite(queryOffset) ? Math.max(0, Math.min(queryFull.length - query.length, queryOffset)) : 0; } this._setQuery(query, queryFull, queryOffset); - performance.mark('display:setQuery:end'); - performance.measure('display:setQuery', 'display:setQuery:start', 'display:setQuery:end'); + safePerformance.mark('display:setQuery:end'); + safePerformance.measure('display:setQuery', 'display:setQuery:start', 'display:setQuery:end'); let {state, content} = this._history; let changeHistory = false; @@ -1363,10 +1364,10 @@ export class Display extends EventDispatcher { let {dictionaryEntries} = content; if (!Array.isArray(dictionaryEntries)) { - performance.mark('display:findDictionaryEntries:start'); + safePerformance.mark('display:findDictionaryEntries:start'); dictionaryEntries = hasEnabledDictionaries && lookup && query.length > 0 ? await this._findDictionaryEntries(type === 'kanji', query, primaryReading, wildcardsEnabled, optionsContext) : []; - performance.mark('display:findDictionaryEntries:end'); - performance.measure('display:findDictionaryEntries', 'display:findDictionaryEntries:start', 'display:findDictionaryEntries:end'); + safePerformance.mark('display:findDictionaryEntries:end'); + safePerformance.measure('display:findDictionaryEntries', 'display:findDictionaryEntries:start', 'display:findDictionaryEntries:end'); if (this._setContentToken !== token) { return; } content.dictionaryEntries = dictionaryEntries; changeHistory = true; @@ -1401,10 +1402,10 @@ export class Display extends EventDispatcher { this._dictionaryEntries = dictionaryEntries; - performance.mark('display:updateNavigationAuto:start'); + safePerformance.mark('display:updateNavigationAuto:start'); this._updateNavigationAuto(); - performance.mark('display:updateNavigationAuto:end'); - performance.measure('display:updateNavigationAuto', 'display:updateNavigationAuto:start', 'display:updateNavigationAuto:end'); + safePerformance.mark('display:updateNavigationAuto:end'); + safePerformance.measure('display:updateNavigationAuto', 'display:updateNavigationAuto:start', 'display:updateNavigationAuto:end'); this._setNoContentVisible(hasEnabledDictionaries && dictionaryEntries.length === 0 && lookup); this._setNoDictionariesVisible(!hasEnabledDictionaries); @@ -1412,19 +1413,19 @@ export class Display extends EventDispatcher { const container = this._container; container.textContent = ''; - performance.mark('display:contentUpdate:start'); + safePerformance.mark('display:contentUpdate:start'); this._triggerContentUpdateStart(); let i = 0; for (const dictionaryEntry of dictionaryEntries) { - performance.mark('display:createEntry:start'); + safePerformance.mark('display:createEntry:start'); if (i > 0) { await promiseTimeout(1); if (this._setContentToken !== token) { return; } } - performance.mark('display:createEntryReal:start'); + safePerformance.mark('display:createEntryReal:start'); const entry = ( dictionaryEntry.type === 'term' ? @@ -1435,9 +1436,9 @@ export class Display extends EventDispatcher { this._dictionaryEntryNodes.push(entry); this._addEntryEventListeners(entry); this._triggerContentUpdateEntry(dictionaryEntry, entry, i); - performance.mark('display:waitMedia:start'); - performance.mark('display:waitMedia:end'); - performance.measure('display:waitMedia', 'display:waitMedia:start', 'display:waitMedia:end'); + safePerformance.mark('display:waitMedia:start'); + safePerformance.mark('display:waitMedia:end'); + safePerformance.measure('display:waitMedia', 'display:waitMedia:start', 'display:waitMedia:end'); if (this._setContentToken !== token) { return; } container.appendChild(entry); @@ -1447,11 +1448,11 @@ export class Display extends EventDispatcher { this._elementOverflowController.addElements(entry); - performance.mark('display:createEntryReal:end'); - performance.measure('display:createEntryReal', 'display:createEntryReal:start', 'display:createEntryReal:end'); + safePerformance.mark('display:createEntryReal:end'); + safePerformance.measure('display:createEntryReal', 'display:createEntryReal:start', 'display:createEntryReal:end'); - performance.mark('display:createEntry:end'); - performance.measure('display:createEntry', 'display:createEntry:start', 'display:createEntry:end'); + safePerformance.mark('display:createEntry:end'); + safePerformance.measure('display:createEntry', 'display:createEntry:start', 'display:createEntry:end'); if (i === 0) { void this._contentManager.executeMediaRequests(); // prioritize loading media for first entry since it is visible @@ -1470,8 +1471,8 @@ export class Display extends EventDispatcher { } this._triggerContentUpdateComplete(); - performance.mark('display:contentUpdate:end'); - performance.measure('display:contentUpdate', 'display:contentUpdate:start', 'display:contentUpdate:end'); + safePerformance.mark('display:contentUpdate:end'); + safePerformance.measure('display:contentUpdate', 'display:contentUpdate:start', 'display:contentUpdate:end'); } /** */ diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js index 886491f41..c99d7c063 100644 --- a/ext/js/language/text-scanner.js +++ b/ext/js/language/text-scanner.js @@ -20,6 +20,7 @@ import {ThemeController} from '../app/theme-controller.js'; import {EventDispatcher} from '../core/event-dispatcher.js'; import {EventListenerCollection} from '../core/event-listener-collection.js'; import {log} from '../core/log.js'; +import {safePerformance} from '../core/safe-performance.js'; import {clone} from '../core/utilities.js'; import {anyNodeMatchesSelector, everyNodeMatchesSelector, getActiveModifiers, getActiveModifiersAndButtons, isPointInSelection} from '../dom/document-util.js'; import {TextSourceElement} from '../dom/text-source-element.js'; @@ -456,7 +457,7 @@ export class TextScanner extends EventDispatcher { */ async _search(textSource, searchTerms, searchKanji, inputInfo, showEmpty = false, disallowExpandStartOffset = false) { try { - performance.mark('scanner:_search:start'); + safePerformance.mark('scanner:_search:start'); const isAltText = textSource instanceof TextSourceElement; if (inputInfo.pointerType === 'touch') { if (isAltText) { @@ -530,8 +531,8 @@ export class TextScanner extends EventDispatcher { } else { this._triggerSearchEmpty(inputInfo); } - performance.mark('scanner:_search:end'); - performance.measure('scanner:_search', 'scanner:_search:start', 'scanner:_search:end'); + safePerformance.mark('scanner:_search:end'); + safePerformance.measure('scanner:_search', 'scanner:_search:start', 'scanner:_search:end'); } catch (error) { this.trigger('searchError', { error: error instanceof Error ? error : new Error(`A search error occurred: ${error}`), @@ -1260,7 +1261,7 @@ export class TextScanner extends EventDispatcher { if (this._pendingLookup) { return; } try { - performance.mark('scanner:_searchAt:start'); + safePerformance.mark('scanner:_searchAt:start'); const sourceInput = inputInfo.input; let searchTerms = this._searchTerms; let searchKanji = this._searchKanji; @@ -1290,8 +1291,8 @@ export class TextScanner extends EventDispatcher { } else { this._triggerSearchEmpty(inputInfo); } - performance.mark('scanner:_searchAt:end'); - performance.measure('scanner:_searchAt', 'scanner:_searchAt:start', 'scanner:_searchAt:end'); + safePerformance.mark('scanner:_searchAt:end'); + safePerformance.measure('scanner:_searchAt', 'scanner:_searchAt:start', 'scanner:_searchAt:end'); } catch (e) { log.error(e); } finally { diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index 0020db3c7..584a39489 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {safePerformance} from '../core/safe-performance.js'; import {applyTextReplacement} from '../general/regex-util.js'; import {isCodePointJapanese} from './ja/japanese.js'; import {LanguageTransformer} from './language-transformer.js'; @@ -76,7 +77,7 @@ export class Translator { * @returns {Promise<{dictionaryEntries: import('dictionary').TermDictionaryEntry[], originalTextLength: number}>} An object containing dictionary entries and the length of the original source text. */ async findTerms(mode, text, options) { - performance.mark('translator:findTerms:start'); + safePerformance.mark('translator:findTerms:start'); const {enabledDictionaryMap, excludeDictionaryDefinitions, sortFrequencyDictionary, sortFrequencyDictionaryOrder, language, primaryReading} = options; const tagAggregator = new TranslatorTagAggregator(); let {dictionaryEntries, originalTextLength} = await this._findTermsInternal(text, options, tagAggregator, primaryReading); @@ -122,8 +123,8 @@ export class Translator { if (pronunciations.length > 1) { this._sortTermDictionaryEntrySimpleData(pronunciations); } } const withUserFacingInflections = this._addUserFacingInflections(language, dictionaryEntries); - performance.mark('translator:findTerms:end'); - performance.measure('translator:findTerms', 'translator:findTerms:start', 'translator:findTerms:end'); + safePerformance.mark('translator:findTerms:end'); + safePerformance.measure('translator:findTerms', 'translator:findTerms:start', 'translator:findTerms:end'); return {dictionaryEntries: withUserFacingInflections, originalTextLength}; } @@ -373,7 +374,7 @@ export class Translator { * @returns {Promise} */ async _getDeinflections(text, options) { - performance.mark('translator:getDeinflections:start'); + safePerformance.mark('translator:getDeinflections:start'); let deinflections = ( options.deinflect ? this._getAlgorithmDeinflections(text, options) : @@ -396,8 +397,8 @@ export class Translator { } deinflections = deinflections.filter((deinflection) => deinflection.databaseEntries.length); - performance.mark('translator:getDeinflections:end'); - performance.measure('translator:getDeinflections', 'translator:getDeinflections:start', 'translator:getDeinflections:end'); + safePerformance.mark('translator:getDeinflections:end'); + safePerformance.measure('translator:getDeinflections', 'translator:getDeinflections:start', 'translator:getDeinflections:end'); return deinflections; } @@ -409,7 +410,7 @@ export class Translator { * @returns {Promise} */ async _getDictionaryDeinflections(language, deinflections, enabledDictionaryMap, matchType) { - performance.mark('translator:getDictionaryDeinflections:start'); + safePerformance.mark('translator:getDictionaryDeinflections:start'); /** @type {import('translation-internal').DatabaseDeinflection[]} */ const dictionaryDeinflections = []; for (const deinflection of deinflections) { @@ -440,8 +441,8 @@ export class Translator { await this._addEntriesToDeinflections(language, dictionaryDeinflections, enabledDictionaryMap, matchType); - performance.mark('translator:getDictionaryDeinflections:end'); - performance.measure('translator:getDictionaryDeinflections', 'translator:getDictionaryDeinflections:start', 'translator:getDictionaryDeinflections:end'); + safePerformance.mark('translator:getDictionaryDeinflections:end'); + safePerformance.measure('translator:getDictionaryDeinflections', 'translator:getDictionaryDeinflections:start', 'translator:getDictionaryDeinflections:end'); return dictionaryDeinflections; } diff --git a/ext/js/pages/settings/dictionary-controller.js b/ext/js/pages/settings/dictionary-controller.js index e2fcf1e32..662de87a5 100644 --- a/ext/js/pages/settings/dictionary-controller.js +++ b/ext/js/pages/settings/dictionary-controller.js @@ -494,6 +494,8 @@ export class DictionaryController { this._settingsController = settingsController; /** @type {import('./modal-controller.js').ModalController} */ this._modalController = modalController; + /** @type {HTMLElement} */ + this._dictionaryModalBody = querySelectorNotNull(document, '#dictionaries-modal-body'); /** @type {import('./status-footer.js').StatusFooter} */ this._statusFooter = statusFooter; /** @type {?import('dictionary-importer').Summary[]} */ @@ -643,7 +645,7 @@ export class DictionaryController { const event = {source: this}; this._settingsController.trigger('dictionarySettingsReordered', event); - await this._updateEntries(); + this._updateCurrentEntries(options); } /** @@ -822,6 +824,28 @@ export class DictionaryController { } } + /** + * @param {import('settings').ProfileOptions} options + */ + _updateCurrentEntries(options) { + const dictionariesModalBodyScrollY = this._dictionaryModalBody.scrollTop; + const dictionaries = this._dictionaries; + if (dictionaries === null) { return; } + + this._dictionaryEntries.map((dictionaryEntry) => dictionaryEntry.cleanup()); + + const dictionaryOptionsArray = options.dictionaries; + for (let i = 0; i < dictionaryOptionsArray.length; i++) { + const {name} = dictionaryOptionsArray[i]; + /** @type {import('dictionary-importer').Summary | undefined} */ + const dictionaryInfo = dictionaries.find((dictionary) => dictionary.title === name); + if (typeof dictionaryInfo === 'undefined') { continue; } + const updateDownloadUrl = dictionaryInfo.downloadUrl ?? null; + this._createDictionaryEntry(i, dictionaryInfo, updateDownloadUrl); + } + this._dictionaryModalBody.scroll({top: dictionariesModalBodyScrollY}); + } + /** * @param {import('settings').ProfileOptions} options */ diff --git a/ext/templates-modals.html b/ext/templates-modals.html index e7c00e55b..48c599551 100644 --- a/ext/templates-modals.html +++ b/ext/templates-modals.html @@ -11,7 +11,7 @@ -