1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Desktop: Optimised sidebar rendering speed

This commit is contained in:
Laurent Cozic 2020-09-24 14:30:20 +01:00
parent 6ca640d2ed
commit 947d81d96d
9 changed files with 178 additions and 36 deletions

View File

@ -176,6 +176,7 @@ ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js
ReactNativeClient/lib/hooks/useEffectDebugger.js
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js
ReactNativeClient/lib/hooks/usePrevious.js
ReactNativeClient/lib/hooks/usePropsDebugger.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
@ -183,6 +184,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/lib/ntpDate.js
ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/debug/populateDatabase.js
ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js

2
.gitignore vendored
View File

@ -169,6 +169,7 @@ ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js
ReactNativeClient/lib/hooks/useEffectDebugger.js
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js
ReactNativeClient/lib/hooks/usePrevious.js
ReactNativeClient/lib/hooks/usePropsDebugger.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
@ -176,6 +177,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/lib/ntpDate.js
ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/debug/populateDatabase.js
ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js

View File

@ -34,6 +34,7 @@ const KeymapService = require('lib/services/KeymapService').default;
const TemplateUtils = require('lib/TemplateUtils');
const CssUtils = require('lib/CssUtils');
const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/reducer').default;
// const populateDatabase = require('lib/services/debug/populateDatabase').default;
const versionInfo = require('lib/versionInfo').default;
const commands = [
@ -1109,7 +1110,7 @@ class Application extends BaseApplication {
try {
await keymapService.loadCustomKeymap(`${dir}/keymap-desktop.json`);
} catch (err) {
bridge().showErrorMessageBox(err.message);
reg.logger().error(err.message);
}
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
@ -1267,6 +1268,8 @@ class Application extends BaseApplication {
};
bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated);
// await populateDatabase(reg.db());
}
}

View File

@ -45,6 +45,50 @@ const commands = [
require('./commands/focusElementSideBar'),
];
function ExpandIcon(props:any) {
const theme = themeStyle(props.themeId);
const style:any = { width: 16, maxWidth: 16, opacity: 0.5, fontSize: Math.round(theme.toolbarIconSize * 0.8), display: 'flex', justifyContent: 'center' };
if (!props.isVisible) style.visibility = 'hidden';
return <i className={props.isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right'} style={style}></i>;
}
function ExpandLink(props:any) {
return props.hasChildren ? (
<StyledExpandLink href="#" data-folder-id={props.folderId} onClick={props.onFolderToggleClick_}>
<ExpandIcon themeId={props.themeId} isVisible={true} isExpanded={props.isExpanded}/>
</StyledExpandLink>
) : (
<StyledExpandLink><ExpandIcon themeId={props.themeId} isVisible={false} isExpanded={false}/></StyledExpandLink>
);
}
function FolderItem(props:any) {
const { hasChildren, isExpanded, depth, selected, folderId, folderTitle, anchorRef, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_ } = props;
return (
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth}`} onDragStart={onFolderDragStart_} onDragOver={onFolderDragOver_} onDrop={onFolderDrop_} draggable={true} data-folder-id={folderId}>
<ExpandLink themeId={props.themeId} hasChildren={hasChildren} folderId={folderId} onClick={onFolderToggleClick_} isExpanded={isExpanded}/>
<StyledListItemAnchor
ref={anchorRef}
className="list-item"
isConflictFolder={folderId === Folder.conflictFolderId()}
href="#"
selected={selected}
data-id={folderId}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={itemContextMenu}
data-folder-id={folderId}
onClick={() => {
folderItem_click(folderId);
}}
onDoubleClick={onFolderToggleClick_}
>
{folderTitle} { noteCount && <StyledNoteCount>{noteCount}</StyledNoteCount>}
</StyledListItemAnchor>
</StyledListItem>
);
}
class SideBarComponent extends React.Component<Props, State> {
private folderItemsOrder_:any[] = [];
@ -68,8 +112,18 @@ class SideBarComponent extends React.Component<Props, State> {
this.onAllNotesClick_ = this.onAllNotesClick_.bind(this);
this.header_contextMenu = this.header_contextMenu.bind(this);
this.onAddFolderButtonClick = this.onAddFolderButtonClick.bind(this);
this.folderItem_click = this.folderItem_click.bind(this);
}
// componentDidUpdate(prevProps:any, _prevState:any) {
// const props = this.props as any;
// for (const k in this.props) {
// if (prevProps[k] !== props[k]) {
// console.info('Props', k, props[k]);
// }
// }
// }
onFolderDragStart_(event:any) {
const folderId = event.currentTarget.getAttribute('data-folder-id');
if (!folderId) return;
@ -257,10 +311,10 @@ class SideBarComponent extends React.Component<Props, State> {
menu.popup(bridge().window());
}
folderItem_click(folder:any) {
folderItem_click(folderId:string) {
this.props.dispatch({
type: 'FOLDER_SELECT',
id: folder ? folder.id : null,
id: folderId ? folderId : null,
});
}
@ -321,41 +375,26 @@ class SideBarComponent extends React.Component<Props, State> {
}
renderFolderItem(folder:any, selected:boolean, hasChildren:boolean, depth:number) {
const isExpanded = this.props.collapsedFolderIds.indexOf(folder.id) < 0;
const expandIcon = this.renderExpandIcon(isExpanded, hasChildren);
const expandLink = hasChildren ? (
<StyledExpandLink href="#" data-folder-id={folder.id} onClick={this.onFolderToggleClick_}>
{expandIcon}
</StyledExpandLink>
) : (
<StyledExpandLink>{expandIcon}</StyledExpandLink>
);
const anchorRef = this.anchorItemRef('folder', folder.id);
const noteCount = folder.note_count ? this.renderNoteCount(folder.note_count) : '';
return (
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth}`} key={folder.id} onDragStart={this.onFolderDragStart_} onDragOver={this.onFolderDragOver_} onDrop={this.onFolderDrop_} draggable={true} data-folder-id={folder.id}>
{expandLink}
<StyledListItemAnchor
ref={anchorRef}
className="list-item"
isConflictFolder={folder.id === Folder.conflictFolderId()}
href="#"
selected={selected}
data-id={folder.id}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={(event:any) => this.itemContextMenu(event)}
data-folder-id={folder.id}
onClick={() => {
this.folderItem_click(folder);
}}
onDoubleClick={this.onFolderToggleClick_}
>
{Folder.displayTitle(folder)} {noteCount}
</StyledListItemAnchor>
</StyledListItem>
);
return <FolderItem
key={folder.id}
folderId={folder.id}
folderTitle={Folder.displayTitle(folder)}
themeId={this.props.themeId}
depth={depth}
selected={selected}
isExpanded={this.props.collapsedFolderIds.indexOf(folder.id) < 0}
hasChildren={hasChildren}
anchorRef={anchorRef}
noteCount={folder.note_count}
onFolderDragStart_={this.onFolderDragStart_}
onFolderDragOver_={this.onFolderDragOver_}
onFolderDrop_={this.onFolderDrop_}
itemContextMenu={this.itemContextMenu}
folderItem_click={this.folderItem_click}
onFolderToggleClick_={this.onFolderToggleClick_}
/>;
}
renderTag(tag:any, selected:boolean) {

View File

@ -9,6 +9,8 @@ class Database {
this.logger_ = new Logger();
this.logExcludedQueryTypes_ = [];
this.batchTransactionMutex_ = new Mutex();
this.profilingEnabled_ = false;
this.queryId_ = 1;
}
setLogExcludedQueryTypes(v) {
@ -71,10 +73,30 @@ class Database {
let waitTime = 50;
let totalWaitTime = 0;
const callStartTime = Date.now();
let profilingTimeoutId = null;
while (true) {
try {
this.logQuery(sql, params);
const queryId = this.queryId_++;
if (this.profilingEnabled_) {
console.info(`SQL START ${queryId}`, sql, params);
profilingTimeoutId = setInterval(() => {
console.warn(`SQL ${queryId} has been running for ${Date.now() - callStartTime}: ${sql}`);
}, 3000);
}
const result = await this.driver()[callName](sql, params);
if (this.profilingEnabled_) {
clearInterval(profilingTimeoutId);
profilingTimeoutId = null;
const elapsed = Date.now() - callStartTime;
if (elapsed > 10) console.info(`SQL END ${queryId}`, elapsed, sql, params);
}
return result; // No exception was thrown
} catch (error) {
if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) {
@ -89,6 +111,8 @@ class Database {
} else {
throw this.sqliteErrorToJsError(error, sql, params);
}
} finally {
if (profilingTimeoutId) clearInterval(profilingTimeoutId);
}
}
}

View File

@ -0,0 +1,13 @@
import useEffectDebugger from './useEffectDebugger';
export default function usePropsDebugger(effectHook:any, props:any) {
const dependencies:any[] = [];
const dependencyNames:string[] = [];
for (const k in props) {
dependencies.push(props[k]);
dependencyNames.push(k);
}
useEffectDebugger(effectHook, dependencies, dependencyNames);
}

View File

@ -156,6 +156,8 @@ class BaseItem extends BaseModel {
}
static async loadItemsByIds(ids) {
if (!ids.length) return [];
const classes = this.syncItemClassNames();
let output = [];
for (let i = 0; i < classes.length; i++) {

View File

@ -109,6 +109,7 @@ class Tag extends BaseItem {
static async tagsByNoteId(noteId) {
const tagIds = await NoteTag.tagIdsByNoteId(noteId);
if (!tagIds.length) return [];
return this.modelSelectAll(`SELECT * FROM tags WHERE id IN ("${tagIds.join('","')}")`);
}

View File

@ -0,0 +1,56 @@
const Folder = require('lib/models/Folder');
const Note = require('lib/models/Note');
function randomIndex(array:any[]):number {
return Math.round(Math.random() * (array.length - 1));
}
export default async function populateDatabase(db:any) {
await db.clearForTesting();
const folderCount = 2000;
const noteCount = 20000;
const createdFolderIds:string[] = [];
const createdNoteIds:string[] = [];
for (let i = 0; i < folderCount; i++) {
const folder:any = {
title: `folder${i}`,
};
const isRoot = Math.random() <= 0.1 || i === 0;
if (!isRoot) {
const parentIndex = randomIndex(createdFolderIds);
folder.parent_id = createdFolderIds[parentIndex];
}
const savedFolder = await Folder.save(folder);
createdFolderIds.push(savedFolder.id);
console.info(`Folders: ${i} / ${folderCount}`);
}
let noteBatch = [];
for (let i = 0; i < noteCount; i++) {
const note:any = { title: `note${i}`, body: `This is note num. ${i}` };
const parentIndex = randomIndex(createdFolderIds);
note.parent_id = createdFolderIds[parentIndex];
noteBatch.push(Note.save(note, { dispatchUpdateAction: false }).then((savedNote:any) => {
createdNoteIds.push(savedNote.id);
console.info(`Notes: ${i} / ${noteCount}`);
}));
if (noteBatch.length > 1000) {
await Promise.all(noteBatch);
noteBatch = [];
}
}
if (noteBatch.length) {
await Promise.all(noteBatch);
noteBatch = [];
}
}