diff options
-rw-r--r-- | connectionregistry.cpp | 5 | ||||
-rw-r--r-- | connectionregistry.h | 2 | ||||
-rw-r--r-- | debian/changelog | 6 | ||||
-rw-r--r-- | html/stats.html | 35 | ||||
-rw-r--r-- | html/stats.js | 96 | ||||
-rw-r--r-- | storage.cpp | 24 | ||||
-rw-r--r-- | storage.h | 7 | ||||
-rw-r--r-- | tests/test-connectionregistry.cpp | 23 | ||||
-rw-r--r-- | tests/test-storage.cpp | 28 | ||||
-rw-r--r-- | whiteboard.cpp | 8 |
10 files changed, 224 insertions, 10 deletions
diff --git a/connectionregistry.cpp b/connectionregistry.cpp index 11a538b..412472d 100644 --- a/connectionregistry.cpp +++ b/connectionregistry.cpp @@ -81,6 +81,11 @@ void ConnectionRegistry::dump() const } } +size_t ConnectionRegistry::number_of_connections() const +{ + return m_connections.size(); +} + ConnectionRegistry::RegistryGuard::RegistryGuard(ConnectionRegistry& registry, ConnectionRegistry::connection c): m_registry{registry}, m_connection{c} diff --git a/connectionregistry.h b/connectionregistry.h index 155ab31..25bd3b6 100644 --- a/connectionregistry.h +++ b/connectionregistry.h @@ -27,6 +27,8 @@ public: void dump() const; + size_t number_of_connections() const; + private: // map connection to id std::unordered_map<connection, std::string> m_connections; diff --git a/debian/changelog b/debian/changelog index 688c98f..7c0064f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +whiteboard (1.6) UNRELEASED; urgency=medium + + * Added stats.html + + -- Roland Reichwein <mail@reichwein.it> Tue, 31 Jan 2023 18:22:42 +0100 + whiteboard (1.5) unstable; urgency=medium * Move from FCGI to websocket interface diff --git a/html/stats.html b/html/stats.html new file mode 100644 index 0000000..3f7e141 --- /dev/null +++ b/html/stats.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="keywords" content="Reichwein, Whiteboard"> + <title>Reichwein Whiteboard</title> + <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/> + <link rel="stylesheet" type="text/css" href="whiteboard.css"/> + <script src="stats.js"></script> + </head> + <body onload="init();"> + <div class="qrwindow" id="qrwindow" hidden> + <img class="qrcode" id="qrcode"></img> + </div> + <div class="page"> + <h1><img class="banner" src="banner256.png" alt="Reichwein.IT"/> Whiteboard</h1> + <h2>Statistics</h2> + <table> + <tr><td>Active Connections:</td><td id="numberofconnections" align="right"></td><td></td></tr> + <tr><td>Number of Documents:</td><td id="numberofdocuments" align="right"></td><td></td></tr> + <tr><td>Database Size (gross):</td><td id="dbsizegross" align="right"></td><td>Bytes</td></tr> + <tr><td>Database Size (net):</td><td id="dbsizenet" align="right"></td><td>Bytes</td></tr> + </table> + <br/> + <span id="status">Starting up...</span> + <button class="buttonred" id="reconnect" onclick="on_reconnect_click();" hidden>Reconnect</button> + <br/> + <br/> + Reichwein.IT Whiteboard <span id="version"></span> by <a href="https://www.reichwein.it">https://www.reichwein.it</a><br/> + </div> + + <a id="download-a" hidden></a> + </body> +</html> diff --git a/html/stats.js b/html/stats.js new file mode 100644 index 0000000..89c674a --- /dev/null +++ b/html/stats.js @@ -0,0 +1,96 @@ +// started on main page load +function init() { + init_stats(); +} + +function set_status(message) +{ + if (message == "") { + document.getElementById("status").textContent = message; + document.getElementById("status").style.display = 'inline'; + } else { + document.getElementById("status").textContent = ""; + document.getElementById("status").style.display = 'none'; + } +} + +var websocket; + +// +// Callbacks for websocket data of different types +// + +function on_stats(numberofdocuments, numberofconnections, dbsizegross, dbsizenet) +{ + document.getElementById("numberofdocuments").textContent = numberofdocuments; + document.getElementById("numberofconnections").textContent = numberofconnections; + document.getElementById("dbsizegross").textContent = dbsizegross; + document.getElementById("dbsizenet").textContent = dbsizenet; +} + +function on_version(version) +{ + document.getElementById("version").textContent = version; +} + +function on_message(e) { + var parser = new DOMParser(); + var xmlDocument = parser.parseFromString(e.data, "text/xml"); + + var type = xmlDocument.getElementsByTagName("type")[0].textContent; + + if (type == "stats") { + on_stats(xmlDocument.getElementsByTagName("numberofdocuments")[0].textContent, + xmlDocument.getElementsByTagName("numberofconnections")[0].textContent, + xmlDocument.getElementsByTagName("dbsizegross")[0].textContent, + xmlDocument.getElementsByTagName("dbsizenet")[0].textContent); + } else if (type == "version") { + on_version(xmlDocument.getElementsByTagName("version")[0].textContent); + } else if (type == "error") { + alert(xmlDocument.getElementsByTagName("message")[0].textContent); + } else { + alert("Unhandled message type: " + e.data + "|" + type); + } +} + +function connect_websocket() { + document.getElementById("reconnect").style.display = 'none'; + set_status("Connecting..."); + var newlocation = location.origin + location.pathname; + newlocation = newlocation.replace(/^http/, 'ws'); + newlocation = newlocation.replace(/stats.html$/, ''); + if (newlocation.slice(-1) != "/") + newlocation += "/"; + newlocation += "websocket"; + + websocket = new WebSocket(newlocation); + + websocket.onmessage = function(e) { on_message(e); }; + + websocket.onopen = function(e) { + websocket.send("<request><command>getversion</command></request>"); + websocket.send("<request><command>getstats</command></request>"); + set_status(""); // ok + }; + + websocket.onclose = function(e) { + alert("Server connection closed."); + document.getElementById("reconnect").style.display = 'inline'; + }; + + websocket.onerror = function(e) { + alert("Error: Server connection closed."); + document.getElementById("reconnect").style.display = 'inline'; + }; +} + +// button in html +function on_reconnect_click() { + connect_websocket(); +} + +function init_stats() { + set_status("Loading..."); + connect_websocket(); +} + diff --git a/storage.cpp b/storage.cpp index f7f15b1..f7b8b5d 100644 --- a/storage.cpp +++ b/storage.cpp @@ -27,7 +27,9 @@ Storage::Storage(const Config& config): m_stmt_setDocument_new(m_db, "INSERT INTO documents (id, value, rev, cursorpos, timestamp) values (?, ?, ?, ?, ?)"), m_stmt_setRevision(m_db, "UPDATE documents SET rev = ? WHERE id = ?"), m_stmt_setCursorPos(m_db, "UPDATE documents SET cursorpos = ? WHERE id = ?"), - m_stmt_setRow(m_db, "INSERT OR REPLACE INTO documents (id, value, rev, cursorpos, timestamp) values (?, ?, ?, ?, ?)") + m_stmt_setRow(m_db, "INSERT OR REPLACE INTO documents (id, value, rev, cursorpos, timestamp) values (?, ?, ?, ?, ?)"), + m_stmt_getDbSizeGross(m_db, "SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"), + m_stmt_getDbSizeNet(m_db, "SELECT (page_count - freelist_count) * page_size as size FROM pragma_page_count(), pragma_freelist_count(), pragma_page_size()") { CompiledSQL::Guard g{m_stmt_create}; m_stmt_create.execute(); @@ -46,6 +48,24 @@ uint64_t Storage::getNumberOfDocuments() return m_stmt_getNumberOfDocuments.getColumn<int64_t>(0); } +uint64_t Storage::dbsize_gross() +{ + CompiledSQL::Guard g{m_stmt_getDbSizeGross}; + if (!m_stmt_getDbSizeGross.execute()) + throw std::runtime_error("DB size count (gross) not possible"); + + return m_stmt_getDbSizeGross.getColumn<int64_t>(0); +} + +uint64_t Storage::dbsize_net() +{ + CompiledSQL::Guard g{m_stmt_getDbSizeNet}; + if (!m_stmt_getDbSizeNet.execute()) + throw std::runtime_error("DB size count (net) not possible"); + + return m_stmt_getDbSizeNet.getColumn<int64_t>(0); +} + namespace { uint64_t unixepoch() { @@ -169,7 +189,7 @@ void Storage::setRow(const std::string& id, const std::string& document, int rev throw std::runtime_error("Unable to insert row with id "s + id); } -uint32_t Storage::checksum32(const std::string& s) +uint32_t checksum32(const std::string& s) { uint32_t result{0}; for (unsigned int i = 0; i < s.size(); i++) { @@ -16,6 +16,8 @@ public: ~Storage(); uint64_t getNumberOfDocuments(); + uint64_t dbsize_gross(); + uint64_t dbsize_net(); bool exists(const std::string& id); std::string getDocument(const std::string& id); @@ -31,7 +33,6 @@ public: void cleanup(); std::string generate_id(); - uint32_t checksum32(const std::string& s); private: SQLite::Database m_db; @@ -52,5 +53,9 @@ private: CompiledSQL m_stmt_setRevision; CompiledSQL m_stmt_setCursorPos; CompiledSQL m_stmt_setRow; + CompiledSQL m_stmt_getDbSizeGross; + CompiledSQL m_stmt_getDbSizeNet; }; +uint32_t checksum32(const std::string& s); + diff --git a/tests/test-connectionregistry.cpp b/tests/test-connectionregistry.cpp index 1f68dd9..282f397 100644 --- a/tests/test-connectionregistry.cpp +++ b/tests/test-connectionregistry.cpp @@ -142,3 +142,26 @@ TEST_F(ConnectionRegistryTest, test_guard) EXPECT_THROW(cr.delConnection(c), std::exception); } +TEST_F(ConnectionRegistryTest, number_of_connections) +{ + boost::asio::io_context ioc{1}; + + boost::asio::ip::tcp::socket ts0{ioc}; + ConnectionRegistry::connection c0 {std::make_shared<ConnectionRegistry::connection::element_type>(std::move(ts0))}; + + boost::asio::ip::tcp::socket ts1{ioc}; + ConnectionRegistry::connection c1 {std::make_shared<ConnectionRegistry::connection::element_type>(std::move(ts1))}; + + ConnectionRegistry cr{}; + + EXPECT_EQ(cr.number_of_connections(), 0); + cr.addConnection(c0); + EXPECT_EQ(cr.number_of_connections(), 1); + cr.addConnection(c1); + EXPECT_EQ(cr.number_of_connections(), 2); + cr.delConnection(c0); + EXPECT_EQ(cr.number_of_connections(), 1); + cr.delConnection(c1); + EXPECT_EQ(cr.number_of_connections(), 0); +} + diff --git a/tests/test-storage.cpp b/tests/test-storage.cpp index d8259e1..51a6058 100644 --- a/tests/test-storage.cpp +++ b/tests/test-storage.cpp @@ -228,13 +228,27 @@ TEST_F(StorageTest, generate_id) TEST_F(StorageTest, checksum32) { + EXPECT_EQ(checksum32(""), 0); + EXPECT_EQ(checksum32("0"), 48); + EXPECT_EQ(checksum32("\x00"), 0); + EXPECT_EQ(checksum32("123"), 1073741862); + EXPECT_EQ(checksum32("a"), 97); + EXPECT_EQ(checksum32("ab"), 82); + EXPECT_EQ(checksum32("abc"), 1073741898); +} + +TEST_F(StorageTest, db_size) +{ Storage storage(*m_config); - EXPECT_EQ(storage.checksum32(""), 0); - EXPECT_EQ(storage.checksum32("0"), 48); - EXPECT_EQ(storage.checksum32("\x00"), 0); - EXPECT_EQ(storage.checksum32("123"), 1073741862); - EXPECT_EQ(storage.checksum32("a"), 97); - EXPECT_EQ(storage.checksum32("ab"), 82); - EXPECT_EQ(storage.checksum32("abc"), 1073741898); + auto dbsize_gross{storage.dbsize_gross()}; + auto dbsize_net{storage.dbsize_net()}; + + EXPECT_LE(0, storage.dbsize_net()); + EXPECT_LE(storage.dbsize_net(), storage.dbsize_gross()); + + storage.setDocument("0", "xyz"); + + EXPECT_LE(dbsize_net, storage.dbsize_net()); + EXPECT_LE(dbsize_gross, storage.dbsize_gross()); } diff --git a/whiteboard.cpp b/whiteboard.cpp index 8736726..ce196f4 100644 --- a/whiteboard.cpp +++ b/whiteboard.cpp @@ -233,6 +233,14 @@ std::string Whiteboard::handle_request(Whiteboard::connection& c, const std::str {"type", "version"}, {"version", WHITEBOARD_VERSION } }); + } else if (command == "getstats") { + return make_xml({ + {"type", "stats" }, + {"dbsizegross", std::to_string(m_storage->dbsize_gross()) }, + {"dbsizenet", std::to_string(m_storage->dbsize_net()) }, + {"numberofdocuments", std::to_string(m_storage->getNumberOfDocuments()) }, + {"numberofconnections", std::to_string(m_registry.number_of_connections()) }, + }); } else { throw std::runtime_error("Bad command: "s + command); } |