[Libreoffice-commits] online.git: 19 commits - common/Rectangle.hpp loleaflet/src test/TileCacheTests.cpp wsd/ClientSession.cpp wsd/ClientSession.hpp wsd/DocumentBroker.cpp wsd/DocumentBroker.hpp wsd/protocol.txt wsd/TestStubs.cpp wsd/TileCache.cpp wsd/TileCache.hpp

Tamás Zolnai tamas.zolnai at collabora.com
Thu Jul 19 12:19:38 UTC 2018


 common/Rectangle.hpp                         |    5 
 loleaflet/src/layer/tile/CalcTileLayer.js    |   47 -----
 loleaflet/src/layer/tile/GridLayer.js        |  106 +++++++-----
 loleaflet/src/layer/tile/ImpressTileLayer.js |   39 ----
 loleaflet/src/layer/tile/TileLayer.js        |   69 +-------
 loleaflet/src/layer/tile/WriterTileLayer.js  |   41 ----
 test/TileCacheTests.cpp                      |    4 
 wsd/ClientSession.cpp                        |  227 ++++++++++++++++++++++++++-
 wsd/ClientSession.hpp                        |   55 ++++++
 wsd/DocumentBroker.cpp                       |  167 ++++++++++++++++---
 wsd/DocumentBroker.hpp                       |    1 
 wsd/TestStubs.cpp                            |    5 
 wsd/TileCache.cpp                            |   27 ++-
 wsd/TileCache.hpp                            |    7 
 wsd/protocol.txt                             |    5 
 15 files changed, 551 insertions(+), 254 deletions(-)

New commits:
commit 33a0cb1ee7f1b3e6a3e234a257a6db1848ba01bc
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Thu Jul 19 14:19:07 2018 +0200

    Fix failing unit tests after latency changes
    
    With the new code, wsd is waiting for tileprocessed messages
    if the upper limit of tiles-on-fly limit is reached. To avoid that
    send canceltiles message.
    
    Change-Id: Iadf16c834f12d14000d630078882dfa8e11a99a0

diff --git a/test/TileCacheTests.cpp b/test/TileCacheTests.cpp
index 10d8ba648..09fe0671e 100644
--- a/test/TileCacheTests.cpp
+++ b/test/TileCacheTests.cpp
@@ -247,6 +247,8 @@ void TileCacheTests::testPerformance()
             std::vector<char> tile = getResponseMessage(socket, "tile:", "tile-performance ");
             CPPUNIT_ASSERT_MESSAGE("did not receive a tile: message as expected", !tile.empty());
         }
+        /// Send canceltiles message to clear tiles-on-fly list, otherwise wsd waits for tileprocessed messages
+        sendTextFrame(socket, "canceltiles");
     }
 
     std::cerr << "Tile rendering roundtrip for 5 x 8 tiles combined: " << timestamp.elapsed() / 1000.
@@ -430,6 +432,8 @@ void TileCacheTests::testUnresponsiveClient()
             std::vector<char> tile = getResponseMessage(socket2, "tile:", "client2 ");
             CPPUNIT_ASSERT_MESSAGE("Did not receive tile #" + std::to_string(i+1) + " of 8: message as expected", !tile.empty());
         }
+        /// Send canceltiles message to clear tiles-on-fly list, otherwise wsd waits for tileprocessed messages
+        sendTextFrame(socket2, "canceltiles");
     }
 }
 
commit fe5507f134c23198eb78276836b619fa227600fa
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Tue Jul 10 14:23:20 2018 +0200

    Add some additional comment related to latency changes
    
    Change-Id: I3ece60ce8a66730a8f8a93757412bcaa2b02a77d

diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index a62f4b39d..1b21425d4 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -111,8 +111,10 @@ public:
     /// Set WOPI fileinfo object
     void setWopiFileInfo(std::unique_ptr<WopiStorage::WOPIFileInfo>& wopiFileInfo) { _wopiFileInfo = std::move(wopiFileInfo); }
 
+    /// Get requested tiles waiting for sending to the client
     boost::optional<std::list<TileDesc>>& getRequestedTiles() { return _requestedTiles; }
 
+    /// Mark a new tile as sent
     void addTileOnFly(const TileDesc& tile);
     void clearTilesOnFly();
     size_t getTilesOnFlyCount() const { return _tilesOnFly.size(); }
@@ -161,12 +163,14 @@ private:
 
     void dumpState(std::ostream& os) override;
 
+    /// Handle invalidation message comming from a kit and transfer it to a tile request.
     void handleTileInvalidation(const std::string& message,
                                 const std::shared_ptr<DocumentBroker>& docBroker);
 
     /// Clear wireId map anytime when client visible area changes (visible area, zoom, part number)
     void resetWireIdMap();
 
+    /// Generate a unique id for a tile
     std::string generateTileID(const TileDesc& tile);
 
 private:
@@ -218,11 +222,13 @@ private:
     /// Client is using a text document?
     bool _isTextDocument;
 
+    /// TileID's of the sent tiles. Push by sending and pop by tileprocessed message from the client.
     std::list<std::string> _tilesOnFly;
 
+    /// Requested tiles are stored in this list, before we can send them to the client
     boost::optional<std::list<TileDesc>> _requestedTiles;
 
-    /// Store wireID's of the sent tiles for the actual visible area
+    /// Store wireID's of the sent tiles inside the actual visible area
     std::map<std::string, TileWireId> _oldWireIds;
 };
 
commit 3215ddfe481e00cf75191841ee052a38fc30104c
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Fri Jul 6 13:48:30 2018 +0200

    Set back debugging code removed by latency related code changes
    
    Change-Id: I6634029bc8c36dfea7219e7e48e1c010b274e687

diff --git a/loleaflet/src/layer/tile/CalcTileLayer.js b/loleaflet/src/layer/tile/CalcTileLayer.js
index 10cdb155b..b616155b0 100644
--- a/loleaflet/src/layer/tile/CalcTileLayer.js
+++ b/loleaflet/src/layer/tile/CalcTileLayer.js
@@ -253,6 +253,7 @@ L.CalcTileLayer = L.TileLayer.extend({
 		var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast());
 		var visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight);
 
+		var needsNewTiles = false;
 		for (var key in this._tiles) {
 			var coords = this._tiles[key].coords;
 			var tileTopLeft = this._coordsToTwips(coords);
@@ -266,6 +267,7 @@ L.CalcTileLayer = L.TileLayer.extend({
 					this._tiles[key]._invalidCount = 1;
 				}
 				if (visibleArea.intersects(bounds)) {
+					needsNewTiles = true;
 					if (this._debug) {
 						this._debugAddInvalidationData(this._tiles[key]);
 					}
@@ -278,6 +280,11 @@ L.CalcTileLayer = L.TileLayer.extend({
 			}
 		}
 
+		if (needsNewTiles && command.part === this._selectedPart && this._debug)
+		{
+			this._debugAddInvalidationMessage(textMsg);
+		}
+
 		for (key in this._tileCache) {
 			// compute the rectangle that each tile covers in the document based
 			// on the zoom level
diff --git a/loleaflet/src/layer/tile/GridLayer.js b/loleaflet/src/layer/tile/GridLayer.js
index da532cf97..b7c451437 100644
--- a/loleaflet/src/layer/tile/GridLayer.js
+++ b/loleaflet/src/layer/tile/GridLayer.js
@@ -703,6 +703,13 @@ L.GridLayer = L.Layer.extend({
 			// Visible area is dirty, update it on the server
 			this._clientVisibleArea = newClientVisibleArea
 			this._map._socket.sendMessage(this._clientVisibleArea);
+			if (this._debug) {
+				this._debugInfo.clearLayers();
+				for (var key in this._tiles) {
+					this._tiles[key]._debugPopup = null;
+					this._tiles[key]._debugTile = null;
+				}
+			}
 		}
 	},
 
diff --git a/loleaflet/src/layer/tile/ImpressTileLayer.js b/loleaflet/src/layer/tile/ImpressTileLayer.js
index 83c78ccfb..4549a6c31 100644
--- a/loleaflet/src/layer/tile/ImpressTileLayer.js
+++ b/loleaflet/src/layer/tile/ImpressTileLayer.js
@@ -365,7 +365,7 @@ L.ImpressTileLayer = L.TileLayer.extend({
 		var visibleTopLeft = this._latLngToTwips(this._map.getBounds().getNorthWest());
 		var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast());
 		var visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight);
-
+		var needsNewTiles = false;
 		for (var key in this._tiles) {
 			var coords = this._tiles[key].coords;
 			var tileTopLeft = this._coordsToTwips(coords);
@@ -379,6 +379,7 @@ L.ImpressTileLayer = L.TileLayer.extend({
 					this._tiles[key]._invalidCount = 1;
 				}
 				if (visibleArea.intersects(bounds)) {
+					needsNewTiles = true;
 					if (this._debug) {
 						this._debugAddInvalidationData(this._tiles[key]);
 					}
@@ -391,6 +392,11 @@ L.ImpressTileLayer = L.TileLayer.extend({
 			}
 		}
 
+		if (needsNewTiles && command.part === this._selectedPart && this._debug)
+		{
+			this._debugAddInvalidationMessage(textMsg);
+		}
+
 		for (key in this._tileCache) {
 			// compute the rectangle that each tile covers in the document based
 			// on the zoom level
diff --git a/loleaflet/src/layer/tile/WriterTileLayer.js b/loleaflet/src/layer/tile/WriterTileLayer.js
index 8c9f46e81..f7bca842b 100644
--- a/loleaflet/src/layer/tile/WriterTileLayer.js
+++ b/loleaflet/src/layer/tile/WriterTileLayer.js
@@ -109,6 +109,7 @@ L.WriterTileLayer = L.TileLayer.extend({
 		var visibleTopLeft = this._latLngToTwips(this._map.getBounds().getNorthWest());
 		var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast());
 		var visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight);
+		var needsNewTiles = false;
 		for (var key in this._tiles) {
 			var coords = this._tiles[key].coords;
 			var tileTopLeft = this._coordsToTwips(coords);
@@ -122,6 +123,7 @@ L.WriterTileLayer = L.TileLayer.extend({
 					this._tiles[key]._invalidCount = 1;
 				}
 				if (visibleArea.intersects(bounds)) {
+					needsNewTiles = true;
 					if (this._debug) {
 						this._debugAddInvalidationData(this._tiles[key]);
 					}
@@ -134,6 +136,11 @@ L.WriterTileLayer = L.TileLayer.extend({
 			}
 		}
 
+		if (needsNewTiles && this._debug)
+		{
+			this._debugAddInvalidationMessage(textMsg);
+		}
+
 		for (key in this._tileCache) {
 			// compute the rectangle that each tile covers in the document based
 			// on the zoom level
commit 8d92b0809de30faeef3819022be6628f634f85f2
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Fri Jul 6 12:38:31 2018 +0200

    Store wiredIDs on the server side
    
    So we can use this information in tile requests.
    
    Change-Id: I87ba420ec0fd699353d48a228268e546ace21921

diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index 8cc1f5d94..9d8cc9e8e 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -1403,17 +1403,11 @@ L.TileLayer = L.GridLayer.extend({
 			}
 
 			ctx.putImageData(imgData, 0, 0);
-
-			tile.oldWireId = tile.wireId;
-			tile.wireId = command.wireId;
 			tile.el.src = canvas.toDataURL('image/png');
 
 			console.log('set new image');
 		}
 		else if (tile) {
-			if (command.wireId != undefined) {
-				tile.oldWireId = command.wireId;
-			}
 			if (this._tiles[key]._invalidCount > 0) {
 				this._tiles[key]._invalidCount -= 1;
 			}
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 205775b36..41fa32193 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -293,6 +293,7 @@ bool ClientSession::_handleInput(const char *buffer, int length)
         else
         {
             _clientVisibleArea = Util::Rectangle(x, y, width, height);
+            resetWireIdMap();
             return forwardToChild(std::string(buffer, length), docBroker);
         }
     }
@@ -310,6 +311,7 @@ bool ClientSession::_handleInput(const char *buffer, int length)
             else
             {
                 _clientSelectedPart = temp;
+                resetWireIdMap();
                 return forwardToChild(std::string(buffer, length), docBroker);
             }
         }
@@ -332,6 +334,7 @@ bool ClientSession::_handleInput(const char *buffer, int length)
             _tileHeightPixel = tilePixelHeight;
             _tileWidthTwips = tileTwipWidth;
             _tileHeightTwips = tileTwipHeight;
+            resetWireIdMap();
             return forwardToChild(std::string(buffer, length), docBroker);
         }
     }
@@ -941,6 +944,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
                 if(getTokenInteger(token, "current", part))
                 {
                     _clientSelectedPart = part;
+                    resetWireIdMap();
                 }
 
                 // Get document type too
@@ -1074,10 +1078,7 @@ Authorization ClientSession::getAuthorization() const
 
 void ClientSession::addTileOnFly(const TileDesc& tile)
 {
-    std::ostringstream tileID;
-    tileID << tile.getPart() << ":" << tile.getTilePosX() << ":" << tile.getTilePosY() << ":"
-           << tile.getTileWidth() << ":" << tile.getTileHeight();
-    _tilesOnFly.push_back(tileID.str());
+    _tilesOnFly.push_back(generateTileID(tile));
 }
 
 void ClientSession::clearTilesOnFly()
@@ -1202,7 +1203,13 @@ void ClientSession::handleTileInvalidation(const std::string& message,
                     j <= std::ceil(intersection._y2 / _tileHeightTwips); ++j)
                 {
                     invalidTiles.emplace_back(TileDesc(part, _tileWidthPixel, _tileHeightPixel, i * _tileWidthTwips, j * _tileHeightTwips, _tileWidthTwips, _tileHeightTwips, -1, 0, -1, false));
-                    invalidTiles.back().setOldWireId(0);
+
+                    TileWireId oldWireId = 0;
+                    auto iter = _oldWireIds.find(generateTileID(invalidTiles.back()));
+                    if(iter != _oldWireIds.end())
+                        oldWireId = iter->second;
+
+                    invalidTiles.back().setOldWireId(oldWireId);
                     invalidTiles.back().setWireId(0);
                 }
             }
@@ -1216,4 +1223,36 @@ void ClientSession::handleTileInvalidation(const std::string& message,
     }
 }
 
+void ClientSession::resetWireIdMap()
+{
+    _oldWireIds.clear();
+}
+
+void ClientSession::traceTileBySend(const TileDesc& tile)
+{
+    const std::string tileID = generateTileID(tile);
+
+    // Store wireId first
+    auto iter = _oldWireIds.find(tileID);
+    if(iter != _oldWireIds.end())
+    {
+        iter->second = tile.getWireId();
+    }
+    else
+    {
+        _oldWireIds.insert(std::pair<std::string, TileWireId>(tileID, tile.getWireId()));
+    }
+
+    // Record that the tile is sent
+    addTileOnFly(tile);
+}
+
+std::string ClientSession::generateTileID(const TileDesc& tile)
+{
+    std::ostringstream tileID;
+    tileID << tile.getPart() << ":" << tile.getTilePosX() << ":" << tile.getTilePosY() << ":"
+           << tile.getTileWidth() << ":" << tile.getTileHeight();
+    return tileID.str();
+}
+
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index 068631044..a62f4b39d 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -19,6 +19,7 @@
 #include <Rectangle.hpp>
 #include <boost/optional.hpp>
 #include <list>
+#include <map>
 
 class DocumentBroker;
 
@@ -120,7 +121,9 @@ public:
     int getTileWidthInTwips() const { return _tileWidthTwips; }
     int getTileHeightInTwips() const { return _tileHeightTwips; }
 
-
+    /// This method updates internal data related to sent tiles (wireID and tiles-on-fly)
+    /// Call this method anytime when a new tile is sent to the client
+    void traceTileBySend(const TileDesc& tile);
 private:
 
     /// SocketHandler: disconnection event.
@@ -161,6 +164,11 @@ private:
     void handleTileInvalidation(const std::string& message,
                                 const std::shared_ptr<DocumentBroker>& docBroker);
 
+    /// Clear wireId map anytime when client visible area changes (visible area, zoom, part number)
+    void resetWireIdMap();
+
+    std::string generateTileID(const TileDesc& tile);
+
 private:
     std::weak_ptr<DocumentBroker> _docBroker;
 
@@ -207,12 +215,15 @@ private:
     int _tileWidthTwips;
     int _tileHeightTwips;
 
-    // Type of the docuemnt, extracter from status message
+    /// Client is using a text document?
     bool _isTextDocument;
 
     std::list<std::string> _tilesOnFly;
 
     boost::optional<std::list<TileDesc>> _requestedTiles;
+
+    /// Store wireID's of the sent tiles for the actual visible area
+    std::map<std::string, TileWireId> _oldWireIds;
 };
 
 
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index 5d019a784..7eead269b 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -1385,7 +1385,6 @@ void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& se
         while(session->getTilesOnFlyCount() < tilesOnFlyUpperLimit && !requestedTiles.get().empty())
         {
             TileDesc& tile = *(requestedTiles.get().begin());
-            session->addTileOnFly(tile);
 
             // Satisfy as many tiles from the cache.
             std::unique_ptr<std::fstream> cachedTile = _tileCache->lookupTile(tile);
@@ -1412,6 +1411,7 @@ void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& se
                 cachedTile->read(output.data() + pos, size);
                 cachedTile->close();
 
+                session->traceTileBySend(tile);
                 session->sendBinaryFrame(output.data(), output.size());
             }
             else
diff --git a/wsd/TestStubs.cpp b/wsd/TestStubs.cpp
index 963bf9618..0a1f7524b 100644
--- a/wsd/TestStubs.cpp
+++ b/wsd/TestStubs.cpp
@@ -15,6 +15,11 @@
 
 #include "DocumentBroker.hpp"
 
+#include "ClientSession.hpp"
+
 void DocumentBroker::assertCorrectThread() const {}
 
+
+void ClientSession::traceTileBySend(const TileDesc& /*tile*/) {}
+
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/TileCache.cpp b/wsd/TileCache.cpp
index b2d8ca793..84dd5b3bd 100644
--- a/wsd/TileCache.cpp
+++ b/wsd/TileCache.cpp
@@ -189,7 +189,10 @@ void TileCache::saveTileAndNotify(const TileDesc& tile, const char *data, const
             auto& firstSubscriber = tileBeingRendered->_subscribers[0];
             std::shared_ptr<ClientSession> firstSession = firstSubscriber.lock();
             if (firstSession)
+            {
+                firstSession->traceTileBySend(tile);
                 firstSession->enqueueSendMessage(payload);
+            }
 
             if (subscriberCount > 1)
             {
@@ -209,6 +212,7 @@ void TileCache::saveTileAndNotify(const TileDesc& tile, const char *data, const
                     std::shared_ptr<ClientSession> session = subscriber.lock();
                     if (session)
                     {
+                        session->traceTileBySend(tile);
                         session->enqueueSendMessage(payload);
                     }
                 }
commit e1b22eaac325a8ee2fe822f5cb37969b80f1fd13
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Thu Jul 5 14:40:28 2018 +0200

    Calculate TilesOnFly limit based on visible area
    
    Use 10 as a minimum value.
    
    Change-Id: I9442a427fd25e1a7a32c3d1d06aa34d2c4ca2472

diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index efa7df4f5..068631044 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -116,6 +116,10 @@ public:
     void clearTilesOnFly();
     size_t getTilesOnFlyCount() const { return _tilesOnFly.size(); }
 
+    Util::Rectangle getVisibleArea() const { return _clientVisibleArea; }
+    int getTileWidthInTwips() const { return _tileWidthTwips; }
+    int getTileHeightInTwips() const { return _tileHeightTwips; }
+
 
 private:
 
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index d17aeb7cb..5d019a784 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -41,7 +41,7 @@
 #include <sys/types.h>
 #include <sys/wait.h>
 
-#define TILES_ON_FLY_UPPER_LIMIT 25
+#define TILES_ON_FLY_MIN_UPPER_LIMIT 10u
 
 using namespace LOOLProtocol;
 
@@ -1367,13 +1367,22 @@ void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& se
 {
     std::unique_lock<std::mutex> lock(_mutex);
 
+    // How many tiles we have on the visible area, set the upper limit accordingly
+    const unsigned tilesFitOnWidth = static_cast<unsigned>(std::ceil(static_cast<float>(session->getVisibleArea().getWidth()) /
+                                                                     static_cast<float>(session->getTileWidthInTwips())));
+    const unsigned tilesFitOnHeight = static_cast<unsigned>(std::ceil(static_cast<float>(session->getVisibleArea().getHeight()) /
+                                                                      static_cast<float>(session->getTileHeightInTwips())));
+    const unsigned tilesInVisArea = tilesFitOnWidth * tilesFitOnHeight;
+
+    const unsigned tilesOnFlyUpperLimit = std::max(TILES_ON_FLY_MIN_UPPER_LIMIT, tilesInVisArea);
+
     // All tiles were processed on client side what we sent last time, so we can send a new banch of tiles
     // which was invalidated / requested in the meantime
     boost::optional<std::list<TileDesc>>& requestedTiles = session->getRequestedTiles();
     if(requestedTiles != boost::none && !requestedTiles.get().empty())
     {
         std::vector<TileDesc> tilesNeedsRendering;
-        while(session->getTilesOnFlyCount() < TILES_ON_FLY_UPPER_LIMIT && !requestedTiles.get().empty())
+        while(session->getTilesOnFlyCount() < tilesOnFlyUpperLimit && !requestedTiles.get().empty())
         {
             TileDesc& tile = *(requestedTiles.get().begin());
             session->addTileOnFly(tile);
commit 2fda5f7d925c26c8ec0de672842c40be73ee3bc8
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Tue Jul 10 14:05:36 2018 +0200

    Use an upper limit for number of tiles we push to the network
    
    I used number 25 as this limit. It's an approximate value. It's enough
    to handle the first 5-10 character input without waiting for tileprocessed
    messages. After the first 5-10 characters the tileprocessed messages are
    arriving continuously (same frequency as the typing speed), so for the later
    character inputs we always have some tileprocessed messages arrived so
    we can push new tiles to the network again.
    This 25 upper limit also seems enough to send all tiles when a whole page is
    requested (e.g. zoom os scroll).
    
    We store the requested tiles in a list, used as a queue. I don't use std::queue
    because sometimes we need to do deduplication (replace older versions of the
    same tile with a newer version).
    
    Change-Id: I22ff3e35c8ded6001c7fc160abdc1f1b12ce3bae

diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index bb1213011..205775b36 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -84,8 +84,6 @@ bool ClientSession::_handleInput(const char *buffer, int length)
     const std::string firstLine = getFirstLine(buffer, length);
     const std::vector<std::string> tokens = LOOLProtocol::tokenize(firstLine.data(), firstLine.size());
 
-    checkTileRequestTimout();
-
     std::shared_ptr<DocumentBroker> docBroker = getDocumentBroker();
     if (!docBroker)
     {
@@ -350,10 +348,6 @@ bool ClientSession::_handleInput(const char *buffer, int length)
         if(iter != _tilesOnFly.end())
             _tilesOnFly.erase(iter);
 
-        if(_tilesOnFly.empty())
-        {
-            _tileCounterStartTime = boost::none;
-        }
         docBroker->sendRequestedTiles(shared_from_this());
         return true;
     }
@@ -1078,25 +1072,17 @@ Authorization ClientSession::getAuthorization() const
     return Authorization();
 }
 
-void ClientSession::setTilesOnFly(boost::optional<TileCombined> tiles)
+void ClientSession::addTileOnFly(const TileDesc& tile)
 {
+    std::ostringstream tileID;
+    tileID << tile.getPart() << ":" << tile.getTilePosX() << ":" << tile.getTilePosY() << ":"
+           << tile.getTileWidth() << ":" << tile.getTileHeight();
+    _tilesOnFly.push_back(tileID.str());
+}
 
+void ClientSession::clearTilesOnFly()
+{
     _tilesOnFly.clear();
-    if(tiles == boost::none)
-    {
-        _tileCounterStartTime = boost::none;
-    }
-    else
-    {
-        for (auto& tile : tiles.get().getTiles())
-        {
-            std::ostringstream tileID;
-            tileID << tile.getPart() << ":" << tile.getTilePosX() << ":" << tile.getTilePosY() << ":"
-                   << tile.getTileWidth() << ":" << tile.getTileHeight();
-            _tilesOnFly.push_back(tileID.str());
-        }
-        _tileCounterStartTime = std::chrono::steady_clock::now();
-    }
 }
 
 void ClientSession::onDisconnect()
@@ -1230,17 +1216,4 @@ void ClientSession::handleTileInvalidation(const std::string& message,
     }
 }
 
-void ClientSession::checkTileRequestTimout()
-{
-    if(_tileCounterStartTime != boost::none)
-    {
-        const auto duration = std::chrono::steady_clock::now() - _tileCounterStartTime.get();
-        if( std::chrono::duration_cast<std::chrono::seconds>(duration).count() > 5)
-        {
-            LOG_WRN("Tile request timeout: server waits too long for tileprocessed messages.");
-            _tileCounterStartTime = boost::none;
-        }
-    }
-}
-
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index 3e5ae2d5f..efa7df4f5 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -18,7 +18,7 @@
 #include <Poco/URI.h>
 #include <Rectangle.hpp>
 #include <boost/optional.hpp>
-#include <vector>
+#include <list>
 
 class DocumentBroker;
 
@@ -110,10 +110,11 @@ public:
     /// Set WOPI fileinfo object
     void setWopiFileInfo(std::unique_ptr<WopiStorage::WOPIFileInfo>& wopiFileInfo) { _wopiFileInfo = std::move(wopiFileInfo); }
 
-    boost::optional<TileCombined>& getRequestedTiles() { return _requestedTiles; }
+    boost::optional<std::list<TileDesc>>& getRequestedTiles() { return _requestedTiles; }
 
-    const std::vector<std::string>& getTilesOnFly() const { return _tilesOnFly; }
-    void setTilesOnFly(boost::optional<TileCombined> tiles);
+    void addTileOnFly(const TileDesc& tile);
+    void clearTilesOnFly();
+    size_t getTilesOnFlyCount() const { return _tilesOnFly.size(); }
 
 
 private:
@@ -156,8 +157,6 @@ private:
     void handleTileInvalidation(const std::string& message,
                                 const std::shared_ptr<DocumentBroker>& docBroker);
 
-    void checkTileRequestTimout();
-
 private:
     std::weak_ptr<DocumentBroker> _docBroker;
 
@@ -207,10 +206,9 @@ private:
     // Type of the docuemnt, extracter from status message
     bool _isTextDocument;
 
-    std::vector<std::string> _tilesOnFly;
-    boost::optional<std::chrono::time_point<std::chrono::steady_clock>> _tileCounterStartTime;
+    std::list<std::string> _tilesOnFly;
 
-    boost::optional<TileCombined> _requestedTiles;
+    boost::optional<std::list<TileDesc>> _requestedTiles;
 };
 
 
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index c575eba82..d17aeb7cb 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -41,6 +41,8 @@
 #include <sys/types.h>
 #include <sys/wait.h>
 
+#define TILES_ON_FLY_UPPER_LIMIT 25
+
 using namespace LOOLProtocol;
 
 using Poco::JSON::Object;
@@ -1318,17 +1320,17 @@ void DocumentBroker::handleTileCombinedRequest(TileCombined& tileCombined,
     }
 
     // Accumulate tiles
-    boost::optional<TileCombined>& requestedTiles = session->getRequestedTiles();
+    boost::optional<std::list<TileDesc>>& requestedTiles = session->getRequestedTiles();
     if(requestedTiles == boost::none)
     {
-        requestedTiles = TileCombined::create(tileCombined.getTiles());
+        requestedTiles = std::list<TileDesc>(tileCombined.getTiles().begin(), tileCombined.getTiles().end());
     }
     // Drop duplicated tiles, but use newer version number
     else
     {
         for (const auto& newTile : tileCombined.getTiles())
         {
-            const TileDesc& firstOldTile = requestedTiles.get().getTiles()[0];
+            const TileDesc& firstOldTile = *(requestedTiles.get().begin());
             if(newTile.getPart() != firstOldTile.getPart() ||
                newTile.getWidth() != firstOldTile.getWidth() ||
                newTile.getHeight() != firstOldTile.getHeight() ||
@@ -1339,7 +1341,7 @@ void DocumentBroker::handleTileCombinedRequest(TileCombined& tileCombined,
             }
 
             bool tileFound = false;
-            for (auto& oldTile : requestedTiles.get().getTiles())
+            for (auto& oldTile : requestedTiles.get())
             {
                 if(oldTile.getTilePosX() == newTile.getTilePosX() &&
                    oldTile.getTilePosY() == newTile.getTilePosY() )
@@ -1352,7 +1354,7 @@ void DocumentBroker::handleTileCombinedRequest(TileCombined& tileCombined,
                 }
             }
             if(!tileFound)
-                requestedTiles.get().getTiles().push_back(newTile);
+                requestedTiles.get().push_back(newTile);
         }
     }
 
@@ -1367,15 +1369,16 @@ void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& se
 
     // All tiles were processed on client side what we sent last time, so we can send a new banch of tiles
     // which was invalidated / requested in the meantime
-    boost::optional<TileCombined>& requestedTiles = session->getRequestedTiles();
-    if(session->getTilesOnFly().empty() && requestedTiles != boost::none && !requestedTiles.get().getTiles().empty())
+    boost::optional<std::list<TileDesc>>& requestedTiles = session->getRequestedTiles();
+    if(requestedTiles != boost::none && !requestedTiles.get().empty())
     {
-        session->setTilesOnFly(requestedTiles.get());
-
-        // Satisfy as many tiles from the cache.
         std::vector<TileDesc> tilesNeedsRendering;
-        for (auto& tile : requestedTiles.get().getTiles())
+        while(session->getTilesOnFlyCount() < TILES_ON_FLY_UPPER_LIMIT && !requestedTiles.get().empty())
         {
+            TileDesc& tile = *(requestedTiles.get().begin());
+            session->addTileOnFly(tile);
+
+            // Satisfy as many tiles from the cache.
             std::unique_ptr<std::fstream> cachedTile = _tileCache->lookupTile(tile);
             if (cachedTile)
             {
@@ -1416,6 +1419,7 @@ void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& se
                 }
                 tileCache().subscribeToTileRendering(tile, session);
             }
+            requestedTiles.get().pop_front();
         }
 
         // Send rendering request for those tiles which were not prerendered
@@ -1426,10 +1430,8 @@ void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& se
             // Forward to child to render.
             const std::string req = newTileCombined.serialize("tilecombine");
             LOG_DBG("Some of the tiles were not prerendered. Sending residual tilecombine: " << req);
-            LOG_DBG("Sending residual tilecombine: " << req);
             _childProcess->sendTextFrame(req);
         }
-        requestedTiles = boost::none;
     }
 }
 
@@ -1438,7 +1440,7 @@ void DocumentBroker::cancelTileRequests(const std::shared_ptr<ClientSession>& se
     std::unique_lock<std::mutex> lock(_mutex);
 
     // Clear tile requests
-    session->setTilesOnFly(boost::none);
+    session->clearTilesOnFly();
     session->getRequestedTiles() = boost::none;
 
     const std::string canceltiles = tileCache().cancelTiles(session);
commit 4e2b50dc0fb3af8016dd14ca90fa563e8f0146c4
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Tue Jun 19 16:41:02 2018 +0200

    Check whether tile rendering request was already sent
    
    Change-Id: Iceb559106dcd95d6ff7db67df76cdfb04f9fb7e0

diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index bbcb2b7b9..c575eba82 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -1408,6 +1408,9 @@ void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& se
                 if(tile.getVersion() == -1) // Rendering of this tile was not requested yet
                 {
                     tile.setVersion(++_tileVersion);
+                }
+                if(!tileCache().hasTileBeingRendered(tile))
+                {
                     tilesNeedsRendering.push_back(tile);
                     _debugRenderedTileCount++;
                 }
diff --git a/wsd/TileCache.cpp b/wsd/TileCache.cpp
index ef697933a..b2d8ca793 100644
--- a/wsd/TileCache.cpp
+++ b/wsd/TileCache.cpp
@@ -129,6 +129,11 @@ void TileCache::forgetTileBeingRendered(const TileDesc& tile)
     _tilesBeingRendered.erase(cachedName);
 }
 
+bool TileCache::hasTileBeingRendered(const TileDesc& tile)
+{
+    return findTileBeingRendered(tile) != nullptr;
+}
+
 std::unique_ptr<std::fstream> TileCache::lookupTile(const TileDesc& tile)
 {
     const std::string fileName = _cacheDir + "/" + cacheFileName(tile);
diff --git a/wsd/TileCache.hpp b/wsd/TileCache.hpp
index d8c48eaa4..560b66ccc 100644
--- a/wsd/TileCache.hpp
+++ b/wsd/TileCache.hpp
@@ -80,10 +80,13 @@ public:
     void saveLastModified(const Poco::Timestamp& timestamp);
 
     void forgetTileBeingRendered(const TileDesc& tile);
+    bool hasTileBeingRendered(const TileDesc& tile);
 
     void setThreadOwner(const std::thread::id &id) { _owner = id; }
     void assertCorrectThread();
 
+
+
 private:
     void invalidateTiles(int part, int x, int y, int width, int height);
 
commit 6c4e4440e89a9a4d8e2747a280c8e96c9d142069
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Tue Jun 19 16:15:37 2018 +0200

    Handle part number a bit more robust in case of Writer
    
    Change-Id: I7390f1c5f4289be67deacf3540068c040b230584

diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 89a1b8155..bb1213011 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -55,7 +55,8 @@ ClientSession::ClientSession(const std::string& id,
     _tileWidthPixel(0),
     _tileHeightPixel(0),
     _tileWidthTwips(0),
-    _tileHeightTwips(0)
+    _tileHeightTwips(0),
+    _isTextDocument(false)
 {
     assert(!creatingPngThumbnail || thumbnailFile != "");
     const size_t curConnections = ++LOOLWSD::NumConnections;
@@ -299,17 +300,20 @@ bool ClientSession::_handleInput(const char *buffer, int length)
     }
     else if (tokens[0] == "setclientpart")
     {
-        int temp;
-        if (tokens.size() != 2 ||
-            !getTokenInteger(tokens[1], "part", temp))
-        {
-            sendTextFrame("error: cmd=setclientpart kind=syntax");
-            return false;
-        }
-        else
+        if(!_isTextDocument)
         {
-            _clientSelectedPart = temp;
-            return forwardToChild(std::string(buffer, length), docBroker);
+            int temp;
+            if (tokens.size() != 2 ||
+                !getTokenInteger(tokens[1], "part", temp))
+            {
+                sendTextFrame("error: cmd=setclientpart kind=syntax");
+                return false;
+            }
+            else
+            {
+                _clientSelectedPart = temp;
+                return forwardToChild(std::string(buffer, length), docBroker);
+            }
         }
     }
     else if (tokens[0] == "clientzoom")
@@ -736,17 +740,20 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
     }
     else if (tokens[0] == "setpart:" && tokens.size() == 2)
     {
-        int setPart;
-        if(getTokenInteger(tokens[1], "part", setPart))
+        if(!_isTextDocument)
         {
-            _clientSelectedPart = setPart;
-        }
-        else if (stringToInteger(tokens[1], setPart))
-        {
-            _clientSelectedPart = setPart;
-        }
-        else
-            return false;
+            int setPart;
+            if(getTokenInteger(tokens[1], "part", setPart))
+            {
+                _clientSelectedPart = setPart;
+            }
+            else if (stringToInteger(tokens[1], setPart))
+            {
+                _clientSelectedPart = setPart;
+            }
+            else
+                return false;
+         }
     }
     else if (tokens.size() == 3 && tokens[0] == "saveas:")
     {
@@ -946,7 +953,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
                 std::string docType;
                 if(getTokenString(token, "type", docType))
                 {
-                    _docType = docType;
+                    _isTextDocument = docType.find("text") != std::string::npos;
                 }
             }
 
@@ -1180,7 +1187,7 @@ void ClientSession::handleTileInvalidation(const std::string& message,
     if(!_clientVisibleArea.hasSurface() ||
        _tileWidthPixel == 0 || _tileHeightPixel == 0 ||
        _tileWidthTwips == 0 || _tileHeightTwips == 0 ||
-       _clientSelectedPart == -1)
+       (_clientSelectedPart == -1 && !_isTextDocument))
     {
         return;
     }
@@ -1189,14 +1196,11 @@ void ClientSession::handleTileInvalidation(const std::string& message,
     int part = result.first;
     Util::Rectangle& invalidateRect = result.second;
 
-    if(_docType.find("text") != std::string::npos) // For Writer we don't have separate parts
-        part = 0;
-
-    if(part == -1) // If no part is specifed we use the part used by the client
+    if( part == -1 ) // If no part is specifed we use the part used by the client
         part = _clientSelectedPart;
 
     std::vector<TileDesc> invalidTiles;
-    if(part == _clientSelectedPart)
+    if(part == _clientSelectedPart || _isTextDocument)
     {
         Util::Rectangle intersection;
         intersection._x1 = std::max(invalidateRect._x1, _clientVisibleArea._x1);
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index e941fa842..3e5ae2d5f 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -205,7 +205,7 @@ private:
     int _tileHeightTwips;
 
     // Type of the docuemnt, extracter from status message
-    std::string _docType;
+    bool _isTextDocument;
 
     std::vector<std::string> _tilesOnFly;
     boost::optional<std::chrono::time_point<std::chrono::steady_clock>> _tileCounterStartTime;
commit 7428c46efe1dd2f948ca9754ab5dc719ff991f4b
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Tue Jul 10 14:12:11 2018 +0200

    In Impress setpart message's syntax is a bit different.
    
    Two alternative sytnax:
    "setpart part=1"
    "setpart 1"
    
    Change-Id: I42683ca46d642d56cfc3dcc52a10d69a3f00462b

diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 5ddf439c9..89a1b8155 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -738,7 +738,13 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
     {
         int setPart;
         if(getTokenInteger(tokens[1], "part", setPart))
+        {
             _clientSelectedPart = setPart;
+        }
+        else if (stringToInteger(tokens[1], setPart))
+        {
+            _clientSelectedPart = setPart;
+        }
         else
             return false;
     }
commit 1aa8f3b17bc7d6489d0c8dc45cec4acac4938871
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Fri Jun 15 16:33:28 2018 +0200

    Send new clientvisiblearea and clientzoom messages
    
    when something is actually changed.
    
    Change-Id: I56983f5700cb9cbd0b660155a4dd0a2396b22e2a

diff --git a/loleaflet/src/layer/tile/CalcTileLayer.js b/loleaflet/src/layer/tile/CalcTileLayer.js
index 3e0bf2dfa..10cdb155b 100644
--- a/loleaflet/src/layer/tile/CalcTileLayer.js
+++ b/loleaflet/src/layer/tile/CalcTileLayer.js
@@ -315,11 +315,7 @@ L.CalcTileLayer = L.TileLayer.extend({
 	},
 
 	_onZoomRowColumns: function () {
-		this._updateClientZoom();
-		if (this._clientZoom) {
-			this._map._socket.sendMessage('clientzoom ' + this._clientZoom);
-			this._clientZoom = null;
-		}
+		this._sendClientZoom();
 		// TODO: test it!
 		this._map.fire('updaterowcolumnheaders');
 	},
diff --git a/loleaflet/src/layer/tile/GridLayer.js b/loleaflet/src/layer/tile/GridLayer.js
index f4f2f05f1..da532cf97 100644
--- a/loleaflet/src/layer/tile/GridLayer.js
+++ b/loleaflet/src/layer/tile/GridLayer.js
@@ -24,6 +24,9 @@ L.GridLayer = L.Layer.extend({
 
 	initialize: function (options) {
 		L.setOptions(this, options);
+
+		this._clientZoom = '';
+		this._clientVisibleArea = '';
 	},
 
 	onAdd: function () {
@@ -556,10 +559,8 @@ L.GridLayer = L.Layer.extend({
 			this._level.el.appendChild(fragment);
 		}
 
-		this._invalidateClientVisibleArea();
 		this._sendClientVisibleArea();
 
-		this._updateClientZoom();
 		this._sendClientZoom();
 	},
 
@@ -690,6 +691,34 @@ L.GridLayer = L.Layer.extend({
 		}
 	},
 
+	_sendClientVisibleArea: function () {
+		var visibleTopLeft = this._latLngToTwips(this._map.getBounds().getNorthWest());
+		var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast());
+		var visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight);
+		var size = new L.Point(visibleArea.getSize().x, visibleArea.getSize().y);
+		var newClientVisibleArea = 'clientvisiblearea x=' + Math.round(visibleTopLeft.x) + ' y=' + Math.round(visibleTopLeft.y) +
+			' width=' + Math.round(size.x) + ' height=' + Math.round(size.y);
+
+		if (this._clientVisibleArea !== newClientVisibleArea) {
+			// Visible area is dirty, update it on the server
+			this._clientVisibleArea = newClientVisibleArea
+			this._map._socket.sendMessage(this._clientVisibleArea);
+		}
+	},
+
+	_sendClientZoom: function () {
+		var newClientZoom = 'tilepixelwidth=' + this._tileWidthPx + ' ' +
+			'tilepixelheight=' + this._tileHeightPx + ' ' +
+			'tiletwipwidth=' + this._tileWidthTwips + ' ' +
+			'tiletwipheight=' + this._tileHeightTwips;
+
+		if (this._clientZoom !== newClientZoom) {
+			// the zoom level has changed
+			this._clientZoom = newClientZoom;
+			this._map._socket.sendMessage('clientzoom ' + this._clientZoom);
+		}
+	},
+
 	_cancelTiles: function() {
 		this._map._socket.sendMessage('canceltiles');
 		for (var key in this._tiles) {
@@ -730,39 +759,6 @@ L.GridLayer = L.Layer.extend({
 		this._emptyTilesCount = 0;
 	},
 
-	_invalidateClientVisibleArea: function() {
-		if (this._debug) {
-			this._debugInfo.clearLayers();
-			for (var key in this._tiles) {
-				this._tiles[key]._debugPopup = null;
-				this._tiles[key]._debugTile = null;
-			}
-		}
-		this._clientVisibleArea = true;
-	},
-
-	_sendClientVisibleArea: function () {
-		if (this._clientVisibleArea) {
-			// Visible area is dirty, update it on the server.
-			var visibleTopLeft = this._latLngToTwips(this._map.getBounds().getNorthWest());
-			var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast());
-			var visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight);
-			var size = new L.Point(visibleArea.getSize().x, visibleArea.getSize().y);
-			var payload = 'clientvisiblearea x=' + Math.round(visibleTopLeft.x) + ' y=' + Math.round(visibleTopLeft.y) +
-				' width=' + Math.round(size.x) + ' height=' + Math.round(size.y);
-			this._map._socket.sendMessage(payload);
-			this._clientVisibleArea = false;
-		}
-	},
-
-	_sendClientZoom: function () {
-		if (this._clientZoom) {
-			// the zoom level has changed
-			this._map._socket.sendMessage('clientzoom ' + this._clientZoom);
-			this._clientZoom = null;
-		}
-	},
-
 	_isValidTile: function (coords) {
 		if (coords.x < 0 || coords.y < 0) {
 			return false;
diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index c31b89ceb..8cc1f5d94 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -149,15 +149,11 @@ L.TileLayer = L.GridLayer.extend({
 		this._msgQueue = [];
 		this._toolbarCommandValues = {};
 		this._previewInvalidations = [];
-		this._updateClientZoom();
 
 		this._followThis = -1;
 		this._editorId = -1;
 		this._followUser = false;
 		this._followEditor = false;
-
-		// Mark visible area as dirty by default.
-		this._invalidateClientVisibleArea();
 	},
 
 	onAdd: function (map) {
@@ -259,9 +255,7 @@ L.TileLayer = L.GridLayer.extend({
 		if (this._docType === 'spreadsheet') {
 			map.on('zoomend', this._onCellCursorShift, this);
 		}
-		map.on('zoomend', this._updateClientZoom, this);
 		map.on('zoomend', L.bind(this.eachView, this, this._viewCursors, this._onUpdateViewCursor, this, false));
-		map.on('resize zoomend', this._invalidateClientVisibleArea, this);
 		map.on('dragstart', this._onDragStart, this);
 		map.on('requestloksession', this._onRequestLOKSession, this);
 		map.on('error', this._mapOnError, this);
@@ -492,7 +486,6 @@ L.TileLayer = L.GridLayer.extend({
 	},
 
 	toggleTileDebugMode: function() {
-		this._invalidateClientVisibleArea();
 		this._debug = !this._debug;
 		if (!this._debug) {
 			this._map.removeLayer(this._debugInfo);
@@ -2270,13 +2263,6 @@ L.TileLayer = L.GridLayer.extend({
 		this._previewInvalidations = [];
 	},
 
-	_updateClientZoom: function () {
-		this._clientZoom = 'tilepixelwidth=' + this._tileWidthPx + ' ' +
-			'tilepixelheight=' + this._tileHeightPx + ' ' +
-			'tiletwipwidth=' + this._tileWidthTwips + ' ' +
-			'tiletwipheight=' + this._tileHeightTwips;
-	},
-
 	_debugGetTimeArray: function() {
 		return {count: 0, ms: 0, best: Number.MAX_SAFE_INTEGER, worst: 0, date: 0};
 	},
commit 29df46219c1fcabc2dad74699127a8bf57acb8a2
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Sat Jun 16 14:22:01 2018 +0200

    Need to extract the initial part id from status message
    
    Change-Id: Ia0651d93fedb71d3ca1e24d0356ac179e95e907e

diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 5197ec39f..5ddf439c9 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -927,11 +927,21 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
             setViewLoaded();
             docBroker->setLoaded();
 
-            const std::string stringMsg(buffer, length);
-            const size_t index = stringMsg.find("type=") + 5;
-            if (index != std::string::npos)
+            for(auto &token : tokens)
             {
-                _docType = stringMsg.substr(index, stringMsg.find_first_of(' ', index) - index);
+                // Need to get the initial part id from status message
+                int part = -1;
+                if(getTokenInteger(token, "current", part))
+                {
+                    _clientSelectedPart = part;
+                }
+
+                // Get document type too
+                std::string docType;
+                if(getTokenString(token, "type", docType))
+                {
+                    _docType = docType;
+                }
             }
 
             // Forward the status response to the client.
commit 85f96bc281f037087fb226b018c40b8410d353d5
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Wed Jun 13 15:04:09 2018 +0200

    Store sent tiles's id instead of using a simple counter
    
    Change-Id: I8cbf84923a53fb6b294bd4039eb7382326f8c445

diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index d8c0de348..c31b89ceb 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -1435,7 +1435,8 @@ L.TileLayer = L.GridLayer.extend({
 		L.Log.log(textMsg, L.INCOMING, key);
 
 		// Send acknowledgment, that the tile message arrived
-		this._map._socket.sendMessage('tileprocessed tile= ' + key);
+		var tileID = command.part + ':' + command.x + ':' + command.y + ':' + command.tileWidth + ':' + command.tileHeight;
+		this._map._socket.sendMessage('tileprocessed tile=' + tileID);
 	},
 
 	_tileOnLoad: function (done, tile) {
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 5ed9e931f..5197ec39f 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -12,6 +12,7 @@
 #include "ClientSession.hpp"
 
 #include <fstream>
+#include <sstream>
 
 #include <Poco/File.h>
 #include <Poco/Net/HTTPResponse.h>
@@ -54,8 +55,7 @@ ClientSession::ClientSession(const std::string& id,
     _tileWidthPixel(0),
     _tileHeightPixel(0),
     _tileWidthTwips(0),
-    _tileHeightTwips(0),
-    _tilesOnFly(0)
+    _tileHeightTwips(0)
 {
     assert(!creatingPngThumbnail || thumbnailFile != "");
     const size_t curConnections = ++LOOLWSD::NumConnections;
@@ -335,13 +335,20 @@ bool ClientSession::_handleInput(const char *buffer, int length)
     }
     else if (tokens[0] == "tileprocessed")
     {
-        if(_tilesOnFly > 0) // canceltiles message can zero this value
+        std::string tileID;
+        if (tokens.size() != 2 ||
+            !getTokenString(tokens[1], "tile", tileID))
         {
-            --_tilesOnFly;
-            if(_tilesOnFly == 0)
-            {
-                _tileCounterStartTime = boost::none;
-            }
+            sendTextFrame("error: cmd=tileprocessed kind=syntax");
+            return false;
+        }
+        auto iter = std::find(_tilesOnFly.begin(), _tilesOnFly.end(), tileID);
+        if(iter != _tilesOnFly.end())
+            _tilesOnFly.erase(iter);
+
+        if(_tilesOnFly.empty())
+        {
+            _tileCounterStartTime = boost::none;
         }
         docBroker->sendRequestedTiles(shared_from_this());
         return true;
@@ -1048,15 +1055,23 @@ Authorization ClientSession::getAuthorization() const
     return Authorization();
 }
 
-void ClientSession::setTilesOnFly(int tilesOnFly)
+void ClientSession::setTilesOnFly(boost::optional<TileCombined> tiles)
 {
-    _tilesOnFly = tilesOnFly;
-    if(tilesOnFly == 0)
+
+    _tilesOnFly.clear();
+    if(tiles == boost::none)
     {
         _tileCounterStartTime = boost::none;
     }
     else
     {
+        for (auto& tile : tiles.get().getTiles())
+        {
+            std::ostringstream tileID;
+            tileID << tile.getPart() << ":" << tile.getTilePosX() << ":" << tile.getTilePosY() << ":"
+                   << tile.getTileWidth() << ":" << tile.getTileHeight();
+            _tilesOnFly.push_back(tileID.str());
+        }
         _tileCounterStartTime = std::chrono::steady_clock::now();
     }
 }
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index 366cb653a..e941fa842 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -18,6 +18,7 @@
 #include <Poco/URI.h>
 #include <Rectangle.hpp>
 #include <boost/optional.hpp>
+#include <vector>
 
 class DocumentBroker;
 
@@ -111,8 +112,8 @@ public:
 
     boost::optional<TileCombined>& getRequestedTiles() { return _requestedTiles; }
 
-    int getTilesOnFly() const { return _tilesOnFly; }
-    void setTilesOnFly(int tilesOnFly);
+    const std::vector<std::string>& getTilesOnFly() const { return _tilesOnFly; }
+    void setTilesOnFly(boost::optional<TileCombined> tiles);
 
 
 private:
@@ -206,7 +207,7 @@ private:
     // Type of the docuemnt, extracter from status message
     std::string _docType;
 
-    int _tilesOnFly;
+    std::vector<std::string> _tilesOnFly;
     boost::optional<std::chrono::time_point<std::chrono::steady_clock>> _tileCounterStartTime;
 
     boost::optional<TileCombined> _requestedTiles;
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index 71f7bea37..bbcb2b7b9 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -1363,15 +1363,14 @@ void DocumentBroker::handleTileCombinedRequest(TileCombined& tileCombined,
 
 void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& session)
 {
-    assert(session->getTilesOnFly() >= 0);
     std::unique_lock<std::mutex> lock(_mutex);
 
     // All tiles were processed on client side what we sent last time, so we can send a new banch of tiles
     // which was invalidated / requested in the meantime
     boost::optional<TileCombined>& requestedTiles = session->getRequestedTiles();
-    if(session->getTilesOnFly() == 0 && requestedTiles != boost::none && !requestedTiles.get().getTiles().empty())
+    if(session->getTilesOnFly().empty() && requestedTiles != boost::none && !requestedTiles.get().getTiles().empty())
     {
-        session->setTilesOnFly(requestedTiles.get().getTiles().size());
+        session->setTilesOnFly(requestedTiles.get());
 
         // Satisfy as many tiles from the cache.
         std::vector<TileDesc> tilesNeedsRendering;
@@ -1436,7 +1435,7 @@ void DocumentBroker::cancelTileRequests(const std::shared_ptr<ClientSession>& se
     std::unique_lock<std::mutex> lock(_mutex);
 
     // Clear tile requests
-    session->setTilesOnFly(0);
+    session->setTilesOnFly(boost::none);
     session->getRequestedTiles() = boost::none;
 
     const std::string canceltiles = tileCache().cancelTiles(session);
diff --git a/wsd/protocol.txt b/wsd/protocol.txt
index c7a126670..7c8d7e6a2 100644
--- a/wsd/protocol.txt
+++ b/wsd/protocol.txt
@@ -34,7 +34,7 @@ canceltiles
 tileprocessed tile=<tileid>
 
     Previously sent tile (server -> client) arrived and processed by the client.
-    Tileid has the next stucture : <tile x coord>:<tile y coord>:<zoom level>:<selected part>
+    Tileid has the next stucture : <selected part>:<tile x coord>:<tile y coord>:<tile width in twips>:<tile height in twips>
 
 downloadas name=<fileName> id=<id> format=<document format> options=<SkipImages, etc>
 
commit 464dd72e1c3471a7e22b5d9cb4c7437e407fa4ba
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Tue Jun 12 14:51:38 2018 +0200

    We might need to rerequest tile rendering when we are at sending them
    
    Change-Id: I0551e51c5f5023931dad13435b4ac3517fc48931

diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index 1478af959..71f7bea37 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -1306,7 +1306,7 @@ void DocumentBroker::handleTileCombinedRequest(TileCombined& tileCombined,
         }
     }
 
-    // Send rendering request
+    // Send rendering request, prerender before we actually send the tiles
     if (!tilesNeedsRendering.empty())
     {
         TileCombined newTileCombined = TileCombined::create(tilesNeedsRendering);
@@ -1374,6 +1374,7 @@ void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& se
         session->setTilesOnFly(requestedTiles.get().getTiles().size());
 
         // Satisfy as many tiles from the cache.
+        std::vector<TileDesc> tilesNeedsRendering;
         for (auto& tile : requestedTiles.get().getTiles())
         {
             std::unique_ptr<std::fstream> cachedTile = _tileCache->lookupTile(tile);
@@ -1404,10 +1405,28 @@ void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& se
             }
             else
             {
-                // Not cached, needs rendering. Rendering request was already sent
+                // Not cached, needs rendering.
+                if(tile.getVersion() == -1) // Rendering of this tile was not requested yet
+                {
+                    tile.setVersion(++_tileVersion);
+                    tilesNeedsRendering.push_back(tile);
+                    _debugRenderedTileCount++;
+                }
                 tileCache().subscribeToTileRendering(tile, session);
             }
         }
+
+        // Send rendering request for those tiles which were not prerendered
+        if (!tilesNeedsRendering.empty())
+        {
+            TileCombined newTileCombined = TileCombined::create(tilesNeedsRendering);
+
+            // Forward to child to render.
+            const std::string req = newTileCombined.serialize("tilecombine");
+            LOG_DBG("Some of the tiles were not prerendered. Sending residual tilecombine: " << req);
+            LOG_DBG("Sending residual tilecombine: " << req);
+            _childProcess->sendTextFrame(req);
+        }
         requestedTiles = boost::none;
     }
 }
commit 30f4cafd3758c3003cbcd44f5923336aa6be0af4
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Mon Jun 11 16:26:09 2018 +0200

    Reduce code deduplication
    
    We can request tilecombine even if client needs actually one tile only.
    
    Change-Id: Id897f219885be4cb93635d727d4ee871a4b55cb7

diff --git a/loleaflet/src/layer/tile/GridLayer.js b/loleaflet/src/layer/tile/GridLayer.js
index 2489a704c..f4f2f05f1 100644
--- a/loleaflet/src/layer/tile/GridLayer.js
+++ b/loleaflet/src/layer/tile/GridLayer.js
@@ -971,53 +971,33 @@ L.GridLayer = L.Layer.extend({
 		var twips, msg;
 		for (var r = 0; r < rectangles.length; ++r) {
 			rectQueue = rectangles[r];
-
-			if (rectQueue.length === 1) {
-				// only one tile here
-				coords = rectQueue[0];
-				key = this._tileCoordsToKey(coords);
-
+			var tilePositionsX = '';
+			var tilePositionsY = '';
+			for (i = 0; i < rectQueue.length; i++) {
+				coords = rectQueue[i];
 				twips = this._coordsToTwips(coords);
-				msg = 'tile ' +
-					'part=' + coords.part + ' ' +
-					'width=' + this._tileWidthPx + ' ' +
-					'height=' + this._tileHeightPx + ' ' +
-					'tileposx=' + twips.x + ' '	+
-					'tileposy=' + twips.y + ' ' +
-					'tilewidth=' + this._tileWidthTwips + ' ' +
-					'tileheight=' + this._tileHeightTwips;
-				this._map._socket.sendMessage(msg, key);
-			}
-			else {
-				// more tiles, use tilecombine
-				var tilePositionsX = '';
-				var tilePositionsY = '';
-				for (i = 0; i < rectQueue.length; i++) {
-					coords = rectQueue[i];
-					twips = this._coordsToTwips(coords);
-
-					if (tilePositionsX !== '') {
-						tilePositionsX += ',';
-					}
-					tilePositionsX += twips.x;
 
-					if (tilePositionsY !== '') {
-						tilePositionsY += ',';
-					}
-					tilePositionsY += twips.y;
+				if (tilePositionsX !== '') {
+					tilePositionsX += ',';
 				}
+				tilePositionsX += twips.x;
 
-				twips = this._coordsToTwips(coords);
-				msg = 'tilecombine ' +
-					'part=' + coords.part + ' ' +
-					'width=' + this._tileWidthPx + ' ' +
-					'height=' + this._tileHeightPx + ' ' +
-					'tileposx=' + tilePositionsX + ' '	+
-					'tileposy=' + tilePositionsY + ' ' +
-					'tilewidth=' + this._tileWidthTwips + ' ' +
-					'tileheight=' + this._tileHeightTwips;
-				this._map._socket.sendMessage(msg, '');
+				if (tilePositionsY !== '') {
+					tilePositionsY += ',';
+				}
+				tilePositionsY += twips.y;
 			}
+
+			twips = this._coordsToTwips(coords);
+			msg = 'tilecombine ' +
+				'part=' + coords.part + ' ' +
+				'width=' + this._tileWidthPx + ' ' +
+				'height=' + this._tileHeightPx + ' ' +
+				'tileposx=' + tilePositionsX + ' '	+
+				'tileposy=' + tilePositionsY + ' ' +
+				'tilewidth=' + this._tileWidthTwips + ' ' +
+				'tileheight=' + this._tileHeightTwips;
+			this._map._socket.sendMessage(msg, '');
 		}
 	},
 
commit 1f2982cdc53236b0312438a1bd812d8d203ac4fb
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Sat Jun 9 21:33:44 2018 +0200

    Send the right visible area to the server
    
    Change-Id: I036dfaa566fa7d4e370386d839bd2397cbf929f8

diff --git a/loleaflet/src/layer/tile/GridLayer.js b/loleaflet/src/layer/tile/GridLayer.js
index 11fbebed5..2489a704c 100644
--- a/loleaflet/src/layer/tile/GridLayer.js
+++ b/loleaflet/src/layer/tile/GridLayer.js
@@ -555,6 +555,12 @@ L.GridLayer = L.Layer.extend({
 			this._addTiles(queue, fragment);
 			this._level.el.appendChild(fragment);
 		}
+
+		this._invalidateClientVisibleArea();
+		this._sendClientVisibleArea();
+
+		this._updateClientZoom();
+		this._sendClientZoom();
 	},
 
 	_updateOnChangePart: function () {
@@ -724,6 +730,39 @@ L.GridLayer = L.Layer.extend({
 		this._emptyTilesCount = 0;
 	},
 
+	_invalidateClientVisibleArea: function() {
+		if (this._debug) {
+			this._debugInfo.clearLayers();
+			for (var key in this._tiles) {
+				this._tiles[key]._debugPopup = null;
+				this._tiles[key]._debugTile = null;
+			}
+		}
+		this._clientVisibleArea = true;
+	},
+
+	_sendClientVisibleArea: function () {
+		if (this._clientVisibleArea) {
+			// Visible area is dirty, update it on the server.
+			var visibleTopLeft = this._latLngToTwips(this._map.getBounds().getNorthWest());
+			var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast());
+			var visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight);
+			var size = new L.Point(visibleArea.getSize().x, visibleArea.getSize().y);
+			var payload = 'clientvisiblearea x=' + Math.round(visibleTopLeft.x) + ' y=' + Math.round(visibleTopLeft.y) +
+				' width=' + Math.round(size.x) + ' height=' + Math.round(size.y);
+			this._map._socket.sendMessage(payload);
+			this._clientVisibleArea = false;
+		}
+	},
+
+	_sendClientZoom: function () {
+		if (this._clientZoom) {
+			// the zoom level has changed
+			this._map._socket.sendMessage('clientzoom ' + this._clientZoom);
+			this._clientZoom = null;
+		}
+	},
+
 	_isValidTile: function (coords) {
 		if (coords.x < 0 || coords.y < 0) {
 			return false;
diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index 96a1299ff..d8c0de348 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -149,10 +149,7 @@ L.TileLayer = L.GridLayer.extend({
 		this._msgQueue = [];
 		this._toolbarCommandValues = {};
 		this._previewInvalidations = [];
-		this._clientZoom = 'tilepixelwidth=' + this._tileWidthPx + ' ' +
-			'tilepixelheight=' + this._tileHeightPx + ' ' +
-			'tiletwipwidth=' + this.options.tileWidthTwips + ' ' +
-			'tiletwipheight=' + this.options.tileHeightTwips;
+		this._updateClientZoom();
 
 		this._followThis = -1;
 		this._editorId = -1;
@@ -1479,22 +1476,10 @@ L.TileLayer = L.GridLayer.extend({
 	},
 
 	_postMouseEvent: function(type, x, y, count, buttons, modifier) {
-		if (this._clientZoom) {
-			// the zoom level has changed
-			this._map._socket.sendMessage('clientzoom ' + this._clientZoom);
-			this._clientZoom = null;
-		}
 
-		if (this._clientVisibleArea) {
-			// Visible area is dirty, update it on the server.
-			var visibleArea = this._map._container.getBoundingClientRect();
-			var pos = this._pixelsToTwips(new L.Point(visibleArea.left, visibleArea.top));
-			var size = this._pixelsToTwips(new L.Point(visibleArea.width, visibleArea.height));
-			var payload = 'clientvisiblearea x=' + Math.round(pos.x) + ' y=' + Math.round(pos.y) +
-				' width=' + Math.round(size.x) + ' height=' + Math.round(size.y);
-			this._map._socket.sendMessage(payload);
-			this._clientVisibleArea = false;
-		}
+		this._sendClientZoom();
+
+		this._sendClientVisibleArea();
 
 		this._map._socket.sendMessage('mouse type=' + type +
 				' x=' + x + ' y=' + y + ' count=' + count +
@@ -1521,21 +1506,11 @@ L.TileLayer = L.GridLayer.extend({
 				this._cellCursorOnPgDn = new L.LatLngBounds(this._prevCellCursor.getSouthWest(), this._prevCellCursor.getNorthEast());
 			}
 		}
-		if (this._clientZoom) {
-			// the zoom level has changed
-			this._map._socket.sendMessage('clientzoom ' + this._clientZoom);
-			this._clientZoom = null;
-		}
-		if (this._clientVisibleArea) {
-			// Visible area is dirty, update it on the server.
-			var visibleArea = this._map._container.getBoundingClientRect();
-			var pos = this._pixelsToTwips(new L.Point(visibleArea.left, visibleArea.top));
-			var size = this._pixelsToTwips(new L.Point(visibleArea.width, visibleArea.height));
-			var payload = 'clientvisiblearea x=' + Math.round(pos.x) + ' y=' + Math.round(pos.y) +
-				' width=' + Math.round(size.x) + ' height=' + Math.round(size.y);
-			this._map._socket.sendMessage(payload);
-			this._clientVisibleArea = false;
-		}
+
+		this._sendClientZoom();
+
+		this._sendClientVisibleArea();
+
 		this._map._socket.sendMessage('key type=' + type +
 				' char=' + charcode + ' key=' + keycode);
 	},
@@ -2301,17 +2276,6 @@ L.TileLayer = L.GridLayer.extend({
 			'tiletwipheight=' + this._tileHeightTwips;
 	},
 
-	_invalidateClientVisibleArea: function() {
-		if (this._debug) {
-			this._debugInfo.clearLayers();
-			for (var key in this._tiles) {
-				this._tiles[key]._debugPopup = null;
-				this._tiles[key]._debugTile = null;
-			}
-		}
-		this._clientVisibleArea = true;
-	},
-
 	_debugGetTimeArray: function() {
 		return {count: 0, ms: 0, best: Number.MAX_SAFE_INTEGER, worst: 0, date: 0};
 	},
commit a9c5ea9022e78969118f23d5784a04df3f3b1e36
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Tue Jul 10 14:10:28 2018 +0200

    Add a timeout for tileprocessed message handling
    
    For debug purposes.
    
    Change-Id: Icc9dfc05b18f9da96b29b7cadeb57f7218832295

diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index d975c7200..5ed9e931f 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -83,6 +83,8 @@ bool ClientSession::_handleInput(const char *buffer, int length)
     const std::string firstLine = getFirstLine(buffer, length);
     const std::vector<std::string> tokens = LOOLProtocol::tokenize(firstLine.data(), firstLine.size());
 
+    checkTileRequestTimout();
+
     std::shared_ptr<DocumentBroker> docBroker = getDocumentBroker();
     if (!docBroker)
     {
@@ -334,7 +336,13 @@ bool ClientSession::_handleInput(const char *buffer, int length)
     else if (tokens[0] == "tileprocessed")
     {
         if(_tilesOnFly > 0) // canceltiles message can zero this value
+        {
             --_tilesOnFly;
+            if(_tilesOnFly == 0)
+            {
+                _tileCounterStartTime = boost::none;
+            }
+        }
         docBroker->sendRequestedTiles(shared_from_this());
         return true;
     }
@@ -1040,6 +1048,19 @@ Authorization ClientSession::getAuthorization() const
     return Authorization();
 }
 
+void ClientSession::setTilesOnFly(int tilesOnFly)
+{
+    _tilesOnFly = tilesOnFly;
+    if(tilesOnFly == 0)
+    {
+        _tileCounterStartTime = boost::none;
+    }
+    else
+    {
+        _tileCounterStartTime = std::chrono::steady_clock::now();
+    }
+}
+
 void ClientSession::onDisconnect()
 {
     LOG_INF(getName() << " Disconnected, current number of connections: " << LOOLWSD::NumConnections);
@@ -1174,4 +1195,17 @@ void ClientSession::handleTileInvalidation(const std::string& message,
     }
 }
 
+void ClientSession::checkTileRequestTimout()
+{
+    if(_tileCounterStartTime != boost::none)
+    {
+        const auto duration = std::chrono::steady_clock::now() - _tileCounterStartTime.get();
+        if( std::chrono::duration_cast<std::chrono::seconds>(duration).count() > 5)
+        {
+            LOG_WRN("Tile request timeout: server waits too long for tileprocessed messages.");
+            _tileCounterStartTime = boost::none;
+        }
+    }
+}
+
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index 53e5201b5..366cb653a 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -112,7 +112,7 @@ public:
     boost::optional<TileCombined>& getRequestedTiles() { return _requestedTiles; }
 
     int getTilesOnFly() const { return _tilesOnFly; }
-    void setTilesOnFly(int tilesOnFly) { _tilesOnFly = tilesOnFly; }
+    void setTilesOnFly(int tilesOnFly);
 
 
 private:
@@ -155,6 +155,8 @@ private:
     void handleTileInvalidation(const std::string& message,
                                 const std::shared_ptr<DocumentBroker>& docBroker);
 
+    void checkTileRequestTimout();
+
 private:
     std::weak_ptr<DocumentBroker> _docBroker;
 
@@ -205,6 +207,7 @@ private:
     std::string _docType;
 
     int _tilesOnFly;
+    boost::optional<std::chrono::time_point<std::chrono::steady_clock>> _tileCounterStartTime;
 
     boost::optional<TileCombined> _requestedTiles;
 };
commit 15afe2c0fb4b34de86f4473bb06349872e92333b
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Thu Jun 7 13:13:36 2018 +0200

    Wait tileprocessed message from client to send new bunch of tiles
    
    We always  one bunch of tiles (e.g. all tiles invalidated) and we
    are waiting until client send tileprocessed message back for all
    tiles before sending the new tiles.
    By canceltiles message we drop every previously requested tiles and
    make wsd ready to send new tiles, which will be requested by the client
    in theory.
    
    Change-Id: I9901420ada549e962ffaf5e6bd58e52b86bd129d

diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index e464b41a8..96a1299ff 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -1436,6 +1436,9 @@ L.TileLayer = L.GridLayer.extend({
 			tile.el.src = img;
 		}
 		L.Log.log(textMsg, L.INCOMING, key);
+
+		// Send acknowledgment, that the tile message arrived
+		this._map._socket.sendMessage('tileprocessed tile= ' + key);
 	},
 
 	_tileOnLoad: function (done, tile) {
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 582a346e8..d975c7200 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -54,7 +54,8 @@ ClientSession::ClientSession(const std::string& id,
     _tileWidthPixel(0),
     _tileHeightPixel(0),
     _tileWidthTwips(0),
-    _tileHeightTwips(0)
+    _tileHeightTwips(0),
+    _tilesOnFly(0)
 {
     assert(!creatingPngThumbnail || thumbnailFile != "");
     const size_t curConnections = ++LOOLWSD::NumConnections;
@@ -138,6 +139,7 @@ bool ClientSession::_handleInput(const char *buffer, int length)
         return loadDocument(buffer, length, tokens, docBroker);
     }
     else if (tokens[0] != "canceltiles" &&
+             tokens[0] != "tileprocessed" &&
              tokens[0] != "clientzoom" &&
              tokens[0] != "clientvisiblearea" &&
              tokens[0] != "outlinestate" &&
@@ -329,6 +331,13 @@ bool ClientSession::_handleInput(const char *buffer, int length)
             return forwardToChild(std::string(buffer, length), docBroker);
         }
     }
+    else if (tokens[0] == "tileprocessed")
+    {
+        if(_tilesOnFly > 0) // canceltiles message can zero this value
+            --_tilesOnFly;
+        docBroker->sendRequestedTiles(shared_from_this());
+        return true;
+    }
     else
     {
         if (tokens[0] == "key")
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index c0eaf9535..53e5201b5 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -17,9 +17,11 @@
 #include "DocumentBroker.hpp"
 #include <Poco/URI.h>
 #include <Rectangle.hpp>
+#include <boost/optional.hpp>
 
 class DocumentBroker;
 
+
 /// Represents a session to a LOOL client, in the WSD process.
 class ClientSession final : public Session, public std::enable_shared_from_this<ClientSession>
 {
@@ -107,6 +109,12 @@ public:
     /// Set WOPI fileinfo object
     void setWopiFileInfo(std::unique_ptr<WopiStorage::WOPIFileInfo>& wopiFileInfo) { _wopiFileInfo = std::move(wopiFileInfo); }
 
+    boost::optional<TileCombined>& getRequestedTiles() { return _requestedTiles; }
+
+    int getTilesOnFly() const { return _tilesOnFly; }
+    void setTilesOnFly(int tilesOnFly) { _tilesOnFly = tilesOnFly; }
+
+
 private:
 
     /// SocketHandler: disconnection event.
@@ -195,8 +203,13 @@ private:
 
     // Type of the docuemnt, extracter from status message
     std::string _docType;
+
+    int _tilesOnFly;
+
+    boost::optional<TileCombined> _requestedTiles;
 };
 
+
 #endif
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index 4d465a39d..1478af959 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -1290,61 +1290,136 @@ void DocumentBroker::handleTileCombinedRequest(TileCombined& tileCombined,
 
     LOG_TRC("TileCombined request for " << tileCombined.serialize());
 
-    // Satisfy as many tiles from the cache.
-    std::vector<TileDesc> tiles;
+    // Check which newly requested tiles needs rendering.
+    std::vector<TileDesc> tilesNeedsRendering;
     for (auto& tile : tileCombined.getTiles())
     {
         std::unique_ptr<std::fstream> cachedTile = _tileCache->lookupTile(tile);
-        if (cachedTile)
-        {
-            //TODO: Combine the response to reduce latency.
-#if ENABLE_DEBUG
-            const std::string response = tile.serialize("tile:") + " renderid=cached\n";
-#else
-            const std::string response = tile.serialize("tile:") + "\n";
-#endif
-
-            std::vector<char> output;
-            output.reserve(static_cast<size_t>(4) * tile.getWidth() * tile.getHeight());
-            output.resize(response.size());
-            std::memcpy(output.data(), response.data(), response.size());
-
-            assert(cachedTile->is_open());
-            cachedTile->seekg(0, std::ios_base::end);
-            const size_t pos = output.size();
-            std::streamsize size = cachedTile->tellg();
-            output.resize(pos + size);
-            cachedTile->seekg(0, std::ios_base::beg);
-            cachedTile->read(output.data() + pos, size);
+        if(cachedTile)
             cachedTile->close();
-
-            session->sendBinaryFrame(output.data(), output.size());
-        }
         else
         {
             // Not cached, needs rendering.
             tile.setVersion(++_tileVersion);
-            tileCache().subscribeToTileRendering(tile, session);
-            tiles.push_back(tile);
+            tilesNeedsRendering.push_back(tile);
             _debugRenderedTileCount++;
         }
     }
 
-    if (!tiles.empty())
+    // Send rendering request
+    if (!tilesNeedsRendering.empty())
     {
-        TileCombined newTileCombined = TileCombined::create(tiles);
+        TileCombined newTileCombined = TileCombined::create(tilesNeedsRendering);
 
         // Forward to child to render.
         const std::string req = newTileCombined.serialize("tilecombine");
         LOG_DBG("Sending residual tilecombine: " << req);
         _childProcess->sendTextFrame(req);
     }
+
+    // Accumulate tiles
+    boost::optional<TileCombined>& requestedTiles = session->getRequestedTiles();
+    if(requestedTiles == boost::none)
+    {
+        requestedTiles = TileCombined::create(tileCombined.getTiles());
+    }
+    // Drop duplicated tiles, but use newer version number
+    else
+    {
+        for (const auto& newTile : tileCombined.getTiles())
+        {
+            const TileDesc& firstOldTile = requestedTiles.get().getTiles()[0];
+            if(newTile.getPart() != firstOldTile.getPart() ||
+               newTile.getWidth() != firstOldTile.getWidth() ||
+               newTile.getHeight() != firstOldTile.getHeight() ||
+               newTile.getTileWidth() != firstOldTile.getTileWidth() ||
+               newTile.getTileHeight() != firstOldTile.getTileHeight() )
+            {
+                LOG_WRN("Different visible area information in tile requests");
+            }
+
+            bool tileFound = false;
+            for (auto& oldTile : requestedTiles.get().getTiles())
+            {
+                if(oldTile.getTilePosX() == newTile.getTilePosX() &&
+                   oldTile.getTilePosY() == newTile.getTilePosY() )
+                {
+                    oldTile.setVersion(newTile.getVersion());
+                    oldTile.setOldWireId(newTile.getOldWireId());
+                    oldTile.setWireId(newTile.getWireId());
+                    tileFound = true;
+                    break;
+                }
+            }
+            if(!tileFound)
+                requestedTiles.get().getTiles().push_back(newTile);
+        }
+    }
+
+    lock.unlock();
+    lock.release();
+    sendRequestedTiles(session);
+}
+
+void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& session)
+{
+    assert(session->getTilesOnFly() >= 0);
+    std::unique_lock<std::mutex> lock(_mutex);
+
+    // All tiles were processed on client side what we sent last time, so we can send a new banch of tiles
+    // which was invalidated / requested in the meantime
+    boost::optional<TileCombined>& requestedTiles = session->getRequestedTiles();
+    if(session->getTilesOnFly() == 0 && requestedTiles != boost::none && !requestedTiles.get().getTiles().empty())
+    {
+        session->setTilesOnFly(requestedTiles.get().getTiles().size());
+
+        // Satisfy as many tiles from the cache.
+        for (auto& tile : requestedTiles.get().getTiles())
+        {
+            std::unique_ptr<std::fstream> cachedTile = _tileCache->lookupTile(tile);
+            if (cachedTile)
+            {
+                //TODO: Combine the response to reduce latency.
+#if ENABLE_DEBUG
+                const std::string response = tile.serialize("tile:") + " renderid=cached\n";
+#else
+                const std::string response = tile.serialize("tile:") + "\n";
+#endif
+
+                std::vector<char> output;
+                output.reserve(static_cast<size_t>(4) * tile.getWidth() * tile.getHeight());
+                output.resize(response.size());
+                std::memcpy(output.data(), response.data(), response.size());
+
+                assert(cachedTile->is_open());
+                cachedTile->seekg(0, std::ios_base::end);
+                const auto pos = output.size();
+                std::streamsize size = cachedTile->tellg();
+                output.resize(pos + size);
+                cachedTile->seekg(0, std::ios_base::beg);
+                cachedTile->read(output.data() + pos, size);
+                cachedTile->close();
+
+                session->sendBinaryFrame(output.data(), output.size());
+            }
+            else
+            {
+                // Not cached, needs rendering. Rendering request was already sent
+                tileCache().subscribeToTileRendering(tile, session);
+            }
+        }
+        requestedTiles = boost::none;
+    }
 }
 
 void DocumentBroker::cancelTileRequests(const std::shared_ptr<ClientSession>& session)
 {
     std::unique_lock<std::mutex> lock(_mutex);
 
+    // Clear tile requests
+    session->setTilesOnFly(0);
+    session->getRequestedTiles() = boost::none;
+
     const std::string canceltiles = tileCache().cancelTiles(session);
     if (!canceltiles.empty())
     {
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 92c39b2e5..863a3d664 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -301,6 +301,7 @@ public:
     void handleDialogRequest(const std::string& dialogCmd);
     void handleTileCombinedRequest(TileCombined& tileCombined,
                                    const std::shared_ptr<ClientSession>& session);
+    void sendRequestedTiles(const std::shared_ptr<ClientSession>& session);
     void cancelTileRequests(const std::shared_ptr<ClientSession>& session);
     void handleTileResponse(const std::vector<char>& payload);
     void handleDialogPaintResponse(const std::vector<char>& payload, bool child);
diff --git a/wsd/protocol.txt b/wsd/protocol.txt
index 1697a8de4..c7a126670 100644
--- a/wsd/protocol.txt
+++ b/wsd/protocol.txt
@@ -31,6 +31,11 @@ canceltiles
     parameter. There is no guarantee of exactly which tile: messages
     might still be sent back to the client.
 
+tileprocessed tile=<tileid>
+
+    Previously sent tile (server -> client) arrived and processed by the client.
+    Tileid has the next stucture : <tile x coord>:<tile y coord>:<zoom level>:<selected part>
+
 downloadas name=<fileName> id=<id> format=<document format> options=<SkipImages, etc>
 
     Exports the current document to the desired format and returns a download URL
commit 57cdd68fcf84feb4a150c03c225c78cdb677479b
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Thu May 31 20:20:09 2018 +0200

    Request new tiles in wsd by invalidateTiles message
    
    And don't wait for the client to send back a tilecombine
    request.
    
    Change-Id: I9ea5de0f6b12dfaaf61992d34735d5b78ea382ed

diff --git a/common/Rectangle.hpp b/common/Rectangle.hpp
index c6c6efce4..1300aa71a 100644
--- a/common/Rectangle.hpp
+++ b/common/Rectangle.hpp
@@ -73,6 +73,11 @@ struct Rectangle
     {
         return _x1 <= _x2 && _y1 <= _y2;
     }
+
+    bool hasSurface()
+    {
+        return _x1 < _x2 && _y1 < _y2;
+    }
 };
 
 }
diff --git a/loleaflet/src/layer/tile/CalcTileLayer.js b/loleaflet/src/layer/tile/CalcTileLayer.js
index a53a6e34a..3e0bf2dfa 100644
--- a/loleaflet/src/layer/tile/CalcTileLayer.js
+++ b/loleaflet/src/layer/tile/CalcTileLayer.js
@@ -242,9 +242,6 @@ L.CalcTileLayer = L.TileLayer.extend({
 			command.height = parseInt(strTwips[3]);
 			command.part = this._selectedPart;
 		}
-		if (this._docType === 'text') {
-			command.part = 0;
-		}
 		var topLeftTwips = new L.Point(command.x, command.y);
 		var offset = new L.Point(command.width, command.height);
 		var bottomRightTwips = topLeftTwips.add(offset);
@@ -256,11 +253,6 @@ L.CalcTileLayer = L.TileLayer.extend({
 		var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast());
 		var visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight);
 
-		var tilePositionsX = '';
-		var tilePositionsY = '';
-		var oldWireIds = '';
-		var needsNewTiles = false;
-
 		for (var key in this._tiles) {
 			var coords = this._tiles[key].coords;
 			var tileTopLeft = this._coordsToTwips(coords);
@@ -274,24 +266,6 @@ L.CalcTileLayer = L.TileLayer.extend({
 					this._tiles[key]._invalidCount = 1;
 				}
 				if (visibleArea.intersects(bounds)) {
-					if (tilePositionsX !== '') {
-						tilePositionsX += ',';
-					}
-					tilePositionsX += tileTopLeft.x;
-					if (tilePositionsY !== '') {
-						tilePositionsY += ',';
-					}
-					tilePositionsY += tileTopLeft.y;
-					if (oldWireIds !== '') {
-						oldWireIds += ',';
-					}
-					if (this._tiles[key].oldWireId === undefined) {
-						oldWireIds += '0';
-					}
-					else {
-						oldWireIds += this._tiles[key].oldWireId;
-					}
-					needsNewTiles = true;
 					if (this._debug) {
 						this._debugAddInvalidationData(this._tiles[key]);
 					}
@@ -304,24 +278,6 @@ L.CalcTileLayer = L.TileLayer.extend({
 			}
 		}
 
-		if (needsNewTiles && command.part === this._selectedPart)
-		{
-			var message = 'tilecombine ' +
-				'part=' + command.part + ' ' +
-				'width=' + this._tileWidthPx + ' ' +
-				'height=' + this._tileHeightPx + ' ' +
-				'tileposx=' + tilePositionsX + ' ' +
-				'tileposy=' + tilePositionsY + ' ' +
-				'tilewidth=' + this._tileWidthTwips + ' ' +
-				'tileheight=' + this._tileHeightTwips + ' ' +
-				'oldwid=' + oldWireIds;
-
-			this._map._socket.sendMessage(message, '');
-			if (this._debug) {
-				this._debugAddInvalidationMessage(message);
-			}
-		}
-
 		for (key in this._tileCache) {
 			// compute the rectangle that each tile covers in the document based
 			// on the zoom level
diff --git a/loleaflet/src/layer/tile/ImpressTileLayer.js b/loleaflet/src/layer/tile/ImpressTileLayer.js
index abad6a799..83c78ccfb 100644
--- a/loleaflet/src/layer/tile/ImpressTileLayer.js
+++ b/loleaflet/src/layer/tile/ImpressTileLayer.js
@@ -366,11 +366,6 @@ L.ImpressTileLayer = L.TileLayer.extend({
 		var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast());
 		var visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight);
 
-		var tilePositionsX = '';
-		var tilePositionsY = '';
-		var oldWireIds = '';
-		var needsNewTiles = false;
-
 		for (var key in this._tiles) {
 			var coords = this._tiles[key].coords;
 			var tileTopLeft = this._coordsToTwips(coords);
@@ -384,24 +379,6 @@ L.ImpressTileLayer = L.TileLayer.extend({
 					this._tiles[key]._invalidCount = 1;
 				}
 				if (visibleArea.intersects(bounds)) {
-					if (tilePositionsX !== '') {
-						tilePositionsX += ',';
-					}
-					tilePositionsX += tileTopLeft.x;
-					if (tilePositionsY !== '') {
-						tilePositionsY += ',';
-					}
-					tilePositionsY += tileTopLeft.y;
-					if (oldWireIds !== '') {
-						oldWireIds += ',';
-					}
-					if (this._tiles[key].oldWireId === undefined) {
-						oldWireIds += '0';
-					}
-					else {
-						oldWireIds += this._tiles[key].oldWireId;
-					}
-					needsNewTiles = true;
 					if (this._debug) {
 						this._debugAddInvalidationData(this._tiles[key]);
 					}
@@ -414,24 +391,6 @@ L.ImpressTileLayer = L.TileLayer.extend({
 			}
 		}
 
-		if (needsNewTiles && command.part === this._selectedPart)
-		{
-			var message = 'tilecombine ' +
-				'part=' + command.part + ' ' +
-				'width=' + this._tileWidthPx + ' ' +
-				'height=' + this._tileHeightPx + ' ' +
-				'tileposx=' + tilePositionsX + ' ' +
-				'tileposy=' + tilePositionsY + ' ' +
-				'tilewidth=' + this._tileWidthTwips + ' ' +
-				'tileheight=' + this._tileHeightTwips + ' ' +
-				'oldwid=' + oldWireIds;
-
-			this._map._socket.sendMessage(message, '');
-			if (this._debug) {
-				this._debugAddInvalidationMessage(message);
-			}
-		}
-
 		for (key in this._tileCache) {
 			// compute the rectangle that each tile covers in the document based
 			// on the zoom level
diff --git a/loleaflet/src/layer/tile/WriterTileLayer.js b/loleaflet/src/layer/tile/WriterTileLayer.js
index 6c1a886b8..8c9f46e81 100644
--- a/loleaflet/src/layer/tile/WriterTileLayer.js
+++ b/loleaflet/src/layer/tile/WriterTileLayer.js
@@ -109,10 +109,6 @@ L.WriterTileLayer = L.TileLayer.extend({
 		var visibleTopLeft = this._latLngToTwips(this._map.getBounds().getNorthWest());
 		var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast());
 		var visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight);
-		var tilePositionsX = '';
-		var tilePositionsY = '';
-		var oldWireIds = '';
-		var needsNewTiles = false;
 		for (var key in this._tiles) {
 			var coords = this._tiles[key].coords;
 			var tileTopLeft = this._coordsToTwips(coords);
@@ -126,24 +122,6 @@ L.WriterTileLayer = L.TileLayer.extend({
 					this._tiles[key]._invalidCount = 1;
 				}
 				if (visibleArea.intersects(bounds)) {
-					if (tilePositionsX !== '') {
-						tilePositionsX += ',';
-					}
-					tilePositionsX += tileTopLeft.x;
-					if (tilePositionsY !== '') {
-						tilePositionsY += ',';
-					}
-					tilePositionsY += tileTopLeft.y;
-					if (oldWireIds !== '') {
-						oldWireIds += ',';
-					}
-					if (this._tiles[key].oldWireId === undefined) {
-						oldWireIds += '0';
-					}
-					else {
-						oldWireIds += this._tiles[key].oldWireId;
-					}
-					needsNewTiles = true;
 					if (this._debug) {
 						this._debugAddInvalidationData(this._tiles[key]);
 					}
@@ -156,28 +134,6 @@ L.WriterTileLayer = L.TileLayer.extend({
 			}
 		}
 
-		if (needsNewTiles)
-		{
-			// CalcTileLayer.js and ImpressTileLayer.js avoid this when
-			// command.part !== this._selectedPart; but in Writer, the part is
-			// always 0 anyway
-			var message = 'tilecombine ' +
-				'part=' + command.part + ' ' +
-				'width=' + this._tileWidthPx + ' ' +
-				'height=' + this._tileHeightPx + ' ' +
-				'tileposx=' + tilePositionsX + ' ' +
-				'tileposy=' + tilePositionsY + ' ' +
-				'tilewidth=' + this._tileWidthTwips + ' ' +
-				'tileheight=' + this._tileHeightTwips + ' ' +
-				'oldwid=' + oldWireIds;
-
-			this._map._socket.sendMessage(message, '');
-
-			if (this._debug) {
-				this._debugAddInvalidationMessage(message);
-			}
-		}
-
 		for (key in this._tileCache) {
 			// compute the rectangle that each tile covers in the document based
 			// on the zoom level
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 67fe8bd20..582a346e8 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -50,7 +50,7 @@ ClientSession::ClientSession(const std::string& id,
     _isViewLoaded(false),
     _keyEvents(1),
     _clientVisibleArea(0, 0, 0, 0),
-    _clientSelectedPart(0),
+    _clientSelectedPart(-1),
     _tileWidthPixel(0),
     _tileHeightPixel(0),
     _tileWidthTwips(0),
@@ -903,6 +903,13 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
             setViewLoaded();
             docBroker->setLoaded();
 
+            const std::string stringMsg(buffer, length);
+            const size_t index = stringMsg.find("type=") + 5;
+            if (index != std::string::npos)
+            {
+                _docType = stringMsg.substr(index, stringMsg.find_first_of(' ', index) - index);
+            }
+
             // Forward the status response to the client.
             return forwardToClient(payload);
         }
@@ -928,7 +935,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
         else if (tokens[0] == "invalidatetiles:")
         {
             assert(firstLine.size() == static_cast<std::string::size_type>(length));
-            docBroker->invalidateTiles(firstLine);
+            handleTileInvalidation(firstLine, docBroker);
         }
         else if (tokens[0] == "invalidatecursor:")
         {
@@ -1103,4 +1110,59 @@ void ClientSession::dumpState(std::ostream& os)
 
 }
 
+void ClientSession::handleTileInvalidation(const std::string& message,
+    const std::shared_ptr<DocumentBroker>& docBroker)
+{
+    docBroker->invalidateTiles(message);
+
+    // Skip requesting new tiles if we don't have client visible area data yet.
+    if(!_clientVisibleArea.hasSurface() ||
+       _tileWidthPixel == 0 || _tileHeightPixel == 0 ||
+       _tileWidthTwips == 0 || _tileHeightTwips == 0 ||
+       _clientSelectedPart == -1)
+    {
+        return;
+    }
+
+    std::pair<int, Util::Rectangle> result = TileCache::parseInvalidateMsg(message);
+    int part = result.first;
+    Util::Rectangle& invalidateRect = result.second;
+
+    if(_docType.find("text") != std::string::npos) // For Writer we don't have separate parts
+        part = 0;
+
+    if(part == -1) // If no part is specifed we use the part used by the client
+        part = _clientSelectedPart;
+
+    std::vector<TileDesc> invalidTiles;
+    if(part == _clientSelectedPart)
+    {
+        Util::Rectangle intersection;
+        intersection._x1 = std::max(invalidateRect._x1, _clientVisibleArea._x1);
+        intersection._y1 = std::max(invalidateRect._y1, _clientVisibleArea._y1);
+        intersection._x2 = std::min(invalidateRect._x2, _clientVisibleArea._x2);
+        intersection._y2 = std::min(invalidateRect._y2, _clientVisibleArea._y2);
+        if(intersection.isValid()) // Client visible area and invalidated rectangle has intersection
+        {
+            for(int i = std::ceil(intersection._x1 / _tileWidthTwips);
+                i <= std::ceil(intersection._x2 / _tileWidthTwips); ++i)
+            {
+                for(int j = std::ceil(intersection._y1 / _tileHeightTwips);
+                    j <= std::ceil(intersection._y2 / _tileHeightTwips); ++j)
+                {
+                    invalidTiles.emplace_back(TileDesc(part, _tileWidthPixel, _tileHeightPixel, i * _tileWidthTwips, j * _tileHeightTwips, _tileWidthTwips, _tileHeightTwips, -1, 0, -1, false));
+                    invalidTiles.back().setOldWireId(0);
+                    invalidTiles.back().setWireId(0);
+                }
+            }
+        }
+    }
+
+    if(!invalidTiles.empty())
+    {
+        TileCombined tileCombined = TileCombined::create(invalidTiles);
+        docBroker->handleTileCombinedRequest(tileCombined, shared_from_this());
+    }
+}
+
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index 919bc8dbd..c0eaf9535 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -144,6 +144,9 @@ private:
 
     void dumpState(std::ostream& os) override;
 
+    void handleTileInvalidation(const std::string& message,
+                                const std::shared_ptr<DocumentBroker>& docBroker);
+
 private:
     std::weak_ptr<DocumentBroker> _docBroker;
 
@@ -189,6 +192,9 @@ private:
     int _tileHeightPixel;
     int _tileWidthTwips;
     int _tileHeightTwips;
+
+    // Type of the docuemnt, extracter from status message
+    std::string _docType;
 };
 
 #endif
diff --git a/wsd/TileCache.cpp b/wsd/TileCache.cpp
index 51f1933de..ef697933a 100644
--- a/wsd/TileCache.cpp
+++ b/wsd/TileCache.cpp
@@ -339,22 +339,27 @@ void TileCache::invalidateTiles(int part, int x, int y, int width, int height)
 
 void TileCache::invalidateTiles(const std::string& tiles)
 {
+    std::pair<int, Util::Rectangle> result = TileCache::parseInvalidateMsg(tiles);
+    Util::Rectangle& invalidateRect = result.second;
+    invalidateTiles(result.first, invalidateRect.getLeft(), invalidateRect.getTop(), invalidateRect.getWidth(), invalidateRect.getHeight());
+}
+
+std::pair<int, Util::Rectangle> TileCache::parseInvalidateMsg(const std::string& tiles)
+{
     StringTokenizer tokens(tiles, " ", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM);
 
     assert(tokens[0] == "invalidatetiles:");
 
     if (tokens.count() == 2 && tokens[1] == "EMPTY")
     {
-        invalidateTiles(-1, 0, 0, INT_MAX, INT_MAX);
-        return;
+        return std::pair<int, Util::Rectangle>(-1, Util::Rectangle(0, 0, INT_MAX, INT_MAX));
     }
     else if (tokens.count() == 3 && tokens[1] == "EMPTY,")
     {
         int part = 0;
         if (stringToInteger(tokens[2], part))
         {
-            invalidateTiles(part, 0, 0, INT_MAX, INT_MAX);
-            return;
+            return std::pair<int, Util::Rectangle>(part, Util::Rectangle(0, 0, INT_MAX, INT_MAX));
         }
     }
     else
@@ -367,12 +372,13 @@ void TileCache::invalidateTiles(const std::string& tiles)
             getTokenInteger(tokens[4], "width", width) &&
             getTokenInteger(tokens[5], "height", height))
         {
-            invalidateTiles(part, x, y, width, height);
-            return;
+
+            return std::pair<int, Util::Rectangle>(part, Util::Rectangle(x, y, width, height));
         }
     }
 
     LOG_ERR("Unexpected invalidatetiles request [" << tiles << "].");
+    return std::pair<int, Util::Rectangle>(-1, Util::Rectangle(0, 0, 0, 0));
 }
 
 void TileCache::removeFile(const std::string& fileName)
diff --git a/wsd/TileCache.hpp b/wsd/TileCache.hpp
index 5f8e71cfe..d8c48eaa4 100644
--- a/wsd/TileCache.hpp
+++ b/wsd/TileCache.hpp
@@ -17,6 +17,7 @@
 #include <string>
 
 #include <Poco/Timestamp.h>
+#include <Rectangle.hpp>
 
 #include "TileDesc.hpp"
 
@@ -72,6 +73,9 @@ public:
     // The tiles parameter is an invalidatetiles: message as sent by the child process
     void invalidateTiles(const std::string& tiles);
 
+    /// Parse invalidateTiles message to a part number and a rectangle of the invalidated area
+    static std::pair<int, Util::Rectangle> parseInvalidateMsg(const std::string& tiles);
+
     /// Store the timestamp to modtime.txt.
     void saveLastModified(const Poco::Timestamp& timestamp);
 
commit 161695eb66f7da5d7a9051b186306f39395fb8c0
Author: Tamás Zolnai <tamas.zolnai at collabora.com>
Date:   Wed May 30 19:35:13 2018 +0200

    Store client's visible are information in wsd
    
    Change-Id: Iec3c146181b7db2e76247d5775076e6ac90eed2c

diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index 1a7cb886f..e464b41a8 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -1481,10 +1481,23 @@ L.TileLayer = L.GridLayer.extend({
 			this._map._socket.sendMessage('clientzoom ' + this._clientZoom);
 			this._clientZoom = null;
 		}
+
+		if (this._clientVisibleArea) {
+			// Visible area is dirty, update it on the server.
+			var visibleArea = this._map._container.getBoundingClientRect();
+			var pos = this._pixelsToTwips(new L.Point(visibleArea.left, visibleArea.top));
+			var size = this._pixelsToTwips(new L.Point(visibleArea.width, visibleArea.height));
+			var payload = 'clientvisiblearea x=' + Math.round(pos.x) + ' y=' + Math.round(pos.y) +
+				' width=' + Math.round(size.x) + ' height=' + Math.round(size.y);
+			this._map._socket.sendMessage(payload);
+			this._clientVisibleArea = false;
+		}
+
 		this._map._socket.sendMessage('mouse type=' + type +
 				' x=' + x + ' y=' + y + ' count=' + count +
 				' buttons=' + buttons + ' modifier=' + modifier);
 
+
 		if (type === 'buttondown') {
 			this._clearSearchResults();
 		}
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 0cd76c9f8..67fe8bd20 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -48,7 +48,13 @@ ClientSession::ClientSession(const std::string& id,
     _isDocumentOwner(false),
     _isAttached(false),
     _isViewLoaded(false),
-    _keyEvents(1)
+    _keyEvents(1),
+    _clientVisibleArea(0, 0, 0, 0),
+    _clientSelectedPart(0),
+    _tileWidthPixel(0),
+    _tileHeightPixel(0),
+    _tileWidthTwips(0),
+    _tileHeightTwips(0)
 {
     assert(!creatingPngThumbnail || thumbnailFile != "");
     const size_t curConnections = ++LOOLWSD::NumConnections;
@@ -266,6 +272,63 @@ bool ClientSession::_handleInput(const char *buffer, int length)
             docBroker->broadcastMessage("commandresult: { \"command\": \"savetostorage\", \"success\": true }");
         }
     }
+    else if (tokens[0] == "clientvisiblearea")
+    {
+        int x;
+        int y;
+        int width;
+        int height;
+        if (tokens.size() != 5 ||
+            !getTokenInteger(tokens[1], "x", x) ||
+            !getTokenInteger(tokens[2], "y", y) ||
+            !getTokenInteger(tokens[3], "width", width) ||
+            !getTokenInteger(tokens[4], "height", height))
+        {
+            sendTextFrame("error: cmd=clientvisiblearea kind=syntax");
+            return false;
+        }
+        else
+        {
+            _clientVisibleArea = Util::Rectangle(x, y, width, height);
+            return forwardToChild(std::string(buffer, length), docBroker);
+        }
+    }
+    else if (tokens[0] == "setclientpart")
+    {
+        int temp;
+        if (tokens.size() != 2 ||
+            !getTokenInteger(tokens[1], "part", temp))
+        {
+            sendTextFrame("error: cmd=setclientpart kind=syntax");
+            return false;
+        }
+        else
+        {
+            _clientSelectedPart = temp;
+            return forwardToChild(std::string(buffer, length), docBroker);
+        }
+    }
+    else if (tokens[0] == "clientzoom")
+    {
+        int tilePixelWidth, tilePixelHeight, tileTwipWidth, tileTwipHeight;
+        if (tokens.size() != 5 ||
+            !getTokenInteger(tokens[1], "tilepixelwidth", tilePixelWidth) ||
+            !getTokenInteger(tokens[2], "tilepixelheight", tilePixelHeight) ||
+            !getTokenInteger(tokens[3], "tiletwipwidth", tileTwipWidth) ||
+            !getTokenInteger(tokens[4], "tiletwipheight", tileTwipHeight))
+        {
+            sendTextFrame("error: cmd=clientzoom kind=syntax");
+            return false;
+        }
+        else
+        {
+            _tileWidthPixel = tilePixelWidth;
+            _tileHeightPixel = tilePixelHeight;
+            _tileWidthTwips = tileTwipWidth;
+            _tileHeightTwips = tileTwipHeight;
+            return forwardToChild(std::string(buffer, length), docBroker);
+        }
+    }
     else
     {
         if (tokens[0] == "key")
@@ -647,6 +710,14 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
         int curPart;
         return getTokenInteger(tokens[1], "part", curPart);
     }
+    else if (tokens[0] == "setpart:" && tokens.size() == 2)
+    {
+        int setPart;
+        if(getTokenInteger(tokens[1], "part", setPart))
+            _clientSelectedPart = setPart;
+        else
+            return false;
+    }
     else if (tokens.size() == 3 && tokens[0] == "saveas:")
     {
         bool isConvertTo = static_cast<bool>(_saveAsSocket);
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index df7faa45f..919bc8dbd 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -16,6 +16,7 @@
 #include "SenderQueue.hpp"
 #include "DocumentBroker.hpp"
 #include <Poco/URI.h>
+#include <Rectangle.hpp>
 
 class DocumentBroker;
 
@@ -176,6 +177,18 @@ private:
     uint64_t _keyEvents;
 
     SenderQueue<std::shared_ptr<Message>> _senderQueue;
+
+    /// Visible area of the client
+    Util::Rectangle _clientVisibleArea;
+
+    /// Selected part of the document viewed by the client (no parts in Writer)
+    int _clientSelectedPart;
+
+    /// Zoom properties of the client
+    int _tileWidthPixel;
+    int _tileHeightPixel;
+    int _tileWidthTwips;
+    int _tileHeightTwips;
 };
 
 #endif


More information about the Libreoffice-commits mailing list