fix #1580: high memory usage on large files, virtual scrolling of entries

This commit is contained in:
antelle 2021-03-20 19:17:57 +01:00
parent 53b5715da0
commit fddf2892cd
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
8 changed files with 171 additions and 47 deletions

View File

@ -251,7 +251,6 @@ class View extends EventEmitter {
FocusManager.setModal(null);
}
}
this.emit(visible ? 'show' : 'hide');
if (this.el) {
this.el.classList.toggle('show', !!visible);
this.el.classList.toggle('hide', !visible);
@ -259,6 +258,7 @@ class View extends EventEmitter {
Tip.hideTips(this.el);
}
}
this.emit(visible ? 'show' : 'hide');
}
isHidden() {

View File

@ -202,7 +202,6 @@ class AppView extends View {
this.views.menu.show();
this.views.menuDrag.$el.parent().show();
this.views.listWrap.show();
this.views.list.show();
this.views.listDrag.show();
this.views.details.show();
this.views.footer.show();
@ -211,6 +210,8 @@ class AppView extends View {
this.hideSettings();
this.hideKeyChange();
this.hideImportCsv();
this.views.list.show();
}
hideOpenFile() {

View File

@ -68,12 +68,20 @@ class ListView extends View {
this.readTableColumnsEnabled();
this.items = new SearchResultCollection();
this.renderedItems = new Map();
}
render() {
if (!this.isVisible()) {
this.pendingRender = true;
return;
}
this.pendingRender = false;
if (!this.itemsEl) {
super.render();
this.itemsEl = this.$el.find('.list__items>.scroller');
this.itemsEl.on('scroll', () => this.renderVisibleItems());
this.views.search.render();
this.setTableView();
@ -84,9 +92,9 @@ class ListView extends View {
});
}
if (this.items.length) {
const itemTemplate = this.getItemTemplate();
const itemsTemplate = this.getItemsTemplate();
const noColor = AppSettingsModel.colorfulIcons ? '' : 'grayscale';
const presenter = new EntryPresenter(
this.getDescField(),
noColor,
@ -99,16 +107,28 @@ class ListView extends View {
}
});
presenter.columns = columns;
let itemsHtml = '';
this.items.forEach((item) => {
presenter.present(item);
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
}, this);
this.presenter = presenter;
presenter.present(this.items[0]);
const itemTemplate = this.getItemTemplate();
const itemsHtml = itemTemplate(presenter, DefaultTemplateOptions);
const html = itemsTemplate(
{ itemsHtml, columns: this.tableColumns },
DefaultTemplateOptions
);
this.itemsEl.html(html);
this.itemsContainerEl = this.itemsEl.find('.list__items-container:first')[0];
const firstListItem = this.itemsContainerEl.firstElementChild;
this.itemHeight = firstListItem.getBoundingClientRect().height;
this.renderedItems = new Map([[0, firstListItem]]);
const totalHeight = this.itemHeight * this.items.length;
this.itemsContainerEl.style.minHeight = totalHeight + 'px';
this.renderVisibleItems();
} else {
this.itemsEl.html(this.emptyTemplate({}, DefaultTemplateOptions));
}
@ -117,16 +137,12 @@ class ListView extends View {
getItemsTemplate() {
if (this.model.settings.tableView) {
return require('templates/list-table.hbs');
return require('templates/list-mode-table.hbs');
} else {
return this.renderPlainItems;
return require('templates/list-mode-list.hbs');
}
}
renderPlainItems(data) {
return data.itemsHtml;
}
getItemTemplate() {
if (this.model.settings.tableView) {
return require('templates/list-item-table.hbs');
@ -135,6 +151,88 @@ class ListView extends View {
}
}
renderVisibleItems() {
if (!this.isVisible()) {
return;
}
const scrollEl = this.itemsEl[0];
const rect = scrollEl.getBoundingClientRect();
const pxTop = scrollEl.scrollTop;
const pxHeight = rect.height;
const itemHeight = this.itemHeight;
const renderedItems = this.renderedItems;
let firstIx = Math.max(0, Math.floor(pxTop / itemHeight));
let lastIx = Math.min(this.items.length - 1, Math.ceil((pxTop + pxHeight) / itemHeight));
const visibleCount = lastIx - firstIx;
firstIx = Math.max(0, firstIx - visibleCount);
lastIx = Math.min(this.items.length - 1, lastIx + visibleCount);
const itemTemplate = this.getItemTemplate();
const presenter = this.presenter;
let itemsHtml = '';
const renderedIndices = [];
for (let ix = firstIx; ix <= lastIx; ix++) {
const item = this.items[ix];
if (renderedItems.has(ix)) {
continue;
}
presenter.present(item);
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
renderedIndices.push(ix);
}
const tempEl = document.createElement('div');
tempEl.innerHTML = itemsHtml;
const renderedElements = [...tempEl.children];
for (let i = 0; i < renderedElements.length; i++) {
const el = renderedElements[i];
const ix = renderedIndices[i];
this.itemsContainerEl.append(el);
el.style.top = ix * itemHeight + 'px';
renderedItems.set(ix, el);
}
const maxRenderedItems = visibleCount * 5;
if (renderedItems.size > maxRenderedItems) {
for (const [ix, el] of this.renderedItems) {
if (ix < firstIx || ix > lastIx) {
el.remove();
renderedItems.delete(ix);
}
}
}
}
ensureItemRendered(ix) {
if (this.renderedItems.has(ix)) {
return;
}
const item = this.items[ix];
const itemTemplate = this.getItemTemplate();
this.presenter.present(item);
const itemHtml = itemTemplate(this.presenter, DefaultTemplateOptions);
const tempEl = document.createElement('div');
tempEl.innerHTML = itemHtml;
const [el] = tempEl.children;
this.itemsContainerEl.append(el);
el.style.top = ix * this.itemHeight + 'px';
this.renderedItems.set(ix, el);
}
getDescField() {
return this.model.sort.replace('-', '');
}
@ -203,7 +301,12 @@ class ListView extends View {
}
selectItem(item) {
this.presenter.activeEntryId = item.id;
this.model.activeEntryId = item.id;
const ix = this.items.indexOf(item);
this.ensureItemRendered(ix);
Events.emit('entry-selected', item);
this.itemsEl.find('.list__item--active').removeClass('list__item--active');
const itemEl = document.getElementById(item.id);
@ -220,6 +323,9 @@ class ListView extends View {
viewShown() {
this.views.search.show();
if (this.pendingRender) {
this.render();
}
}
viewHidden() {
@ -248,6 +354,7 @@ class ListView extends View {
viewResized(size) {
this.setSize(size);
this.throttleSetViewSizeSetting(size);
this.renderVisibleItems();
}
throttleSetViewSizeSetting = throttle((size) => {
@ -256,6 +363,7 @@ class ListView extends View {
filterChanged(filter) {
this.items = filter.entries;
this.renderedItems = new Map();
this.render();
}

View File

@ -119,20 +119,29 @@
}
&__table {
border-collapse: collapse;
width: calc(100% - 2px);
td,
th {
&-body {
position: relative;
}
&-row {
display: flex;
align-items: center;
width: 100%;
}
&-cell {
padding: $base-padding;
text-align: left;
flex-grow: 1;
flex-shrink: 0;
flex-basis: 0;
&:first-child {
text-align: center;
padding: 0;
width: 3em;
flex-basis: 3em;
flex-grow: 0;
flex-shrink: 0;
}
}
th:first-child {
padding: 0;
width: 3em;
}
&-options {
@include icon-btn();
cursor: pointer;
@ -140,10 +149,13 @@
}
&__item {
padding: $base-padding;
left: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
position: absolute;
top: 0;
box-sizing: border-box;
@include nomobile {
@include area-selectable();
&--active,
@ -163,7 +175,9 @@
@include nomobile {
border-radius: var(--block-border-radius);
}
padding: $base-padding;
margin: 0 $small-spacing;
width: calc(100% - #{$small-spacing * 2});
}
&--expired {

View File

@ -1,16 +1,16 @@
<tr class="list__item list__item--table {{#if active}}list__item--active{{/if}} {{#if expired}}list__item--expired{{/if}}" id="{{id}}" draggable="true">
<td>
<div class="list__item list__item--table list__table-row {{#if active}}list__item--active{{/if}} {{#if expired}}list__item--expired{{/if}}" id="{{id}}" draggable="true">
<div class="list__table-cell">
{{~#if customIcon~}}
<img src="{{customIcon}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
{{~else~}}
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} list__item-icon"></i>
{{~/if~}}
</td>
{{#if columns.title}}<td>{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</td>{{/if}}
{{#if columns.user}}<td>{{user}}</td>{{/if}}
{{#if columns.url}}<td>{{url}}</td>{{/if}}
{{#if columns.tags}}<td>{{tags}}</td>{{/if}}
{{#if columns.notes}}<td>{{notes}}</td>{{/if}}
{{#if columns.groupName}}<td>{{groupName}}</td>{{/if}}
{{#if columns.fileName}}<td>{{fileName}}</td>{{/if}}
</tr>
</div>
{{#if columns.title}}<div class="list__table-cell">{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</div>{{/if}}
{{#if columns.user}}<div class="list__table-cell">{{user}}</div>{{/if}}
{{#if columns.url}}<div class="list__table-cell">{{url}}</div>{{/if}}
{{#if columns.tags}}<div class="list__table-cell">{{tags}}</div>{{/if}}
{{#if columns.notes}}<div class="list__table-cell">{{notes}}</div>{{/if}}
{{#if columns.groupName}}<div class="list__table-cell">{{groupName}}</div>{{/if}}
{{#if columns.fileName}}<div class="list__table-cell">{{fileName}}</div>{{/if}}
</div>

View File

@ -0,0 +1,3 @@
<div class="list__list list__items-container">
{{{itemsHtml}}}
</div>

View File

@ -0,0 +1,11 @@
<div class="list__table">
<div class="list__table-head list__table-row">
<div class="list__table-cell"><i class="fa fa-bars muted-color list__table-options"></i></div>
{{#each columns as |col|}}
{{#if col.enabled}}<div class="list__table-cell">{{Res col.name}}</div>{{/if}}
{{/each}}
</div>
<div class="list__table-body list__items-container">
{{{itemsHtml}}}
</div>
</div>

View File

@ -1,13 +0,0 @@
<table class="list__table">
<thead>
<tr>
<th><i class="fa fa-bars muted-color list__table-options"></i></th>
{{#each columns as |col|}}
{{#if col.enabled}}<th>{{Res col.name}}</th>{{/if}}
{{/each}}
</tr>
</thead>
<tbody>
{{{itemsHtml}}}
</tbody>
</table>