Rewrite page loading cache algorithm

* Refactor the page cache algorithm
* Add a page preloading feature
* Add settings for number of pages to preload
* Add option to evict from page cache on page scroll (enabled by default)

In practice, evict on page scroll reduces memory consumption much more than when
having it off, so it has been enabled by default.
upstream-master
Bryan Tan 5 years ago committed by Bryan Tan
parent 0c07b2663c
commit cc46ff7e25
  1. 42
      src/control/settings/Settings.cpp
  2. 23
      src/control/settings/Settings.h
  3. 64
      src/gui/XournalView.cpp
  4. 4
      src/gui/XournalView.h
  5. 16
      src/gui/dialog/SettingsDialog.cpp
  6. 110
      ui/settings.glade

@ -139,6 +139,9 @@ void Settings::loadDefault() {
this->pageRerenderThreshold = 5.0;
this->pdfPageCacheSize = 10;
this->preloadPagesBefore = 3U;
this->preloadPagesAfter = 5U;
this->eagerPageCleanup = true;
this->selectionBorderColor = 0xff0000U; // red
this->selectionMarkerColor = 0x729fcfU; // light blue
@ -398,6 +401,12 @@ void Settings::parseItem(xmlDocPtr doc, xmlNodePtr cur) {
this->pageRerenderThreshold = g_ascii_strtod(reinterpret_cast<const char*>(value), nullptr);
} else if (xmlStrcmp(name, reinterpret_cast<const xmlChar*>("pdfPageCacheSize")) == 0) {
this->pdfPageCacheSize = g_ascii_strtoll(reinterpret_cast<const char*>(value), nullptr, 10);
} else if (xmlStrcmp(name, reinterpret_cast<const xmlChar*>("preloadPagesBefore")) == 0) {
this->preloadPagesBefore = g_ascii_strtoull(reinterpret_cast<const char*>(value), nullptr, 10);
} else if (xmlStrcmp(name, reinterpret_cast<const xmlChar*>("preloadPagesAfter")) == 0) {
this->preloadPagesAfter = g_ascii_strtoull(reinterpret_cast<const char*>(value), nullptr, 10);
} else if (xmlStrcmp(name, reinterpret_cast<const xmlChar*>("eagerPageCleanup")) == 0) {
this->eagerPageCleanup = xmlStrcmp(value, reinterpret_cast<const xmlChar*>("true")) == 0;
} else if (xmlStrcmp(name, reinterpret_cast<const xmlChar*>("selectionBorderColor")) == 0) {
this->selectionBorderColor = Color(g_ascii_strtoull(reinterpret_cast<const char*>(value), nullptr, 10));
} else if (xmlStrcmp(name, reinterpret_cast<const xmlChar*>("selectionMarkerColor")) == 0) {
@ -843,6 +852,9 @@ void Settings::save() {
WRITE_INT_PROP(pdfPageCacheSize);
WRITE_COMMENT("The count of rendered PDF pages which will be cached.");
WRITE_UINT_PROP(preloadPagesBefore);
WRITE_UINT_PROP(preloadPagesAfter);
WRITE_BOOL_PROP(eagerPageCleanup);
WRITE_COMMENT("Config for new pages");
WRITE_STRING_PROP(pageTemplate);
@ -1624,6 +1636,36 @@ void Settings::setPdfPageCacheSize(int size) {
save();
}
auto Settings::getPreloadPagesBefore() const -> unsigned int { return this->preloadPagesBefore; }
void Settings::setPreloadPagesBefore(unsigned int n) {
if (this->preloadPagesBefore == n) {
return;
}
this->preloadPagesBefore = n;
save();
}
auto Settings::getPreloadPagesAfter() const -> unsigned int { return this->preloadPagesAfter; }
void Settings::setPreloadPagesAfter(unsigned int n) {
if (this->preloadPagesAfter == n) {
return;
}
this->preloadPagesAfter = n;
save();
}
auto Settings::isEagerPageCleanup() const -> bool { return this->eagerPageCleanup; }
void Settings::setEagerPageCleanup(bool b) {
if (this->eagerPageCleanup == b) {
return;
}
this->eagerPageCleanup = b;
save();
}
auto Settings::getBorderColor() const -> Color { return this->selectionBorderColor; }
void Settings::setBorderColor(Color color) {

@ -330,6 +330,15 @@ public:
int getPdfPageCacheSize() const;
[[maybe_unused]] void setPdfPageCacheSize(int size);
unsigned int getPreloadPagesBefore() const;
void setPreloadPagesBefore(unsigned int n);
unsigned int getPreloadPagesAfter() const;
void setPreloadPagesAfter(unsigned int n);
bool isEagerPageCleanup() const;
void setEagerPageCleanup(bool b);
string const& getPageTemplate() const;
void setPageTemplate(const string& pageTemplate);
@ -937,4 +946,18 @@ private:
* e.g. "en_US"
*/
std::string preferredLocale;
/**
* The number of pages to pre-load before the current page.
*/
unsigned int preloadPagesBefore{};
/**
* The number of pages to pre-load after the current page.
*/
unsigned int preloadPagesAfter{};
/**
* Whether to evict from the page buffer cache when scrolling.
*/
bool eagerPageCleanup{};
};

@ -1,5 +1,6 @@
#include "XournalView.h"
#include <algorithm>
#include <cmath>
#include <memory>
#include <tuple>
@ -24,6 +25,14 @@
#include "XournalppCursor.h"
#include "filesystem.h"
std::pair<size_t, size_t> XournalView::preloadPageBounds(size_t page, size_t maxPage) {
const size_t preloadBefore = this->control->getSettings()->getPreloadPagesBefore();
const size_t preloadAfter = this->control->getSettings()->getPreloadPagesAfter();
const size_t lower = page > preloadBefore ? page - preloadBefore : 0;
const size_t upper = std::min(maxPage, page + preloadAfter);
return {lower, upper};
}
XournalView::XournalView(GtkWidget* parent, Control* control, ScrollHandling* scrollHandling):
scrollHandling(scrollHandling), control(control) {
this->cache = new PdfCache(control->getSettings()->getPdfPageCacheSize());
@ -87,40 +96,24 @@ void XournalView::staticLayoutPages(GtkWidget* widget, GtkAllocation* allocation
xv->layoutPages();
}
auto XournalView::clearMemoryTimer(XournalView* widget) -> gboolean {
GList* list = nullptr;
for (auto&& page: widget->viewPages) {
if (page->getLastVisibleTime() > 0) {
list = g_list_insert_sorted(list, page, reinterpret_cast<GCompareFunc>(pageViewIncreasingClockTime));
}
}
int pixel = 2884560;
int firstPages = 4;
int i = 0;
auto XournalView::clearMemoryTimer(XournalView* widget) -> gboolean {
widget->cleanupBufferCache();
return true;
}
for (GList* l = g_list_last(list); l != nullptr; l = l->prev) // older (higher time) to newer (lower time)
{
if (firstPages) {
firstPages--;
} else {
auto* v = static_cast<XojPageView*>(l->data);
auto XournalView::cleanupBufferCache() -> void {
const auto& [pagesLower, pagesUpper] = this->preloadPageBounds(this->currentPage, this->viewPages.size());
g_assert(pagesLower <= pagesUpper);
if (pixel <= 0) {
v->deleteViewBuffer();
} else {
pixel -= v->getBufferPixels();
}
for (size_t i = 0; i < this->viewPages.size(); i++) {
auto&& page = this->viewPages[i];
const size_t pageNum = i + 1;
const bool isPreload = pagesLower <= pageNum && pageNum <= pagesUpper;
if (!isPreload && page->getLastVisibleTime() > 0 && page->getBufferPixels() > 0) {
page->deleteViewBuffer();
}
i++;
}
g_list_free(list);
// call again
return true;
}
auto XournalView::getCurrentPage() const -> size_t { return currentPage; }
@ -378,6 +371,19 @@ void XournalView::pageSelected(size_t page) {
control->updatePageNumbers(currentPage, pdfPage);
control->updateBackgroundSizeButton();
if (control->getSettings()->isEagerPageCleanup()) {
this->cleanupBufferCache();
}
// Load surrounding pages if they are not
const auto& [pagesLower, pagesUpper] = preloadPageBounds(page, this->viewPages.size());
g_assert(pagesLower <= pagesUpper);
for (size_t i = pagesLower; i < pagesUpper; i++) {
if (this->viewPages[i]->getBufferPixels() == 0) {
this->viewPages[i]->rerenderPage();
}
}
}
auto XournalView::getControl() -> Control* { return control; }

@ -140,12 +140,14 @@ public:
private:
void fireZoomChanged();
void addLoadPageToQue(PageRef page, int priority);
std::pair<size_t, size_t> preloadPageBounds(size_t page, size_t maxPage);
Rectangle<double>* getVisibleRect(size_t page);
static gboolean clearMemoryTimer(XournalView* widget);
void cleanupBufferCache();
static void staticLayoutPages(GtkWidget* widget, GtkAllocation* allocation, void* data);
private:

@ -487,6 +487,12 @@ void SettingsDialog::load() {
loadCheckbox("cbHidePresentationSidebar", hidePresentationSidebar);
loadCheckbox("cbHideMenubarStartup", settings->isMenubarVisible());
gtk_spin_button_set_value(GTK_SPIN_BUTTON(get("preloadPagesBefore")),
static_cast<double>(settings->getPreloadPagesBefore()));
gtk_spin_button_set_value(GTK_SPIN_BUTTON(get("preloadPagesAfter")),
static_cast<double>(settings->getPreloadPagesAfter()));
loadCheckbox("cbEagerPageCleanup", settings->isEagerPageCleanup());
enableWithCheckbox("cbAutosave", "boxAutosave");
enableWithCheckbox("cbIgnoreFirstStylusEvents", "spNumIgnoredStylusEvents");
enableWithCheckbox("cbAddVerticalSpace", "spAddVerticalSpace");
@ -706,6 +712,16 @@ void SettingsDialog::save() {
settings->setMenubarVisible(getCheckbox("cbHideMenubarStartup"));
constexpr auto spinAsUint = [&](GtkSpinButton* btn) {
int v = gtk_spin_button_get_value_as_int(btn);
return v < 0 ? 0U : static_cast<unsigned int>(v);
};
unsigned int preloadPagesBefore = spinAsUint(GTK_SPIN_BUTTON(get("preloadPagesBefore")));
unsigned int preloadPagesAfter = spinAsUint(GTK_SPIN_BUTTON(get("preloadPagesAfter")));
settings->setPreloadPagesAfter(preloadPagesAfter);
settings->setPreloadPagesBefore(preloadPagesBefore);
settings->setEagerPageCleanup(getCheckbox("cbEagerPageCleanup"));
settings->setDefaultSaveName(gtk_entry_get_text(GTK_ENTRY(get("txtDefaultSaveName"))));
// Todo(fabian): use Util::fromGFilename!
char* uri = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(get("fcAudioPath")));

@ -48,6 +48,16 @@
<property name="step-increment">1</property>
<property name="page-increment">1</property>
</object>
<object class="GtkAdjustment" id="adjustmentPreloadPagesAfter">
<property name="upper">99</property>
<property name="step-increment">1</property>
<property name="page-increment">10</property>
</object>
<object class="GtkAdjustment" id="adjustmentPreloadPagesBefore">
<property name="upper">99</property>
<property name="step-increment">1</property>
<property name="page-increment">10</property>
</object>
<object class="GtkAdjustment" id="adjustmentPressureMultiplier">
<property name="lower">0.5</property>
<property name="upper">4</property>
@ -3227,6 +3237,106 @@ This also enables touch drawing.&lt;/i&gt;</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.009999999776482582</property>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="left-padding">12</property>
<child>
<!-- n-columns=2 n-rows=3 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="column-spacing">10</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Pre-load pages before</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Pre-load pages after</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="preloadPagesBefore">
<property name="name">preloadPagesBefore</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="input-purpose">number</property>
<property name="adjustment">adjustmentPreloadPagesBefore</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="preloadPagesAfter">
<property name="name">preloadPagesAfter</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="input-purpose">number</property>
<property name="adjustment">adjustmentPreloadPagesAfter</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="cbEagerPageCleanup">
<property name="label" translatable="yes">Clear cached paged while scrolling</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
<property name="width">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Performance</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
</object>
</child>
</object>

Loading…
Cancel
Save