openhab / openhab-webui

Web UIs of openHAB
Eclipse Public License 2.0
224 stars 242 forks source link

[Main UI] Caching for API responses #793

Open dlaplexurenet opened 3 years ago

dlaplexurenet commented 3 years ago

The problem

Personally I use pretty often the "Add Items from Textual Definition". When you open this page, it's basically doing 3 XHR calls to load things, items and links. I have roughly 220 things and over 1400 items. After a restart of OpenHAB, this takes roughly 2s from things endpoint and 6s for items. After a couple of days, these waiting times go down hill (I opened 2109 in openhab-core for this).

Regardless from the root cause of this issue, I believe that the things, items and links endpoints are probably reloaded very often even if there's no need to load them from the server. Example: searching the items or display the badge with items count.

Your suggestion

Would it be possible to load things, items and links and check the last modification of these objects on the server side? So before initiating a GET, you could do a HEAD, verify last modification or etag and compare with information in localStorage of the browser. As long as no changes have been made, all operations could be done on the data in the localStorage.

Your environment

runtimeInfo:
  version: 3.0.0
  buildString: Release Build
locale: en_LU
systemInfo:
  configFolder: /etc/openhab
  userdataFolder: /var/lib/openhab
  logFolder: /var/log/openhab
  javaVersion: 11.0.8
  javaVendor: Azul Systems, Inc.
  javaVendorVersion: Zulu11.41+75-CA
  osName: Linux
  osVersion: 5.4.72-v8+
  osArchitecture: arm
  availableProcessors: 4
  freeMemory: 77327728
  totalMemory: 259522560
bindings:
  - airquality
  - astro
  - exec
  - gardena
  - homeconnect
  - hpprinter
  - hue
  - icalendar
  - irobot
  - km200
  - knx
  - logreader
  - mqtt
  - mystrom
  - netatmo
  - ntp
  - samsungtv
  - sonos
  - telegram
  - unifi
  - unifiprotect
  - weathercompany
clientInfo:
  device:
    ios: false
    android: false
    androidChrome: false
    desktop: true
    iphone: false
    ipod: false
    ipad: false
    edge: false
    ie: false
    firefox: false
    macos: true
    windows: false
    cordova: false
    phonegap: false
    electron: false
    nwjs: false
    webView: false
    webview: false
    standalone: false
    os: macos
    pixelRatio: 2
    prefersColorScheme: dark
  isSecureContext: true
  locationbarVisible: true
  menubarVisible: true
  navigator:
    cookieEnabled: true
    deviceMemory: N/A
    hardwareConcurrency: N/A
    language: en-gb
    languages:
      - en-gb
    onLine: true
    platform: MacIntel
  screen:
    width: 2048
    height: 1280
    colorDepth: 24
  support:
    touch: false
    pointerEvents: true
    observer: true
    passiveListener: true
    gestures: false
    intersectionObserver: true
  themeOptions:
    dark: dark
    filled: true
    pageTransitionAnimation: default
    bars: light
    homeNavbar: default
    homeBackground: default
    expandableCardAnimation: default
  userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15
    (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15
timestamp: 2021-01-12T22:06:06.501Z

Additional information

ghys commented 3 years ago

You raise an important issue actually; how to make sure a locally cached list could be trusted to be current; as things stand I don't have an answer (your proposal to check the Etag does make sense but it is not supported by the API).

dlaplexurenet commented 3 years ago

The advantage of adding a HEAD method to the API won't break any backward compatibility as we do not touch anything existing. Additionally, yet I think it's not too relevant in a local network, is the ability of adding gzip compression. In your requests you allow gzipped responses, but the API unfortunately doesn't reply with a compressed response. Another way for fine tuning is basically allowing http/2 in jetty (supported as of 9.3, OH3 is using 9.4 - so I guess it's just a matter of configuration). Well, actually on the UI side, there's not too much you can actually do. A complete async architecture where the backend informs you through SSE about any changes on the objects such as Items, Things, Links could trigger on the UI side a partial of full update of the local cache. This would be even more proactive than doing a HEAD before the GET.

ghys commented 3 years ago

A complete async architecture where the backend informs you through SSE about any changes on the objects such as Items, Things, Links could trigger on the UI side a partial of full update of the local cache. This would be even more proactive than doing a HEAD before the GET.

That's basically how other UIs operate - HABPanel, HABot, the former Paper UI - the problem being that some folks have very busy event buses and it would lead to problems such as increased CPU usage, reduced battery lives... So for this UI I made the choice of keeping the SSE traffic/simultaneous connections as low as possible (SSE even has strict limitations in the latter when not using HTTP/2), and close/reopen them with topic filters relevant to the current page to avoid these problems as much as possible. This as the cost of having more regular requests to ensure the data is always current (this could possibly be improved in some areas like the item picker).

The HEAD requests/ETag approach can work, but I suspect your bottleneck is in the processing rather than the downloading of the data, so if you hit the same bottleneck during the HEAD request, it's not going to help much (it'll even be worse as you'll have to wait for it to end before requesting the actual data).

6 seconds to get items is really too much, even with 1400 items, so I think bringing that number down would be the most pressing problem.

dlaplexurenet commented 3 years ago

some folks have very busy event buses

Sure, I do understand, but here we are rather talking about things, items and links and I thought rather about a notification to tell the frontend to gather new full list of items (which is useful when you have maybe more than 10 items to load after a file reload or bulk addition through text definition in the frontend) or just a notification to gather a specific item.

I do not really know where the bottleneck comes from in my case. In the browser I only see the dowloading part, and I have not sufficient knowledge of Java and its components to see/understand in Jetty/Pax, what is taking so much time to download 1MB from LAN - at least from what I can read about jsondb, it's actually loading the content into memory, so I doubt it's related to system level or I/Os (running Buster on an RPi with SSD and 4GB of memory). Here is at least some information I can provide by opening the Items list in Main UI:

Screenshot 2021-01-18 at 20 49 58
20:48:53.279 [DEBUG] [pax.web.service.spi.model.ServerModel] - Matching [/rest/items]...
20:48:53.284 [DEBUG] [pax.web.service.spi.model.ServerModel] - Path [/rest/items] matched to {pattern=/rest/.*,model=ServletModel{id=org.ops4j.pax.web.service.spi.model.ServletModel-11,name=cxf-servlet,urlPatterns=[/*],alias=null,servlet=org.apache.aries.jax.rs.whiteboard.internal.Whiteboard$1@1c991d2,initParams={hide-service-list-page=true},context=ContextModel{id=org.ops4j.pax.web.service.spi.model.ContextModel-10,name=rest,httpContext=org.ops4j.pax.web.extender.whiteboard.internal.WebApplication$1@103f320,contextParams={httpContext.shared=true, webapp.context=rest},virtualHosts={},connectors={}}}}
20:48:53.293 [DEBUG] [ice.jetty.internal.HttpServiceContext] - Handling request for [/items] using http context [org.ops4j.pax.web.extender.whiteboard.internal.WebApplication$1@103f320]
20:48:53.297 [DEBUG] [ty.internal.HttpServiceServletHandler] - handling request org.ops4j.pax.web.service.jetty.internal.HttpServiceRequestWrapper@1b2f3bc, org.ops4j.pax.web.service.jetty.internal.HttpServiceResponseWrapper@136fe13
20:48:58.772 [DEBUG] [pax.web.service.spi.model.ServerModel] - Matching [/rest/events]...
20:48:58.778 [DEBUG] [pax.web.service.spi.model.ServerModel] - Path [/rest/events] matched to {pattern=/rest/.*,model=ServletModel{id=org.ops4j.pax.web.service.spi.model.ServletModel-11,name=cxf-servlet,urlPatterns=[/*],alias=null,servlet=org.apache.aries.jax.rs.whiteboard.internal.Whiteboard$1@1c991d2,initParams={hide-service-list-page=true},context=ContextModel{id=org.ops4j.pax.web.service.spi.model.ContextModel-10,name=rest,httpContext=org.ops4j.pax.web.extender.whiteboard.internal.WebApplication$1@103f320,contextParams={httpContext.shared=true, webapp.context=rest},virtualHosts={},connectors={}}}}
20:48:58.788 [DEBUG] [ice.jetty.internal.HttpServiceContext] - Handling request for [/events] using http context [org.ops4j.pax.web.extender.whiteboard.internal.WebApplication$1@103f320]
20:48:58.794 [DEBUG] [ty.internal.HttpServiceServletHandler] - handling request org.ops4j.pax.web.service.jetty.internal.HttpServiceRequestWrapper@1a62ac5, org.ops4j.pax.web.service.jetty.internal.HttpServiceResponseWrapper@4162ef

I'll see what I can provide more ...

hubsif commented 3 years ago

I'm coming from #573: I'm thinking of following another approach (REST cache), but that approach needs two requirements to be met:

  1. is the openHAB REST API a "clean" and consistent REST API? I'm asking because I've seen many APIs that call themselves "RESTful" mainly just because they support GET and PUT of json data, but otherwise don't really follow the full concept. Additionally, I think unfortunately REST is missing some specs and some features like always responding with updated resources would be helpful here.
  2. Can it be assumed that when using mainUI that the current device/browser will be the only UI being used (and changing data) at the time?