summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRoland Reichwein <mail@reichwein.it>2023-02-04 12:43:51 +0100
committerRoland Reichwein <mail@reichwein.it>2023-02-04 12:43:51 +0100
commitc4a1f194e79a7834a54fdbf63d73c33e434b4825 (patch)
tree51eae6c701f38e47790b7200b423de8c89b38465
parent1771f788a5b9e844f0a5315faee104648e3b7d88 (diff)
Added stats.html
-rw-r--r--connectionregistry.cpp5
-rw-r--r--connectionregistry.h2
-rw-r--r--debian/changelog6
-rw-r--r--html/stats.html35
-rw-r--r--html/stats.js96
-rw-r--r--storage.cpp24
-rw-r--r--storage.h7
-rw-r--r--tests/test-connectionregistry.cpp23
-rw-r--r--tests/test-storage.cpp28
-rw-r--r--whiteboard.cpp8
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++) {
diff --git a/storage.h b/storage.h
index fe8d060..f997657 100644
--- a/storage.h
+++ b/storage.h
@@ -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);
}