FTUI longpoll / mehrere offene websockets und doppelte Events / pagebuttons

Begonnen von fruemmel, 26 November 2019, 20:29:21

Vorheriges Thema - Nächstes Thema

fruemmel

Hallo,

ich hoffe ich bin nicht zu vorschnell, aber ich glaube eine Ursache gefunden zu haben, warum meine eher langsamen Android-Tablets in Verbindung mit Pagebuttons teilweise sehr träge sind und manche Readings nicht aktualisiert werden.

Durch remote-debugging und etliche Hilfsausgaben ist mir folgendes aufgefallen:
Es kommt insb. bei der Verwendung von Pagebuttons reproduzierbar vor, dass mehrere websocket-Verbindungen gleichzeitig offen sind. Das führt anscheinend dazu, dass alle offenen Verbindungen die Events bei Readings-Änderungen fast zeitgleich bekommen und diese mehrfach abgearbeitet werden. Ich habe teilweise 4 websocket-Verbindungen gleichzeitig. Ursache ist vermutlich die folgende:
Ich arbeite viel mit Pagebuttons. Der Wechsel auf eine Seite, die noch nicht geladen war, stoppt das longpoll und startet es wieder neu. Aufgrund von timing-Problemen durch die asynchrone Verarbeitung kommt es anscheinend dazu, dass laufende websocket-Verbindungen nicht sauber geschlossen werden und weiterlaufen. Insb. bei der Verwendung von "prefetch" bei Pagebuttons kann man den Effekt gut sehen.

Ich konnte das mit einem Eingriff in den Code zum Test abstellen:
startLongpoll: function () {
        ftui.log(2, 'startLongpoll: ' + ftui.config.doLongPoll);
        ftui.poll.long.lastEventTimestamp = new Date();
        if (ftui.config.doLongPoll) {
            ftui.config.shortpollInterval = $("meta[name='shortpoll_interval']").attr("content") || 15 * 60; // 15 minutes
            ftui.poll.long.timer = setTimeout(function () {
                ftui.longPoll();
            }, 3000); // <= hier steht im Original eine 0
        }
    },

Ich habe in der function startLongpoll (ftui-tablet-ui.cs) den Start vom longPoll um 3 Sekunden verzögert. Dadurch bleibt anscheinend genug Zeit, dass die Neustarts der Websocket-Verbindungen sauber ablaufen. Das ist sicher keine Dauerlösung, scheint mir aber meine Theorie zu belegen.

Auch ein anderer Effekt scheint dadurch vermieden zu werden: Bei mir landete das ftui öfters in folgendem Bereich der Funktion ftui.handleUpdates, obwohl die websocket-Verbindung nicht gestört war:
if (!ftui.poll.long.websocket) {
            // Ajax longpoll
            // cumulative data -> remember last line
            // restart after 9999 lines to avoid overflow
       
            ftui.poll.long.currLine = lines.length - 1;
            if (ftui.poll.long.currLine > 9999) {
                ftui.states.longPollRestart = true;
                ftui.poll.long.request.abort();
            }
           
        }


Die Zeile ... = lines.length-1 führt dazu, dass folgende Aufrufe der Funktion handleUpdates() Events teilweise nicht mehr verarbeiten, da die Schleife   for (var i = ftui.poll.long.currLine, len = lines.length; i < len; i++) { sofort verlassen wurde. Da die Events per websocket und nicht ajax kamen, wurden auch keine kumulierten Daten übergeben. Ich konnte dadurch beobachen, dass nur noch Events von devices verarbeitet wurden, bei denen viele Readings auf einmal übergeben wurden (da...currLine auf 4 stand, und somit einfache Events mit weniger Zeilen im Array "lines" ignoriert wurden).

Ich hoffe, dass mir die Spezialisten unter Euch folgen können und der Theorie etwas abgewinnen können. Ich kann die Effekte bzw. deren Behebung mit der o.g. Anpassung reproduzierbar abstellen.

Auf schnellen Geräten scheint es die Probleme nicht zu geben, das liegt wohl am besseren Timing. Langsame Geräte werden durch die mehrfache Verarbeitung der Events zusätzlich ausgebremst.

Gruß Wolfgang



amenomade

Pi 3B, Alexa, CUL868+Selbstbau 1/2λ-Dipol-Antenne, USB Optolink / Vitotronic, Debmatic und HM / HmIP Komponenten, Rademacher Duofern Jalousien, Fritz!Dect Thermostaten, Proteus

fruemmel

So, ich denke ich kann das Problem und auch eine Lösung auf den Punkt bringen. Der Haken liegt in der Funktion ftui.stopLongpoll. Diese wird aus ftui.restartLongpoll aufgerufen. Das passiert insb. beim erstmaligen Anwählen eines pagebuttons, oder beim prefetch eines pagebuttons. In beiden Fällen wird das restartLongpoll aufgrund von "initWidgetsDone" aufgerufen.

Das eigentliche Problem liegt in folgendem Abschnitt in ftui.stopLongpoll:
if (ftui.poll.long.websocket) {
            if (ftui.poll.long.websocket.readyState === WebSocket.OPEN) {
                ftui.poll.long.websocket.close();
            }
            ftui.poll.long.websocket = undefined;
            ftui.log(2, 'stopped websocket');
        }


Hiermit soll m.E. sichergestellt werden, dass eine offene Websocket-Verbindung geschlossen wird, bevor mittels restartLongpoll bzw. ftui.startLongpoll eine neue Verbindung geöffnet wird. ABER: es wird nicht berücksichtigt, dass sich gerade bei langsamen Netz oder Endgerät der Websocket noch im Status WebSocket.CONNECTING befinden kann. In diesem Fall wird die im Aufbau befindliche Verbindung nicht beendet und zusätzlich eine neue aufgebaut.
Das führt dann dazu, dass mehrere Verbindungen Events empfangen und entsprechend mehrfach abarbeiten. Als weiteres Folgeproblem kann es, wie im ersten Post beschrieben, wohl passieren, dass einzelne Readings gar nicht mehr aktualisiert werden.

Ich habe bei mir zum Test die Abfrage angepasst:if (ftui.poll.long.websocket.readyState != WebSocket.CLOSED
            && ftui.poll.long.websocket.readyState != WebSocket.CLOSING) {
Seitdem sind die parallelen Verbindungen weg. Ob man das eleganter lösen sollte, indem man eine im Aufbau befindliche Verbindung abwartet und erst schließt, wenn sie offen ist, kann ich nicht beurteilen.

Jetzt würde ich mich sehr freuen, wenn einer der FTUI-Profis diese Ausführungen beurteilt und kommentiert.

setstate

Sehr gute Analyse des Problems! Alles richtig was du schreibst.

Ich werde deine Änderung mal ausprobieren, aber kann mich noch dunkel erinnern, dass es Konstellationen gab, wo das Schließen in einem bestimmten readyState Fehler wirft und das auch noch unterschiedlich auf anderen Browsers/Devices.

fruemmel

Zitat von: setstate am 28 November 2019, 07:33:13
Ich werde deine Änderung mal ausprobieren, aber kann mich noch dunkel erinnern, dass es Konstellationen gab, wo das Schließen in einem bestimmten readyState Fehler wirft und das auch noch unterschiedlich auf anderen Browsers/Devices.
Das kann ich bestätigen, in google chrome wird beim Anzeigen der websocket-Verbindungen ein Fehler angezeigt, wenn die Verbindung im Zustand "CONNECTING" geschlossen wird. Es scheint zwar alles zu funktionieren, aber sauber ist das also nicht. Als Alternative habe ich zum Test in der Funktion stopLongpoll einen Timer eingebaut, der immer wieder mit 500ms angestoßen wird, wenn er eine Verbindung im Zustand "CONNECTING" findet. Sobald diese "OPEN" ist, wird das close abgesetzt. Das sieht im Ergebnis dann richtig aus. Aber im sauberen Einbauen von solchen Konstrukten seid Ihr mit Sicherheit besser als ich.

fruemmel

Ich habe jetzt nochmal einen vermeintlich besseren Weg eingebaut, scheint in den ersten Tests zu funktionieren:
Zitat
if (ftui.poll.long.websocket.readyState == WebSocket.CONNECTING) {
   ftui.poll.long.websocket.onopen = function (event) {
                  event.target.close();
        }
}
else if (ftui.poll.long.websocket.readyState == WebSocket.OPEN) {
....

Damit wird dafür gesorgt, dass ein WebSocket, der sich im Zustand CONNECTING befindet, erst geschlossen wird, wenn die Verbindung hergestellt wurde. Das sieht im Chrome-Debugger erstmal gut aus.
Allerdings bin ich mir nicht sicher, wie das in javascript mit den Referenzen läuft, da ja der Variablen ftui.poll.long.websocket beim restartLongpoll umgehend eine neue Websocket-Verbindung zugewiesen wird.

setstate

Der Lösungsansatz ist sehr elegant. Du hast aber recht, es könnte durch die Benutzung der globalen Variable andere Probleme auftreten.

fruemmel

Ich bin fleßig am Testen, bei mir scheint die Lösung stabil zu funktionieren (Google Chrome unter Window und Fully Browser unter Android).
Wenn man es technisch einwandfrei lösen wollte, müsste man wohl eine neue WebSocket-Verbindung immer in eine neue Variable (z. B. in einem Array) ablegen. Oder hast Du eine bessere Idee? Nach derzeitigem Stand scheint aber die Variante mit dem onOpen-Event mit minimalem Aufwand zu helfen.

fruemmel

Zitat von: setstate am 28 November 2019, 17:58:11
Der Lösungsansatz ist sehr elegant. Du hast aber recht, es könnte durch die Benutzung der globalen Variable andere Probleme auftreten.
Hallo setstate,

ich habe das jetzt noch einmal überarbeitet. Im Anhang findet sich eine diff-Datei, die als patch auf die aktuelle fhem-tablet-ui.js angewendet werden kann.
Im Kern habe ich ein Array für max. 10 Verbindungen angelegt, in dem die neuen websockets rollierend generiert werden, damit sie auch beim Anlegen eines neuen websockets noch für das nachträgliche Beenden zur Verfügung stehen. Sind ungefähr 20 Zeilen zusätzlicher Code. Die Variable ftui.poll.long.websocket verweist weiterhin auf den aktuellen Websocket. Bei mir läuft das seit einigen Tagen stabil in Chrome, Firefox, Safari und Fully Kiosk. Es wäre super, wenn Du Dir das mal ansehen und ggfls. einchecken könntest.

Ich habe mir eine etwas erweiterte Debug-Umgebung gebaut, bei der man sieht, wieviel Zeit die Funktion plugin.update pro event bzw. reading ungefähr benötigt.
Auf PC oder aktuellem iPad bewegt sich das bei ca. 50ms, auf einem etwas älteren Android mit Fully Kiosk schon bei über 200ms pro event. Da lohnt es sich schon, wenn die Events nicht mehrfach verarbeitet werden. Und natürlich lohnt sich auch das generelle Optimieren der übermittelten Events mit event-on-change o.ä.

setstate

Woher weißt du, dass parallele Verbindungen bestehen bzw. wie kann man die nachweisen?

fruemmel

Das sieht man in den Entwicklertools von Chrome ganz gut (Strg-Shift-I). Im Menü "Network" auswählen und "WS" wie websocket aktivieren. Dann sieht man dnymisch das Verhalten der Websockets. Screenshot siehe Anhang.

In der Spalte "Time" sieht man, welche websockets noch auf "pending" stehen. Diese empfangen die Events von fhem. Wenn mehrere Websockets auf "pending" stehen, werden die Events entsprechend oft empfangen und verarbeitet. Das kann man derzeit ganz gut erzeugen, wenn man mehrere pagebuttons nutzt und mit "prefetch" lädt. Auch beim Wechsel auf eine noch nicht geladene pagebutton-Seite sieht man, wie idealerweise ein Websocket geschlossen wird und ein neuer geöffnet wird. Falls in der Spalte Time eine Zeit steht, dann ist das die Zeit, die der entsprechende Websocket insgesamt geöffnet war.

Bei meinem Patch wird dafür gesorgt, dass wirklich immer nur ein Websocket aktiv (pending) ist.

setstate