1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Desktop: Fixes #3893 (maybe): Trying to fix sidebar performance issue when there are many notebooks or tags

This commit is contained in:
Laurent Cozic 2020-10-19 23:24:40 +01:00
parent 3a57cfea02
commit 8254206f44
14 changed files with 189 additions and 66 deletions

View File

@ -180,6 +180,7 @@ ElectronClient/gui/SideBar/styles/index.js
ElectronClient/gui/StatusScreen/StatusScreen.js
ElectronClient/gui/style/StyledInput.js
ElectronClient/gui/style/StyledTextInput.js
ElectronClient/gui/TagList.js
ElectronClient/gui/ToggleEditorsButton/styles/index.js
ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.js
ElectronClient/gui/ToolbarBase.js

1
.gitignore vendored
View File

@ -174,6 +174,7 @@ ElectronClient/gui/SideBar/styles/index.js
ElectronClient/gui/StatusScreen/StatusScreen.js
ElectronClient/gui/style/StyledInput.js
ElectronClient/gui/style/StyledTextInput.js
ElectronClient/gui/TagList.js
ElectronClient/gui/ToggleEditorsButton/styles/index.js
ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.js
ElectronClient/gui/ToolbarBase.js

View File

@ -123,6 +123,7 @@ ElectronClient/gui/SideBar/styles/index.js
ElectronClient/gui/StatusScreen/StatusScreen.js
ElectronClient/gui/style/StyledInput.js
ElectronClient/gui/style/StyledTextInput.js
ElectronClient/gui/TagList.js
ElectronClient/gui/ToggleEditorsButton/styles/index.js
ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.js
ElectronClient/gui/ToolbarBase.js

View File

@ -26,6 +26,7 @@ import { AppState } from '../../app';
import ToolbarButtonUtils from 'lib/services/commands/ToolbarButtonUtils';
import { _ } from 'lib/locale';
import stateToWhenClauseContext from 'lib/services/commands/stateToWhenClauseContext';
import TagList from '../TagList';
const { themeStyle } = require('lib/theme');
const { substrWithEllipsis } = require('lib/string-utils');
@ -39,7 +40,6 @@ const Note = require('lib/models/Note.js');
const bridge = require('electron').remote.require('./bridge').default;
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
const NoteRevisionViewer = require('../NoteRevisionViewer.min');
const TagList = require('../TagList.min.js');
const commands = [
require('./commands/showRevisions'),

View File

@ -1,4 +1,3 @@
import * as React from 'react';
import app from '../app';
import MainScreen from './MainScreen/MainScreen';
import ConfigScreen from './ConfigScreen/ConfigScreen';
@ -10,6 +9,7 @@ import { themeStyle } from 'lib/theme';
import { Size } from './ResizableLayout/ResizableLayout';
import MenuBar from './MenuBar';
import { _ } from 'lib/locale';
const React = require('react');
const { render } = require('react-dom');
const { connect, Provider } = require('react-redux');

View File

@ -569,6 +569,20 @@ class SideBarComponent extends React.Component<Props, State> {
CommandService.instance().execute('newFolder');
}
// componentDidUpdate(prevProps:any, prevState:any) {
// for (const n in prevProps) {
// if (prevProps[n] !== (this.props as any)[n]) {
// console.info('CHANGED PROPS', n);
// }
// }
// for (const n in prevState) {
// if (prevState[n] !== (this.state as any)[n]) {
// console.info('CHANGED STATE', n);
// }
// }
// }
render() {
const theme = themeStyle(this.props.themeId);

View File

@ -1,52 +0,0 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('lib/theme');
const TagItem = require('./TagItem.min.js');
class TagListComponent extends React.Component {
render() {
const style = Object.assign({}, this.props.style);
const theme = themeStyle(this.props.themeId);
const tags = this.props.items;
style.display = 'flex';
style.flexDirection = 'row';
// style.borderBottom = `1px solid ${theme.dividerColor}`;
style.boxSizing = 'border-box';
style.fontSize = theme.fontSize;
style.whiteSpace = 'nowrap';
// style.height = 40;
style.paddingTop = 8;
style.paddingBottom = 8;
const tagItems = [];
if (tags && tags.length > 0) {
tags.sort((a, b) => {
return a.title < b.title ? -1 : +1;
});
for (let i = 0; i < tags.length; i++) {
const props = {
title: tags[i].title,
key: tags[i].id,
};
tagItems.push(<TagItem {...props} />);
}
}
return (
<div className="tag-list" style={style}>
{tagItems}
</div>
);
}
}
const mapStateToProps = state => {
return { themeId: state.settings.theme };
};
const TagList = connect(mapStateToProps)(TagListComponent);
module.exports = TagList;

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import { useMemo } from 'react';
import { AppState } from '../app';
const { connect } = require('react-redux');
const { themeStyle } = require('lib/theme');
const TagItem = require('./TagItem.min.js');
interface Props {
themeId: number,
style: any,
items: any[],
}
function TagList(props:Props) {
const style = useMemo(() => {
const theme = themeStyle(props.themeId);
const output = { ...props.style };
output.display = 'flex';
output.flexDirection = 'row';
output.boxSizing = 'border-box';
output.fontSize = theme.fontSize;
output.whiteSpace = 'nowrap';
output.paddingTop = 8;
output.paddingBottom = 8;
return output;
}, [props.style, props.themeId]);
const tags = useMemo(() => {
const output = props.items.slice();
output.sort((a:any, b:any) => {
return a.title < b.title ? -1 : +1;
});
return output;
}, [props.items]);
const tagItems = useMemo(() => {
const output = [];
for (let i = 0; i < tags.length; i++) {
const props = {
title: tags[i].title,
key: tags[i].id,
};
output.push(<TagItem {...props} />);
}
return output;
}, [tags]);
return (
<div className="tag-list" style={style}>
{tagItems}
</div>
);
}
const mapStateToProps = (state:AppState) => {
return { themeId: state.settings.theme };
};
export default connect(mapStateToProps)(TagList);

View File

@ -546,7 +546,13 @@ export default class BaseApplication {
await this.refreshNotes(newState, refreshNotesUseSelectedNoteId, refreshNotesHash);
}
if (action.type === 'NOTE_UPDATE_ONE' || action.type === 'NOTE_DELETE') {
if (action.type === 'NOTE_UPDATE_ONE') {
if (!action.changedFields.length || action.changedFields.includes('parent_id') || action.changedFields.includes('encryption_applied')) {
refreshFolders = true;
}
}
if (action.type === 'NOTE_DELETE') {
refreshFolders = true;
}

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import { View, Button, Text } from 'react-native';
import { themeStyle } from 'lib/theme';
import { _ } from 'lib/locale';
const { View, Button, Text } = require('react-native');
const PopupDialog = require('react-native-popup-dialog').default;
const { DialogTitle, DialogButton } = require('react-native-popup-dialog');

View File

@ -43,8 +43,11 @@ const reduxSharedMiddleware = async function(store, next, action) {
DecryptionWorker.instance().scheduleStart();
}
// 2020-10-19: Removed "NOTE_UPDATE_ONE" because there's no property in a note that
// should trigger a refreshing of the tags.
// Trying to fix this: https://github.com/laurent22/joplin/issues/3893
if (action.type == 'NOTE_DELETE' ||
action.type == 'NOTE_UPDATE_ONE' ||
// action.type == 'NOTE_UPDATE_ONE' ||
action.type == 'NOTE_UPDATE_ALL' ||
action.type == 'NOTE_TAG_REMOVE' ||
action.type == 'TAG_UPDATE_ONE') {

View File

@ -587,10 +587,29 @@ class Note extends BaseItem {
// decide what to keep and what to ignore, but in practice keeping the previous content is a bit
// heavy - the note needs to be reloaded here, the JSON blob needs to be saved, etc.
// So the check for old note here is basically an optimisation.
// 2020-10-19: It's not ideal to reload the previous version of the note before saving it again
// but it should be relatively fast anyway. This is so that code that listens to the NOTE_UPDATE_ONE
// action can decide what to do based on the fields that have been modified.
// This is necessary for example so that the folder list is not refreshed every time a note is changed.
// Now it can look at the properties and refresh only if the "parent_id" property is changed.
// Trying to fix: https://github.com/laurent22/joplin/issues/3893
const oldNote = await Note.load(o.id);
let beforeNoteJson = null;
if (!isNew && this.revisionService().isOldNote(o.id)) {
beforeNoteJson = await Note.load(o.id);
if (beforeNoteJson) beforeNoteJson = JSON.stringify(beforeNoteJson);
if (oldNote) beforeNoteJson = JSON.stringify(oldNote);
}
const changedFields = [];
if (oldNote) {
for (const field in o) {
if (!o.hasOwnProperty(field)) continue;
if (o[field] !== oldNote[field]) {
changedFields.push(field);
}
}
}
const note = await super.save(o, options);
@ -603,6 +622,7 @@ class Note extends BaseItem {
type: 'NOTE_UPDATE_ONE',
note: note,
provisional: isProvisional,
changedFields: changedFields,
});
}

View File

@ -176,7 +176,12 @@ class Tag extends BaseItem {
}
static async save(o, options = null) {
if (options && options.userSideValidation) {
options = Object.assign({}, {
dispatchUpdateAction: true,
userSideValidation: false,
}, options);
if (options.userSideValidation) {
if ('title' in o) {
o.title = o.title.trim().toLowerCase();
@ -186,10 +191,13 @@ class Tag extends BaseItem {
}
return super.save(o, options).then(tag => {
if (options.dispatchUpdateAction) {
this.dispatch({
type: 'TAG_UPDATE_ONE',
item: tag,
});
}
return tag;
});
}

View File

@ -1,18 +1,42 @@
const Folder = require('lib/models/Folder');
const Note = require('lib/models/Note');
const Tag = require('lib/models/Tag');
function randomIndex(array:any[]):number {
return Math.round(Math.random() * (array.length - 1));
}
function randomIndexes(arrayLength:number, count:number):number[] {
const arr = [];
while (arr.length < count) {
const r = Math.floor(Math.random() * arrayLength);
if (arr.indexOf(r) === -1) arr.push(r);
}
return arr;
}
function randomElements(array:any[], count:number):any[] {
const indexes = randomIndexes(array.length, count);
const output = [];
for (const index of indexes) {
output.push(array[index]);
}
return output;
}
// Use the constants below to define how many folders, notes and tags
// should be created.
export default async function populateDatabase(db:any) {
await db.clearForTesting();
const folderCount = 2000;
const noteCount = 20000;
const folderCount = 200;
const noteCount = 1000;
const tagCount = 5000;
const tagsPerNote = 10;
const createdFolderIds:string[] = [];
const createdNoteIds:string[] = [];
const createdTagIds:string[] = [];
for (let i = 0; i < folderCount; i++) {
const folder:any = {
@ -32,6 +56,24 @@ export default async function populateDatabase(db:any) {
console.info(`Folders: ${i} / ${folderCount}`);
}
let tagBatch = [];
for (let i = 0; i < tagCount; i++) {
tagBatch.push(Tag.save({ title: `tag${i}` }, { dispatchUpdateAction: false }).then((savedTag:any) => {
createdTagIds.push(savedTag.id);
console.info(`Tags: ${i} / ${tagCount}`);
}));
if (tagBatch.length > 1000) {
await Promise.all(tagBatch);
tagBatch = [];
}
}
if (tagBatch.length) {
await Promise.all(tagBatch);
tagBatch = [];
}
let noteBatch = [];
for (let i = 0; i < noteCount; i++) {
const note:any = { title: `note${i}`, body: `This is note num. ${i}` };
@ -53,4 +95,20 @@ export default async function populateDatabase(db:any) {
await Promise.all(noteBatch);
noteBatch = [];
}
let noteTagBatch = [];
for (const noteId of createdNoteIds) {
const tagIds = randomElements(createdTagIds, tagsPerNote);
noteTagBatch.push(Tag.setNoteTagsByIds(noteId, tagIds));
if (noteTagBatch.length > 1000) {
await Promise.all(noteTagBatch);
noteTagBatch = [];
}
}
if (noteTagBatch.length) {
await Promise.all(noteTagBatch);
noteTagBatch = [];
}
}