Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

7804 dataverse page speedup #8143

Merged
merged 39 commits into from
Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d708004
A fix for one instance of lots of unnecessary queries in the header f…
landreev Sep 22, 2021
4da12f3
Merge branch 'develop' into 7804-dataverse-page-speedup
landreev Sep 29, 2021
27cd648
A flyway script that creates a custom function to replace a
landreev Sep 29, 2021
400165e
consolidatig more fixes in the same branch (#7804)
landreev Sep 29, 2021
78c2d57
multiple lookups of the root dataverse from the header fragment (#7804)
landreev Sep 30, 2021
39f32a2
more fixes and improvements finalized last week. (#7804)
landreev Oct 4, 2021
2183edf
minor/cosmetic changes (#7804)
landreev Oct 4, 2021
3966f26
(work in progress) more changes to the auth calls in permission wrapp…
landreev Oct 5, 2021
dafac80
Cleaned up the authusers class permissions in the permissionswrapper …
landreev Oct 5, 2021
66be5d3
further rearrangement of code in PermissionsWrapper caching class (#7…
landreev Oct 6, 2021
714dac8
minor cleanup (kept saying "authorized users" insted of "authenticate…
landreev Oct 6, 2021
ec40c58
minor cleanup of the branch (#7804)
landreev Oct 7, 2021
2729ab3
extra index and a doc. writeup on render logic (#7804)
landreev Oct 12, 2021
9d4a2ae
Merge branch 'develop' into 7804-dataverse-page-speedup
landreev Oct 12, 2021
0cb5bf9
Renaming the flyway script to resolve a conflict w/ the script from e…
landreev Oct 12, 2021
9bd755b
some typos etc. in the guide entry (#7804)
landreev Oct 12, 2021
8db3d87
typo (#7804)
landreev Oct 12, 2021
7cad1b3
reorganized the doc entry (#7804)
landreev Oct 12, 2021
2cb74c1
Merge branch 'develop' into 7804-dataverse-page-speedup
landreev Oct 13, 2021
5b99389
fixes a conflict from the merge with the external labels pr (#7804)
landreev Oct 13, 2021
ece3293
Update doc/sphinx-guides/source/developers/tips.rst
landreev Oct 13, 2021
e0823a0
tested the approach of switching to .isUserAllowedOn() for Authentica…
landreev Oct 14, 2021
1606296
Merge branch '7804-dataverse-page-speedup' of https://github.com/IQSS…
landreev Oct 14, 2021
6b5f92d
streamlined the AuthenticatedUsers lookups in the wrapper. (#7804)
landreev Oct 14, 2021
181cb2b
Merge branch 'develop' into 7804-dataverse-page-speedup
landreev Oct 18, 2021
e30cb07
Caching the new curation labels options (#7804)
landreev Oct 19, 2021
fdaea64
doh! - the stored procedure was not working on an empty database (#7804)
landreev Oct 20, 2021
8867dca
resolving flyway conflict with the embargo pr that got merged first
landreev Nov 1, 2021
9fea2f6
Merge branch 'develop' into 7804-dataverse-page-speedup
landreev Nov 1, 2021
a2c0753
Merge branch 'develop' into 7804-dataverse-page-speedup
landreev Nov 6, 2021
6ed3d69
artifact of resolving a minor merge conflict in SearchIncludeFragment…
landreev Nov 6, 2021
c6d7f39
cleanup of the page speedup PR from last week (#7804)
landreev Nov 8, 2021
1730161
adding release note stub
djbrooke Nov 15, 2021
0fc3d40
adding note about dev guide as well
djbrooke Nov 15, 2021
ec73dfa
renamed the flyway script (#7804)
landreev Nov 22, 2021
00fdefe
Merge branch 'develop' into 7804-dataverse-page-speedup
landreev Nov 22, 2021
1748d7f
Merge branch 'develop' into 7804-dataverse-page-speedup
landreev Dec 2, 2021
8ad042c
synced the branch up with develop; renamed the flyway script. #7804
landreev Dec 2, 2021
5c64c47
fixed a hard-coded command instance left in the code. (#7804)
landreev Dec 3, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/sphinx-guides/source/developers/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Developers have accessed the simple properties via
1. ``System.getProperty(...)`` for JVM system property settings
2. ``SettingsServiceBean.get(...)`` for database settings and
3. ``SystemConfig.xxx()`` for specially treated settings, maybe mixed from 1 and 2 and other sources.
4. ``SettingsWrapper``, reading from 2 and 3 for use in frontend pages based on JSF
4. ``SettingsWrapper`` must be used to obtain settings from 2 and 3 in frontend JSF (xhtml) pages. Please see the note on how to :ref:`avoid common efficiency issues with JSF render logic expressions <avoid-efficiency-issues-with-render-logic-expressions>`.

As of Dataverse Software 5.3, we start to streamline our efforts into using a more consistent approach, also bringing joy and
happiness to all the system administrators out there. This will be done by adopting the use of
Expand Down
37 changes: 37 additions & 0 deletions doc/sphinx-guides/source/developers/tips.rst
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,43 @@ If you already have a working dev environment with Glassfish and want to switch

- Copy the "domain1" directory from Glassfish to Payara.

UI Pages Development
--------------------

While most of the information in this guide focuses on service and backing beans ("the back end") development in Java, working on JSF/Primefaces xhtml pages presents its own unique challenges.

.. _avoid-efficiency-issues-with-render-logic-expressions:

Avoiding Inefficiencies in JSF render logic
landreev marked this conversation as resolved.
Show resolved Hide resolved
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

It is important to keep in mind that the expressions in JSF ``rendered=`` attributes may be evaluated **multiple** times. So it is crucial not to use any expressions that require database lookups, or otherwise take any appreciable amount of time and resources. Render attributes should exclusively contain calls to methods in backing beans or caching service wrappers that perform any real work on the first call only, then keep returning the cached result on all the consecutive calls. This way it is irrelevant how many times PrimeFaces may need to call the method as any effect on the performance will be negligible.

If you are ever in doubt as to how many times the method in your render logic expression is called, you can simply add a logging statement to the method in question. Or you can simply err on the side of assuming that it's going to be called a lot, and ensure that any repeated calls are not expensive to process.

A simplest, trivial example would be a direct call to a method in SystemConfig service bean. For example,

``<h:outputText rendered="#{systemConfig.advancedModeEnabled}" ...``

If this method (``public boolean isAdvancedModeEnabled()`` in ``SystemConfig.java``) consults a database setting every time it is called, this database query will be repeated every time JSF reevaluates the expression above. A lookup of a single database setting is not very expensive of course, but repeated enough times unnecessary queries do add up, especially on a busy server. So instead of SystemConfig, SettingsWrapper (a ViewScope bean) should be used to cache the result on the first call:

``<h:outputText rendered="#{settingsWrapper.advancedModeEnabled}" ...``

with the following code in ``SettingsWrapper.java``:

.. code:: java

private Boolean advancedModeEnabled = null;

public boolean isAdvancedModeEnabled() {
if (advancedModeEnabled == null) {
advancedModeEnabled = systemConfig.isAdvancedModeEnabled();
}
return advancedModeEnabled;
}

A more serious example would be direct calls to PermissionServiceBean methods used in render logic expressions. This is something that has happened and caused some problems in real life. A simple permission service lookup (for example, whether a user is authorized to create a dataset in the current dataverse) can easily take 15 database queries. Repeated multiple times, this can quickly become a measurable delay in rendering the page. PermissionsWrapper must be used exclusively for any such lookups from JSF pages.

----

Previous: :doc:`dev-environment` | Next: :doc:`troubleshooting`
12 changes: 1 addition & 11 deletions src/main/java/edu/harvard/iq/dataverse/DatasetPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,6 @@ public void setSelectedHostDataverse(Dataverse selectedHostDataverse) {

private Boolean hasTabular = false;

//External Vocabulary support
Map<Long, JsonObject> cachedCvocMap=null;

/**
* If the dataset version has at least one tabular file. The "hasTabular"
Expand Down Expand Up @@ -5532,16 +5530,8 @@ public String getLocaleDisplayName(String code) {
return displayName;
}

public Map<Long, JsonObject> getCVocConf() {
//Cache this in the view
if(cachedCvocMap==null) {
cachedCvocMap = fieldService.getCVocConf(false);
}
return cachedCvocMap;
}

public List<String> getVocabScripts() {
return fieldService.getVocabScripts(getCVocConf());
return fieldService.getVocabScripts(settingsWrapper.getCVocConf());
}

public String getFieldLanguage(String languages) {
Expand Down
55 changes: 26 additions & 29 deletions src/main/java/edu/harvard/iq/dataverse/DataverseHeaderFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.authorization.users.User;
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import static edu.harvard.iq.dataverse.util.JsfHelper.JH;

import edu.harvard.iq.dataverse.util.BundleUtil;
import edu.harvard.iq.dataverse.util.SystemConfig;
Expand Down Expand Up @@ -40,15 +39,9 @@ public class DataverseHeaderFragment implements java.io.Serializable {
@EJB
DataverseServiceBean dataverseService;

@EJB
SettingsServiceBean settingsService;

@EJB
GroupServiceBean groupService;

@EJB
PermissionServiceBean permissionService;

@EJB
SystemConfig systemConfig;

Expand All @@ -75,7 +68,7 @@ public class DataverseHeaderFragment implements java.io.Serializable {

List<Breadcrumb> breadcrumbs = new ArrayList<>();

private List<BannerMessage> bannerMessages = new ArrayList<>();
private List<BannerMessage> bannerMessages = null;

private Long unreadNotificationCount = null;

Expand Down Expand Up @@ -161,7 +154,7 @@ public Long getUnreadNotificationCount(Long userId){
this.unreadNotificationCount = userNotificationService.getUnreadNotificationCountByUser(userId);
}catch (Exception e){
logger.warning("Error trying to retrieve unread notification count for user." + e.getMessage());
this.unreadNotificationCount = new Long("0");
this.unreadNotificationCount = 0L;
}
return this.unreadNotificationCount;
}
Expand Down Expand Up @@ -261,7 +254,7 @@ public String logout() {
private Boolean signupAllowed = null;

private String redirectToRoot(){
return "dataverse.xhtml?alias=" + dataverseService.findRootDataverse().getAlias();
return "dataverse.xhtml?alias=" + settingsWrapper.getRootDataverse().getAlias();
}

public boolean isSignupAllowed() {
Expand All @@ -287,28 +280,32 @@ public boolean isRootDataverseThemeDisabled(Dataverse dataverse) {


public List<BannerMessage> getBannerMessages() {
User user = dataverseSession.getUser();
AuthenticatedUser au = null;
if (user.isAuthenticated()) {
au = (AuthenticatedUser) user;
}
if (bannerMessages == null) {
bannerMessages = new ArrayList<>();

User user = dataverseSession.getUser();
AuthenticatedUser au = null;
if (user.isAuthenticated()) {
au = (AuthenticatedUser) user;
}

if(au == null){
bannerMessages = bannerMessageService.findBannerMessages();
} else{
bannerMessages = bannerMessageService.findBannerMessages(au.getId());
}
if (au == null) {
bannerMessages = bannerMessageService.findBannerMessages();
} else {
bannerMessages = bannerMessageService.findBannerMessages(au.getId());
}

if (!dataverseSession.getDismissedMessages().isEmpty()) {
for (BannerMessage dismissed : dataverseSession.getDismissedMessages()) {
Iterator<BannerMessage> itr = bannerMessages.iterator();
while (itr.hasNext()) {
BannerMessage test = itr.next();
if (test.equals(dismissed)) {
itr.remove();
if (!dataverseSession.getDismissedMessages().isEmpty()) {
for (BannerMessage dismissed : dataverseSession.getDismissedMessages()) {
Iterator<BannerMessage> itr = bannerMessages.iterator();
while (itr.hasNext()) {
BannerMessage test = itr.next();
if (test.equals(dismissed)) {
itr.remove();
}
}
}
}
}
}
}

return bannerMessages;
Expand Down
16 changes: 9 additions & 7 deletions src/main/java/edu/harvard/iq/dataverse/DataversePage.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import edu.harvard.iq.dataverse.util.BundleUtil;
import edu.harvard.iq.dataverse.util.JsfHelper;
import static edu.harvard.iq.dataverse.util.JsfHelper.JH;
import edu.harvard.iq.dataverse.util.SystemConfig;
import java.util.List;
import javax.ejb.EJB;
import javax.faces.application.FacesMessage;
Expand Down Expand Up @@ -97,8 +96,6 @@ public enum LinkMode {
ControlledVocabularyValueServiceBean controlledVocabularyValueServiceBean;
@EJB
SavedSearchServiceBean savedSearchService;
@EJB
SystemConfig systemConfig;
@EJB DataverseRoleServiceBean dataverseRoleServiceBean;
@Inject
SearchIncludeFragment searchIncludeFragment;
Expand Down Expand Up @@ -318,7 +315,7 @@ public String init() {
dataverse = dataverseService.find(this.getId());
} else {
try {
dataverse = dataverseService.findRootDataverse();
dataverse = settingsWrapper.getRootDataverse();
} catch (EJBException e) {
// @todo handle case with no root dataverse (a fresh installation) with message about using API to create the root
dataverse = null;
Expand All @@ -330,6 +327,7 @@ public String init() {
return permissionsWrapper.notFound();
}
if (!dataverse.isReleased() && !permissionService.on(dataverse).has(Permission.ViewUnpublishedDataverse)) {
// the permission lookup above should probably be moved into the permissionsWrapper -- L.A. 5.7
return permissionsWrapper.notAuthorized();
}

Expand All @@ -340,6 +338,7 @@ public String init() {
if (dataverse.getOwner() == null) {
return permissionsWrapper.notFound();
} else if (!permissionService.on(dataverse.getOwner()).has(Permission.AddDataverse)) {
// the permission lookup above should probably be moved into the permissionsWrapper -- L.A. 5.7
return permissionsWrapper.notAuthorized();
}

Expand Down Expand Up @@ -681,7 +680,7 @@ public String save() {
if (editMode != null && editMode.equals(EditMode.FEATURED)) {
message = BundleUtil.getStringFromBundle("dataverse.feature.update");
} else {
message = (create) ? BundleUtil.getStringFromBundle("dataverse.create.success", Arrays.asList(settingsWrapper.getGuidesBaseUrl(), systemConfig.getGuidesVersion())) : BundleUtil.getStringFromBundle("dataverse.update.success");
message = (create) ? BundleUtil.getStringFromBundle("dataverse.create.success", Arrays.asList(settingsWrapper.getGuidesBaseUrl(), settingsWrapper.getGuidesVersion())) : BundleUtil.getStringFromBundle("dataverse.update.success");
}
JsfHelper.addSuccessMessage(message);

Expand Down Expand Up @@ -710,10 +709,13 @@ public String cancel() {
return "/dataverse.xhtml?alias=" + dataverse.getAlias() + "&faces-redirect=true";
}

public boolean isRootDataverse() {
return dataverse.getOwner() == null;
public boolean isRootDataverse() {
return dataverse == null ? false : dataverse.getOwner() == null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this changed? would dataverse ever be null here? (I'm not a big fan of checking for null when something should never be null and it only hides an error elsewhere in the code).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I did it for no specific reason, out of habit. I somehow still had that assumption, that it's generally a good idea always. But you are right of course; reversing.
... there is that "create" mode, where we don't have a dataverse... but I guess it is safe to say, that if the page tries to evaluate isRootDataverse() in create mode, it should be considered an error/problem that should not be hidden.

}

// Wondering what this method is for - what would be the situation, where
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@landreev at quick glance, we do have the ownerId as a parameter when you are in createMode (since that parameter tells you where to create the new dataverse)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makese sense.

// we only have the id of the parent dataverse, but it hasn't been instantiated
// yet? It doesn't appear like it's being used anywyere. -- L.A. 5.7
public Dataverse getOwner() {
return (ownerId != null) ? dataverseService.find(ownerId) : null;
}
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/GuestbookResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
*
* @author skraffmiller
*/
@NamedStoredProcedureQuery(
name = "GuestbookResponse.estimateGuestBookResponseTableSize",
procedureName = "estimateGuestBookResponseTableSize",
parameters = {
@StoredProcedureParameter(mode = ParameterMode.OUT, type = Long.class)
}
)
@Entity
@Table(indexes = {
@Index(columnList = "guestbook_id"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.StoredProcedureQuery;
import javax.persistence.TypedQuery;

/**
Expand Down Expand Up @@ -917,7 +918,34 @@ public Long getCountGuestbookResponsesByDatasetId(Long datasetId) {
}

public Long getCountOfAllGuestbookResponses() {
// dataset id is null, will return 0
// dataset id is null, will return 0

// "SELECT COUNT(*)" is notoriously expensive in PostgresQL for large
// tables. This makes this call fairly expensive for any installation
// with a lot of download/access activity. (This "total download" metrics
// is always displayed when the homepage is loaded).
// It is safe to say that no real life user will need this number to be
// precise down to the last few digits, and/or withing a second,
// especially once that number is in the millions. The
// solution implemented below relies on estimating the number. It is
// very efficient, but also very PostgresQL specific - hence it is
// defined as a custom database function.
// An alternative solution would be to get the exact number every
// hour or so, and cache it centrally for the rest of the application
// somehow. -- L.A. 5.6


try {
StoredProcedureQuery query = this.em.createNamedStoredProcedureQuery("GuestbookResponse.estimateGuestBookResponseTableSize");
query.execute();
Long totalCount = (Long) query.getOutputParameterValue(1);

if (totalCount != null) {
return totalCount;
}
} catch (IllegalArgumentException iae) {
// Don't do anything, we'll fall back to using "SELECT COUNT()"
}
Query query = em.createNativeQuery("select count(o.id) from GuestbookResponse o;");
return (Long) query.getSingleResult();
}
Expand Down
Loading