From a74c012b65d29b17c93e2b44a8101d7d57606345 Mon Sep 17 00:00:00 2001 From: Martynas Bagdonas Date: Mon, 3 May 2021 21:12:33 +0300 Subject: [PATCH] Improve contextPane notes list performance --- .../zotero/components/itemPane/notesList.jsx | 33 ++++++++++++----- chrome/content/zotero/contextPane.js | 35 +++++++++++++++---- chrome/content/zotero/xpcom/date.js | 17 +++++++-- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/chrome/content/zotero/components/itemPane/notesList.jsx b/chrome/content/zotero/components/itemPane/notesList.jsx index aced7d0b3b..08611e5ca8 100644 --- a/chrome/content/zotero/components/itemPane/notesList.jsx +++ b/chrome/content/zotero/components/itemPane/notesList.jsx @@ -26,7 +26,7 @@ import React, { forwardRef, useImperativeHandle, useState, memo } from 'react'; import cx from 'classnames'; -const MAX_ALL_NOTES = 7; +const MAX_UNEXPANDED_ALL_NOTES = 7; const NoteRow = memo(({ id, title, body, date, onClick, onContextMenu, parentItemType, parentTitle }) => { return ( @@ -54,15 +54,32 @@ const NoteRow = memo(({ id, title, body, date, onClick, onContextMenu, parentIte const NotesList = forwardRef(({ onClick, onContextMenu, onAddChildButtonDown, onAddStandaloneButtonDown }, ref) => { const [notes, setNotes] = useState([]); const [expanded, setExpanded] = useState(false); + const [numVisible, setNumVisible] = useState(0); const [hasParent, setHasParent] = useState(true); - useImperativeHandle(ref, () => ({ setNotes, setExpanded, setHasParent })); + + const _setExpanded = (value) => { + setExpanded(value); + if (value) { + setNumVisible(numVisible + 1000); + } + else { + setNumVisible(0); + } + }; + + useImperativeHandle(ref, () => ({ + setNotes, + setHasParent, + setExpanded: _setExpanded + })); function handleClickMore() { - setExpanded(true); + _setExpanded(true); } let childNotes = notes.filter(x => x.isCurrentChild); let allNotes = notes.filter(x => !x.isCurrentChild); + let visibleNotes = allNotes.slice(0, expanded ? numVisible : MAX_UNEXPANDED_ALL_NOTES); return (
{hasParent &&
@@ -80,12 +97,12 @@ const NotesList = forwardRef(({ onClick, onContextMenu, onAddChildButtonDown, on
{!allNotes.length &&
{Zotero.getString('pane.context.noNotes')}
} - {(expanded ? allNotes : allNotes.slice(0, MAX_ALL_NOTES)) - .map(note => )} - {!expanded && allNotes.length > MAX_ALL_NOTES + {visibleNotes.map(note => )} + {allNotes.length > visibleNotes.length &&
{ - Zotero.getString('general.numMore', Zotero.Utilities.numberFormat([allNotes.length - MAX_ALL_NOTES], 0)) + Zotero.getString('general.numMore', Zotero.Utilities.numberFormat( + [(allNotes.length - visibleNotes.length) - MAX_UNEXPANDED_ALL_NOTES], 0)) }
} diff --git a/chrome/content/zotero/contextPane.js b/chrome/content/zotero/contextPane.js index 67a9e0c41a..9b65b5205a 100644 --- a/chrome/content/zotero/contextPane.js +++ b/chrome/content/zotero/contextPane.js @@ -156,6 +156,9 @@ var ZoteroContextPane = new function () { } } else if (action == 'select') { + // It seems that changing `hidden` or `collapsed` values might + // be related with significant slow down when there are too many + // DOM nodes (i.e. 10k notes) if (Zotero_Tabs.selectedIndex == 0) { _contextPaneSplitter.setAttribute('hidden', true); _contextPane.setAttribute('collapsed', true); @@ -470,9 +473,27 @@ var ZoteroContextPane = new function () { var notesListRef = React.createRef(); + function _isVisible() { + let splitter = Zotero.Prefs.get('layout') == 'stacked' + ? _contextPaneSplitterStacked : _contextPaneSplitter; + + return Zotero_Tabs.selectedID != 'zotero-pane' + && _panesDeck.selectedIndex == 1 + && context.node.selectedIndex == 0 + && splitter.getAttribute('state') != 'collapsed'; + } + async function _updateNotesList(useCached) { var query = input.value; var notes; + + // Calls itself and debounces until notes list becomes + // visible, and then updates + if (!useCached && !_isVisible()) { + context.update(); + return; + } + if (useCached && context.cachedNotes.length) { notes = context.cachedNotes; } @@ -490,22 +511,25 @@ var ZoteroContextPane = new function () { notes = await s.search(); notes = Zotero.Items.get(notes); notes.sort((a, b) => { - a = a.getField('dateModified'); - b = b.getField('dateModified'); - return b.localeCompare(a); + a = a.dateModified; + b = b.dateModified; + return (a > b ? -1 : (a < b ? 1 : 0)); }); + let cachedNotesIndex = new Map(); + for (let cachedNote of context.cachedNotes) { + cachedNotesIndex.set(cachedNote.id, cachedNote); + } notes = notes.map(note => { var parentItem = note.parentItem; // If neither note nor parent item is affected try to return the cached note if (!context.affectedIDs.has(note.id) && (!parentItem || !context.affectedIDs.has(parentItem.id))) { - let cachedNote = context.cachedNotes.find(x => x.id == note.id); + let cachedNote = cachedNotesIndex.get(note.id); if (cachedNote) { return cachedNote; } } - var text = note.note; text = Zotero.Utilities.unescapeHTML(text); text = text.trim(); @@ -513,7 +537,6 @@ var ZoteroContextPane = new function () { var parts = text.split('\n').map(x => x.trim()).filter(x => x.length); var title = parts[0] && parts[0].slice(0, Zotero.Notes.MAX_TITLE_LENGTH); var date = Zotero.Date.sqlToDate(note.dateModified, true); - // This takes half of the CPU time date = Zotero.Date.toFriendlyDate(date); return { diff --git a/chrome/content/zotero/xpcom/date.js b/chrome/content/zotero/xpcom/date.js index 848a6acc84..9f13c0fc0b 100644 --- a/chrome/content/zotero/xpcom/date.js +++ b/chrome/content/zotero/xpcom/date.js @@ -852,17 +852,28 @@ Zotero.Date = new function(){ return Zotero.getString("date.relative." + key + "." + (n ? "multiple" : "one"), n); } + // Initializing `toFriendlyDate` formatters, since + // `toLocaleDateString` is extremely slow (4500ms vs 200ms + // for 10k calls) + var _friendlyDateTodayFormatter = new Intl.DateTimeFormat( + false, { hour: 'numeric', minute: 'numeric' }); + + var _friendlyDateWeekFormatter = new Intl.DateTimeFormat( + false, { weekday: 'long' }); + + var _friendlyDateRegularFormatter = new Intl.DateTimeFormat( + false, { year: '2-digit', month: 'numeric', day: 'numeric' }); this.toFriendlyDate = function (date) { // 6:14:36 PM if (isToday(date)) { - return date.toLocaleString(false, { hour: 'numeric', minute: 'numeric' }) + return _friendlyDateTodayFormatter.format(date); } // 'Thursday' if (isThisWeek(date)) { - return date.toLocaleString(false, { weekday: 'long' }); + return _friendlyDateWeekFormatter.format(date); } - return date.toLocaleDateString(false, { year: '2-digit', month: 'numeric', day: 'numeric' }); + return _friendlyDateRegularFormatter.format(date); };