diff --git a/src-tauri/.sqlx/query-498b544eb893fa8b1ecd10984601e9b4bc7bef591e480ede9964743091422baf.json b/src-tauri/.sqlx/query-498b544eb893fa8b1ecd10984601e9b4bc7bef591e480ede9964743091422baf.json new file mode 100644 index 000000000..f5ad6c655 --- /dev/null +++ b/src-tauri/.sqlx/query-498b544eb893fa8b1ecd10984601e9b4bc7bef591e480ede9964743091422baf.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "SELECT id \"id!\", tunnel_id, SUM(MAX(upload_diff, 0)) \"upload!: i64\", SUM(MAX(download_diff, 0)) \"download!: i64\", 0 \"upload_diff!: i64\", 0 \"download_diff!: i64\", last_handshake \"last_handshake!: i64\", strftime($1, collected_at) \"collected_at!: NaiveDateTime\", listen_port \"listen_port!: u32\", persistent_keepalive_interval \"persistent_keepalive_interval!: u16\" FROM tunnel_stats WHERE tunnel_id = $2 AND collected_at >= datetime(strftime($1, $3)) GROUP BY strftime($1, collected_at) ORDER BY collected_at", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "tunnel_id", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "upload!: i64", + "ordinal": 2, + "type_info": "Null" + }, + { + "name": "download!: i64", + "ordinal": 3, + "type_info": "Null" + }, + { + "name": "upload_diff!: i64", + "ordinal": 4, + "type_info": "Null" + }, + { + "name": "download_diff!: i64", + "ordinal": 5, + "type_info": "Null" + }, + { + "name": "last_handshake!: i64", + "ordinal": 6, + "type_info": "Integer" + }, + { + "name": "collected_at!: NaiveDateTime", + "ordinal": 7, + "type_info": "Null" + }, + { + "name": "listen_port!: u32", + "ordinal": 8, + "type_info": "Integer" + }, + { + "name": "persistent_keepalive_interval!: u16", + "ordinal": 9, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + true, + false, + null, + null, + null, + null, + false, + null, + false, + false + ] + }, + "hash": "498b544eb893fa8b1ecd10984601e9b4bc7bef591e480ede9964743091422baf" +} diff --git a/src-tauri/.sqlx/query-7f97fe602e8d1c75f79d88abd2b20eca881e3d8682aaff17b2342f282e3916bf.json b/src-tauri/.sqlx/query-5aafcc34860d0cb6412a0ae1d94f7372ff389f8afc11d21baddc9aabe07692be.json similarity index 84% rename from src-tauri/.sqlx/query-7f97fe602e8d1c75f79d88abd2b20eca881e3d8682aaff17b2342f282e3916bf.json rename to src-tauri/.sqlx/query-5aafcc34860d0cb6412a0ae1d94f7372ff389f8afc11d21baddc9aabe07692be.json index f826464a4..50a1a21af 100644 --- a/src-tauri/.sqlx/query-7f97fe602e8d1c75f79d88abd2b20eca881e3d8682aaff17b2342f282e3916bf.json +++ b/src-tauri/.sqlx/query-5aafcc34860d0cb6412a0ae1d94f7372ff389f8afc11d21baddc9aabe07692be.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "WITH prev_download AS (\n SELECT download\n FROM location_stats\n WHERE location_id = $1\n ORDER BY collected_at DESC\n LIMIT 1 OFFSET 1\n )\n SELECT ls.id \"id!: i64\",\n ls.location_id,\n ls.upload \"upload!: i64\",\n ls.download \"download!: i64\",\n ls.last_handshake,\n ls.collected_at \"collected_at!: NaiveDateTime\",\n ls.listen_port \"listen_port!: u32\",\n ls.persistent_keepalive_interval \"persistent_keepalive_interval?: u16\"\n FROM location_stats ls\n LEFT JOIN prev_download pd\n WHERE ls.location_id = $1\n AND (pd.download IS NULL OR ls.download != pd.download)\n ORDER BY ls.collected_at DESC\n LIMIT 1", + "query": "WITH prev_download AS (\n SELECT download\n FROM location_stats\n WHERE location_id = $1\n ORDER BY collected_at DESC\n LIMIT 1 OFFSET 1\n )\n SELECT ls.id \"id!: i64\",\n ls.location_id,\n ls.upload \"upload!: i64\",\n ls.download \"download!: i64\",\n ls.upload_diff,\n ls.download_diff,\n ls.last_handshake,\n ls.collected_at \"collected_at!: NaiveDateTime\",\n ls.listen_port \"listen_port!: u32\",\n ls.persistent_keepalive_interval \"persistent_keepalive_interval?: u16\"\n FROM location_stats ls\n LEFT JOIN prev_download pd\n WHERE ls.location_id = $1\n AND (pd.download IS NULL OR ls.download != pd.download)\n ORDER BY ls.collected_at DESC\n LIMIT 1", "describe": { "columns": [ { @@ -24,23 +24,33 @@ "type_info": "Integer" }, { - "name": "last_handshake", + "name": "upload_diff", "ordinal": 4, "type_info": "Integer" }, { - "name": "collected_at!: NaiveDateTime", + "name": "download_diff", "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "last_handshake", + "ordinal": 6, + "type_info": "Integer" + }, + { + "name": "collected_at!: NaiveDateTime", + "ordinal": 7, "type_info": "Datetime" }, { "name": "listen_port!: u32", - "ordinal": 6, + "ordinal": 8, "type_info": "Integer" }, { "name": "persistent_keepalive_interval?: u16", - "ordinal": 7, + "ordinal": 9, "type_info": "Integer" } ], @@ -55,8 +65,10 @@ false, false, false, + false, + false, true ] }, - "hash": "7f97fe602e8d1c75f79d88abd2b20eca881e3d8682aaff17b2342f282e3916bf" + "hash": "5aafcc34860d0cb6412a0ae1d94f7372ff389f8afc11d21baddc9aabe07692be" } diff --git a/src-tauri/.sqlx/query-abded0999cc848a4baaad2e57a3247e9d7c7062bc43c84d3405a19c94006b8f8.json b/src-tauri/.sqlx/query-7a39250f44f7415c09362f62efcc0b2eded31017ac7c2a531a4917db5100bfda.json similarity index 51% rename from src-tauri/.sqlx/query-abded0999cc848a4baaad2e57a3247e9d7c7062bc43c84d3405a19c94006b8f8.json rename to src-tauri/.sqlx/query-7a39250f44f7415c09362f62efcc0b2eded31017ac7c2a531a4917db5100bfda.json index 5a0b6593a..e0f1523f2 100644 --- a/src-tauri/.sqlx/query-abded0999cc848a4baaad2e57a3247e9d7c7062bc43c84d3405a19c94006b8f8.json +++ b/src-tauri/.sqlx/query-7a39250f44f7415c09362f62efcc0b2eded31017ac7c2a531a4917db5100bfda.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO location_stats (location_id, upload, download, last_handshake, collected_at, listen_port, persistent_keepalive_interval) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id \"id!\"", + "query": "INSERT INTO location_stats (location_id, upload, download, upload_diff, download_diff, last_handshake, collected_at, listen_port, persistent_keepalive_interval) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id \"id!\"", "describe": { "columns": [ { @@ -10,11 +10,11 @@ } ], "parameters": { - "Right": 7 + "Right": 9 }, "nullable": [ true ] }, - "hash": "abded0999cc848a4baaad2e57a3247e9d7c7062bc43c84d3405a19c94006b8f8" + "hash": "7a39250f44f7415c09362f62efcc0b2eded31017ac7c2a531a4917db5100bfda" } diff --git a/src-tauri/.sqlx/query-17a3b10441e63bde5b97c861747617eadddbc843e42545860afdd8ad6951b4ee.json b/src-tauri/.sqlx/query-996cf1bb9cab0f82963a519f0991a5a19ee3e5d2729c16b26c5c7ce9d73a5b5b.json similarity index 52% rename from src-tauri/.sqlx/query-17a3b10441e63bde5b97c861747617eadddbc843e42545860afdd8ad6951b4ee.json rename to src-tauri/.sqlx/query-996cf1bb9cab0f82963a519f0991a5a19ee3e5d2729c16b26c5c7ce9d73a5b5b.json index 4c0ace380..dc9f97715 100644 --- a/src-tauri/.sqlx/query-17a3b10441e63bde5b97c861747617eadddbc843e42545860afdd8ad6951b4ee.json +++ b/src-tauri/.sqlx/query-996cf1bb9cab0f82963a519f0991a5a19ee3e5d2729c16b26c5c7ce9d73a5b5b.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO tunnel_stats (tunnel_id, upload, download, last_handshake, collected_at, listen_port, persistent_keepalive_interval) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id \"id!\"", + "query": "INSERT INTO tunnel_stats (tunnel_id, upload, download, upload_diff, download_diff, last_handshake, collected_at, listen_port, persistent_keepalive_interval) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id \"id!\"", "describe": { "columns": [ { @@ -10,11 +10,11 @@ } ], "parameters": { - "Right": 7 + "Right": 9 }, "nullable": [ true ] }, - "hash": "17a3b10441e63bde5b97c861747617eadddbc843e42545860afdd8ad6951b4ee" + "hash": "996cf1bb9cab0f82963a519f0991a5a19ee3e5d2729c16b26c5c7ce9d73a5b5b" } diff --git a/src-tauri/.sqlx/query-a694907453e48370dfc3f4de499d03dc47fa9fdd542af8c85764628f92133040.json b/src-tauri/.sqlx/query-a694907453e48370dfc3f4de499d03dc47fa9fdd542af8c85764628f92133040.json deleted file mode 100644 index e3f28c60e..000000000 --- a/src-tauri/.sqlx/query-a694907453e48370dfc3f4de499d03dc47fa9fdd542af8c85764628f92133040.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "db_name": "SQLite", - "query": "WITH cte AS (SELECT id, location_id, COALESCE(upload - LAG(upload) OVER (PARTITION BY location_id ORDER BY collected_at), 0) upload, COALESCE(download - LAG(download) OVER (PARTITION BY location_id ORDER BY collected_at), 0) download, last_handshake, strftime($1, collected_at) collected_at, listen_port, persistent_keepalive_interval FROM location_stats ORDER BY collected_at LIMIT -1 OFFSET 1) SELECT id, location_id, SUM(MAX(upload, 0)) \"upload!: i64\", SUM(MAX(download, 0)) \"download!: i64\", last_handshake, collected_at \"collected_at!: NaiveDateTime\", listen_port \"listen_port!: u32\", persistent_keepalive_interval \"persistent_keepalive_interval?: u16\" FROM cte WHERE location_id = $2 AND collected_at >= $3 GROUP BY collected_at ORDER BY collected_at LIMIT $4", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "location_id", - "ordinal": 1, - "type_info": "Integer" - }, - { - "name": "upload!: i64", - "ordinal": 2, - "type_info": "Null" - }, - { - "name": "download!: i64", - "ordinal": 3, - "type_info": "Null" - }, - { - "name": "last_handshake", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "collected_at!: NaiveDateTime", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "listen_port!: u32", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "persistent_keepalive_interval?: u16", - "ordinal": 7, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 4 - }, - "nullable": [ - false, - false, - true, - true, - false, - true, - false, - true - ] - }, - "hash": "a694907453e48370dfc3f4de499d03dc47fa9fdd542af8c85764628f92133040" -} diff --git a/src-tauri/.sqlx/query-a87e237432cfd0bc54575c4b5f6ff1283fadfa9f50ac1de75fa283be52e970a1.json b/src-tauri/.sqlx/query-a87e237432cfd0bc54575c4b5f6ff1283fadfa9f50ac1de75fa283be52e970a1.json new file mode 100644 index 000000000..eee6229fe --- /dev/null +++ b/src-tauri/.sqlx/query-a87e237432cfd0bc54575c4b5f6ff1283fadfa9f50ac1de75fa283be52e970a1.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "SELECT id \"id!\", location_id,\n SUM(MAX(upload_diff, 0)) \"upload!: i64\", SUM(MAX(download_diff, 0)) \"download!: i64\", 0 \"upload_diff!: i64\", 0 \"download_diff!: i64\", last_handshake \"last_handshake!: i64\", strftime($1, collected_at) \"collected_at!: NaiveDateTime\", listen_port \"listen_port!: u32\", persistent_keepalive_interval \"persistent_keepalive_interval?: u16\" FROM location_stats WHERE location_id = $2 AND collected_at >= datetime(strftime($1, $3)) GROUP BY strftime($1, collected_at) ORDER BY collected_at LIMIT $4", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "location_id", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "upload!: i64", + "ordinal": 2, + "type_info": "Null" + }, + { + "name": "download!: i64", + "ordinal": 3, + "type_info": "Null" + }, + { + "name": "upload_diff!: i64", + "ordinal": 4, + "type_info": "Null" + }, + { + "name": "download_diff!: i64", + "ordinal": 5, + "type_info": "Null" + }, + { + "name": "last_handshake!: i64", + "ordinal": 6, + "type_info": "Integer" + }, + { + "name": "collected_at!: NaiveDateTime", + "ordinal": 7, + "type_info": "Null" + }, + { + "name": "listen_port!: u32", + "ordinal": 8, + "type_info": "Integer" + }, + { + "name": "persistent_keepalive_interval?: u16", + "ordinal": 9, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 4 + }, + "nullable": [ + true, + false, + null, + null, + null, + null, + false, + null, + false, + true + ] + }, + "hash": "a87e237432cfd0bc54575c4b5f6ff1283fadfa9f50ac1de75fa283be52e970a1" +} diff --git a/src-tauri/.sqlx/query-8e5e1ade31ec88a0e75eb5d8aa86aa8f73e244f6d3128b255a367ad19f534721.json b/src-tauri/.sqlx/query-ac0b93f9b64aa7f17824d4a55c714d8834ce46dacc032d2a900c36cd7c2b6a1e.json similarity index 62% rename from src-tauri/.sqlx/query-8e5e1ade31ec88a0e75eb5d8aa86aa8f73e244f6d3128b255a367ad19f534721.json rename to src-tauri/.sqlx/query-ac0b93f9b64aa7f17824d4a55c714d8834ce46dacc032d2a900c36cd7c2b6a1e.json index 44220f462..b953e7983 100644 --- a/src-tauri/.sqlx/query-8e5e1ade31ec88a0e75eb5d8aa86aa8f73e244f6d3128b255a367ad19f534721.json +++ b/src-tauri/.sqlx/query-ac0b93f9b64aa7f17824d4a55c714d8834ce46dacc032d2a900c36cd7c2b6a1e.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "WITH prev_download AS (\n SELECT download\n FROM tunnel_stats\n WHERE tunnel_id = $1\n ORDER BY collected_at DESC\n LIMIT 1 OFFSET 1\n )\n SELECT ts.id \"id!: i64\",\n ts.tunnel_id,\n ts.upload \"upload!: i64\",\n ts.download \"download!: i64\",\n ts.last_handshake,\n ts.collected_at \"collected_at!: NaiveDateTime\",\n ts.listen_port \"listen_port!: u32\",\n ts.persistent_keepalive_interval \"persistent_keepalive_interval!: u16\"\n FROM tunnel_stats ts\n LEFT JOIN prev_download pd\n WHERE ts.tunnel_id = $1\n AND (pd.download IS NULL OR ts.download != pd.download)\n ORDER BY ts.collected_at DESC\n LIMIT 1", + "query": "WITH prev_download AS (\n SELECT download\n FROM tunnel_stats\n WHERE tunnel_id = $1\n ORDER BY collected_at DESC\n LIMIT 1 OFFSET 1\n )\n SELECT ts.id \"id!: i64\",\n ts.tunnel_id,\n ts.upload \"upload!: i64\",\n ts.download \"download!: i64\",\n ts.upload_diff,\n ts.download_diff,\n ts.last_handshake,\n ts.collected_at \"collected_at!: NaiveDateTime\",\n ts.listen_port \"listen_port!: u32\",\n ts.persistent_keepalive_interval \"persistent_keepalive_interval!: u16\"\n FROM tunnel_stats ts\n LEFT JOIN prev_download pd\n WHERE ts.tunnel_id = $1\n AND (pd.download IS NULL OR ts.download != pd.download)\n ORDER BY ts.collected_at DESC\n LIMIT 1", "describe": { "columns": [ { @@ -24,23 +24,33 @@ "type_info": "Integer" }, { - "name": "last_handshake", + "name": "upload_diff", "ordinal": 4, "type_info": "Integer" }, { - "name": "collected_at!: NaiveDateTime", + "name": "download_diff", "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "last_handshake", + "ordinal": 6, + "type_info": "Integer" + }, + { + "name": "collected_at!: NaiveDateTime", + "ordinal": 7, "type_info": "Datetime" }, { "name": "listen_port!: u32", - "ordinal": 6, + "ordinal": 8, "type_info": "Integer" }, { "name": "persistent_keepalive_interval!: u16", - "ordinal": 7, + "ordinal": 9, "type_info": "Integer" } ], @@ -48,6 +58,8 @@ "Right": 1 }, "nullable": [ + true, + false, false, false, false, @@ -58,5 +70,5 @@ false ] }, - "hash": "8e5e1ade31ec88a0e75eb5d8aa86aa8f73e244f6d3128b255a367ad19f534721" + "hash": "ac0b93f9b64aa7f17824d4a55c714d8834ce46dacc032d2a900c36cd7c2b6a1e" } diff --git a/src-tauri/.sqlx/query-db08146110d6df2759b34328c8ac6f87350d379fce3541ea43ed3519b5757d72.json b/src-tauri/.sqlx/query-db08146110d6df2759b34328c8ac6f87350d379fce3541ea43ed3519b5757d72.json deleted file mode 100644 index 3bc260a50..000000000 --- a/src-tauri/.sqlx/query-db08146110d6df2759b34328c8ac6f87350d379fce3541ea43ed3519b5757d72.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "db_name": "SQLite", - "query": "WITH cte AS (SELECT id, tunnel_id, COALESCE(upload - LAG(upload) OVER (PARTITION BY tunnel_id ORDER BY collected_at), 0) upload, COALESCE(download - LAG(download) OVER (PARTITION BY tunnel_id ORDER BY collected_at), 0) download, last_handshake, strftime($1, collected_at) collected_at, listen_port, persistent_keepalive_interval FROM tunnel_stats ORDER BY collected_at LIMIT -1 OFFSET 1) SELECT id, tunnel_id, SUM(MAX(upload, 0)) \"upload!: i64\", SUM(MAX(download, 0)) \"download!: i64\", last_handshake, collected_at \"collected_at!: NaiveDateTime\", listen_port \"listen_port!: u32\", persistent_keepalive_interval \"persistent_keepalive_interval!: u16\" FROM cte WHERE tunnel_id = $2 AND collected_at >= $3 GROUP BY collected_at ORDER BY collected_at", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "tunnel_id", - "ordinal": 1, - "type_info": "Integer" - }, - { - "name": "upload!: i64", - "ordinal": 2, - "type_info": "Null" - }, - { - "name": "download!: i64", - "ordinal": 3, - "type_info": "Null" - }, - { - "name": "last_handshake", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "collected_at!: NaiveDateTime", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "listen_port!: u32", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "persistent_keepalive_interval!: u16", - "ordinal": 7, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 3 - }, - "nullable": [ - false, - false, - true, - true, - false, - true, - false, - false - ] - }, - "hash": "db08146110d6df2759b34328c8ac6f87350d379fce3541ea43ed3519b5757d72" -} diff --git a/src-tauri/core/src/database/models/location_stats.rs b/src-tauri/core/src/database/models/location_stats.rs index aad5f3255..36d57432b 100644 --- a/src-tauri/core/src/database/models/location_stats.rs +++ b/src-tauri/core/src/database/models/location_stats.rs @@ -14,6 +14,8 @@ pub struct LocationStats { pub location_id: Id, upload: i64, download: i64, + upload_diff: i64, + download_diff: i64, pub last_handshake: i64, pub collected_at: NaiveDateTime, listen_port: u32, @@ -86,6 +88,8 @@ impl LocationStats { location_id, upload, download, + upload_diff: 0, + download_diff: 0, last_handshake, collected_at: Utc::now().naive_utc(), listen_port, @@ -93,18 +97,27 @@ impl LocationStats { } } + #[must_use] + pub fn with_diffs(mut self, upload_diff: i64, download_diff: i64) -> Self { + self.upload_diff = upload_diff; + self.download_diff = download_diff; + self + } + pub async fn save<'e, E>(self, executor: E) -> sqlx::Result> where E: SqliteExecutor<'e>, { let id = query_scalar!( - "INSERT INTO location_stats (location_id, upload, download, last_handshake, \ - collected_at, listen_port, persistent_keepalive_interval) \ - VALUES ($1, $2, $3, $4, $5, $6, $7) \ + "INSERT INTO location_stats (location_id, upload, download, upload_diff, download_diff, \ + last_handshake, collected_at, listen_port, persistent_keepalive_interval) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) \ RETURNING id \"id!\"", self.location_id, self.upload, self.download, + self.upload_diff, + self.download_diff, self.last_handshake, self.collected_at, self.listen_port, @@ -118,6 +131,8 @@ impl LocationStats { location_id: self.location_id, upload: self.upload, download: self.download, + upload_diff: self.upload_diff, + download_diff: self.download_diff, last_handshake: self.last_handshake, collected_at: self.collected_at, listen_port: self.listen_port, @@ -143,21 +158,18 @@ impl LocationStats { let query_limit = limit.unwrap_or(-1); let stats = query_as!( LocationStats, - "WITH cte AS (\ - SELECT id, location_id, \ - COALESCE(upload - LAG(upload) OVER (PARTITION BY location_id ORDER BY collected_at), 0) upload, \ - COALESCE(download - LAG(download) OVER (PARTITION BY location_id ORDER BY collected_at), 0) download, \ - last_handshake, strftime($1, collected_at) collected_at, listen_port, persistent_keepalive_interval \ - FROM location_stats ORDER BY collected_at LIMIT -1 OFFSET 1) \ - SELECT id, location_id, \ - SUM(MAX(upload, 0)) \"upload!: i64\", \ - SUM(MAX(download, 0)) \"download!: i64\", \ - last_handshake, \ - collected_at \"collected_at!: NaiveDateTime\", \ - listen_port \"listen_port!: u32\", \ - persistent_keepalive_interval \"persistent_keepalive_interval?: u16\" \ - FROM cte WHERE location_id = $2 AND collected_at >= $3 \ - GROUP BY collected_at ORDER BY collected_at LIMIT $4", + "SELECT id \"id!\", location_id, + SUM(MAX(upload_diff, 0)) \"upload!: i64\", \ + SUM(MAX(download_diff, 0)) \"download!: i64\", \ + 0 \"upload_diff!: i64\", \ + 0 \"download_diff!: i64\", \ + last_handshake \"last_handshake!: i64\", \ + strftime($1, collected_at) \"collected_at!: NaiveDateTime\", \ + listen_port \"listen_port!: u32\", \ + persistent_keepalive_interval \"persistent_keepalive_interval?: u16\" \ + FROM location_stats \ + WHERE location_id = $2 AND collected_at >= datetime(strftime($1, $3)) \ + GROUP BY strftime($1, collected_at) ORDER BY collected_at LIMIT $4", aggregation, location_id, from, @@ -188,6 +200,8 @@ impl LocationStats { ls.location_id, ls.upload \"upload!: i64\", ls.download \"download!: i64\", + ls.upload_diff, + ls.download_diff, ls.last_handshake, ls.collected_at \"collected_at!: NaiveDateTime\", ls.listen_port \"listen_port!: u32\", diff --git a/src-tauri/core/src/database/models/mod.rs b/src-tauri/core/src/database/models/mod.rs index 449de17a2..429c8434d 100644 --- a/src-tauri/core/src/database/models/mod.rs +++ b/src-tauri/core/src/database/models/mod.rs @@ -14,7 +14,7 @@ pub type Id = i64; #[derive(Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct NoId; -const PURGE_DURATION: chrono::Duration = chrono::Duration::days(30); +const PURGE_DURATION: chrono::Duration = chrono::Duration::hours(28); #[cfg(target_os = "macos")] use self::{location::Location, tunnel::Tunnel}; diff --git a/src-tauri/core/src/database/models/tunnel.rs b/src-tauri/core/src/database/models/tunnel.rs index fc0cc7dfd..03f47b0ba 100644 --- a/src-tauri/core/src/database/models/tunnel.rs +++ b/src-tauri/core/src/database/models/tunnel.rs @@ -281,6 +281,8 @@ pub struct TunnelStats { pub tunnel_id: Id, upload: i64, download: i64, + upload_diff: i64, + download_diff: i64, pub last_handshake: i64, pub collected_at: NaiveDateTime, listen_port: u32, @@ -314,6 +316,8 @@ impl TunnelStats { tunnel_id, upload, download, + upload_diff: 0, + download_diff: 0, last_handshake, collected_at, listen_port, @@ -321,17 +325,26 @@ impl TunnelStats { } } + #[must_use] + pub fn with_diffs(mut self, upload_diff: i64, download_diff: i64) -> Self { + self.upload_diff = upload_diff; + self.download_diff = download_diff; + self + } + pub async fn save<'e, E>(self, executor: E) -> sqlx::Result> where E: SqliteExecutor<'e>, { let id = query_scalar!( - "INSERT INTO tunnel_stats (tunnel_id, upload, download, last_handshake, collected_at, \ - listen_port, persistent_keepalive_interval) \ - VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id \"id!\"", + "INSERT INTO tunnel_stats (tunnel_id, upload, download, upload_diff, download_diff, \ + last_handshake, collected_at, listen_port, persistent_keepalive_interval) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id \"id!\"", self.tunnel_id, self.upload, self.download, + self.upload_diff, + self.download_diff, self.last_handshake, self.collected_at, self.listen_port, @@ -345,6 +358,8 @@ impl TunnelStats { tunnel_id: self.tunnel_id, upload: self.upload, download: self.download, + upload_diff: self.upload_diff, + download_diff: self.download_diff, last_handshake: self.last_handshake, collected_at: self.collected_at, listen_port: self.listen_port, @@ -366,20 +381,18 @@ impl TunnelStats { let aggregation = aggregation.fstring(); let stats = query_as!( TunnelStats, - "WITH cte AS (\ - SELECT id, tunnel_id, \ - COALESCE(upload - LAG(upload) OVER (PARTITION BY tunnel_id ORDER BY collected_at), 0) upload, \ - COALESCE(download - LAG(download) OVER (PARTITION BY tunnel_id ORDER BY collected_at), 0) download, \ - last_handshake, strftime($1, collected_at) collected_at, listen_port, persistent_keepalive_interval \ - FROM tunnel_stats ORDER BY collected_at LIMIT -1 OFFSET 1) \ - SELECT id, tunnel_id, \ - SUM(MAX(upload, 0)) \"upload!: i64\", \ - SUM(MAX(download, 0)) \"download!: i64\", \ - last_handshake, collected_at \"collected_at!: NaiveDateTime\", \ + "SELECT id \"id!\", tunnel_id, \ + SUM(MAX(upload_diff, 0)) \"upload!: i64\", \ + SUM(MAX(download_diff, 0)) \"download!: i64\", \ + 0 \"upload_diff!: i64\", \ + 0 \"download_diff!: i64\", \ + last_handshake \"last_handshake!: i64\", \ + strftime($1, collected_at) \"collected_at!: NaiveDateTime\", \ listen_port \"listen_port!: u32\", \ persistent_keepalive_interval \"persistent_keepalive_interval!: u16\" \ - FROM cte WHERE tunnel_id = $2 AND collected_at >= $3 \ - GROUP BY collected_at ORDER BY collected_at", + FROM tunnel_stats \ + WHERE tunnel_id = $2 AND collected_at >= datetime(strftime($1, $3)) \ + GROUP BY strftime($1, collected_at) ORDER BY collected_at", aggregation, tunnel_id, from @@ -409,6 +422,8 @@ impl TunnelStats { ts.tunnel_id, ts.upload \"upload!: i64\", ts.download \"download!: i64\", + ts.upload_diff, + ts.download_diff, ts.last_handshake, ts.collected_at \"collected_at!: NaiveDateTime\", ts.listen_port \"listen_port!: u32\", @@ -456,6 +471,8 @@ where tunnel_id: tunnel.id, upload: peer.tx_bytes.cast_signed(), download: peer.rx_bytes.cast_signed(), + upload_diff: 0, + download_diff: 0, last_handshake: peer.last_handshake.map_or(0, |ts| { ts.duration_since(SystemTime::UNIX_EPOCH) .map_or(0, |duration| duration.as_secs().cast_signed()) diff --git a/src-tauri/migrations/20260729120000_add_stats_diffs.sql b/src-tauri/migrations/20260729120000_add_stats_diffs.sql new file mode 100644 index 000000000..8138563ef --- /dev/null +++ b/src-tauri/migrations/20260729120000_add_stats_diffs.sql @@ -0,0 +1,11 @@ +ALTER TABLE location_stats ADD COLUMN upload_diff INTEGER NOT NULL DEFAULT 0; +ALTER TABLE location_stats ADD COLUMN download_diff INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE tunnel_stats ADD COLUMN upload_diff BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tunnel_stats ADD COLUMN download_diff BIGINT NOT NULL DEFAULT 0; + +CREATE INDEX idx_location_stats_location_collected + ON location_stats (location_id, collected_at); + +CREATE INDEX idx_tunnel_stats_tunnel_collected + ON tunnel_stats (tunnel_id, collected_at); diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 876ac00cc..fc1d58991 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,7 +1,7 @@ -#[cfg(not(target_os = "macos"))] -use std::str::FromStr; #[cfg(target_os = "macos")] use std::time::Duration; +#[cfg(not(target_os = "macos"))] +use std::{collections::HashMap, str::FromStr}; use std::{env, process::Command}; #[cfg(target_os = "linux")] use std::{fs, path::Path}; @@ -55,6 +55,15 @@ use crate::{ #[cfg(target_os = "macos")] static TUNNEL_START_DELAY: Duration = Duration::from_secs(1); +fn stats_diffs(previous_totals: Option<(i64, i64)>, current_totals: (i64, i64)) -> (i64, i64) { + previous_totals.map_or((0, 0), |(previous_upload, previous_download)| { + ( + current_totals.0.saturating_sub(previous_upload).max(0), + current_totals.1.saturating_sub(previous_download).max(0), + ) + }) +} + #[cfg(target_os = "linux")] const NVIDIA_EXPLICIT_SYNC_ENV: &str = "__NV_DISABLE_EXPLICIT_SYNC"; #[cfg(target_os = "linux")] @@ -126,6 +135,7 @@ pub(crate) async fn stats_handler(id: Id, connection_type: ConnectionType) { let mut interval = tokio::time::interval(CHECK_INTERVAL); let pool = DB_POOL.clone(); + let mut previous_totals = None; loop { debug!("Waiting for the next stats collection interval for ID {id} and connection type {connection_type:?}"); @@ -135,6 +145,8 @@ pub(crate) async fn stats_handler(id: Id, connection_type: ConnectionType) { let Some(stats) = stats else { continue; }; + let current_totals = (stats.tx_bytes.cast_signed(), stats.rx_bytes.cast_signed()); + let (upload_diff, download_diff) = stats_diffs(previous_totals, current_totals); let mut transaction = match pool.begin().await { Ok(transactions) => transactions, @@ -145,6 +157,7 @@ pub(crate) async fn stats_handler(id: Id, connection_type: ConnectionType) { continue; } }; + let mut saved = false; if connection_type == ConnectionType::Location { let location_stats = LocationStats::new( @@ -154,10 +167,12 @@ pub(crate) async fn stats_handler(id: Id, connection_type: ConnectionType) { stats.last_handshake.cast_signed(), 0, None, - ); + ) + .with_diffs(upload_diff, download_diff); match location_stats.save(&mut *transaction).await { Ok(_) => { debug!("Saved network usage stats for location ID {id}"); + saved = true; } Err(err) => { error!("Failed to save network usage stats for location ID {id}: {err}"); @@ -172,10 +187,12 @@ pub(crate) async fn stats_handler(id: Id, connection_type: ConnectionType) { chrono::Utc::now().naive_utc(), 0, 0, - ); + ) + .with_diffs(upload_diff, download_diff); match tunnel_stats.save(&mut *transaction).await { Ok(_) => { debug!("Saved network usage stats for tunnel ID {id}"); + saved = true; } Err(err) => { error!("Failed to save network usage stats for tunnel ID {id}: {err}"); @@ -185,6 +202,8 @@ pub(crate) async fn stats_handler(id: Id, connection_type: ConnectionType) { if let Err(err) = transaction.commit().await { error!("Failed to commit database transaction for saving location/tunnel stats: {err}"); + } else if saved { + previous_totals = Some(current_totals); } } } @@ -201,6 +220,7 @@ pub(crate) async fn stats_handler(interface_name: String, connection_type: Conne .await .expect("Failed to connect to interface stats stream for interface {interface_name}") .into_inner(); + let mut previous_totals = HashMap::new(); loop { match stream.message().await { @@ -223,8 +243,10 @@ pub(crate) async fn stats_handler(interface_name: String, connection_type: Conne .peers .into_iter() .map(Into::into) - .collect::>(); + .collect::>(); + let mut pending_totals = Vec::new(); for peer in peers { + let current_totals = (peer.tx_bytes.cast_signed(), peer.rx_bytes.cast_signed()); if connection_type.eq(&ConnectionType::Location) { let location_stats = match peer_to_location_stats( &peer, @@ -249,9 +271,14 @@ pub(crate) async fn stats_handler(interface_name: String, connection_type: Conne (interface {interface_name})." ); trace!("Stats: {location_stats:?}"); + let location_id = location_stats.location_id; + let (upload_diff, download_diff) = + stats_diffs(previous_totals.get(&location_id).copied(), current_totals); + let location_stats = location_stats.with_diffs(upload_diff, download_diff); match location_stats.save(&mut *transaction).await { Ok(_) => { debug!("Saved network usage stats for location {location_name}"); + pending_totals.push((location_id, current_totals)); } Err(err) => { error!( @@ -282,9 +309,14 @@ pub(crate) async fn stats_handler(interface_name: String, connection_type: Conne "Saving network usage stats related to tunnel {tunnel_name} \ (interface {interface_name}): {tunnel_stats:?}" ); + let tunnel_id = tunnel_stats.tunnel_id; + let (upload_diff, download_diff) = + stats_diffs(previous_totals.get(&tunnel_id).copied(), current_totals); + let tunnel_stats = tunnel_stats.with_diffs(upload_diff, download_diff); match tunnel_stats.save(&mut *transaction).await { Ok(_) => { debug!("Saved stats for tunnel {tunnel_name}"); + pending_totals.push((tunnel_id, current_totals)); } Err(err) => { error!("Failed to save stats for tunnel {tunnel_name}: {err}"); @@ -299,6 +331,8 @@ pub(crate) async fn stats_handler(interface_name: String, connection_type: Conne "Failed to commit database transaction for saving location/tunnel stats: \ {err}", ); + } else { + previous_totals.extend(pending_totals); } } Ok(None) => { @@ -924,3 +958,23 @@ pub async fn sync_connections(app_handle: &AppHandle) -> Result<(), Error> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::stats_diffs; + + #[test] + fn stats_diffs_returns_zero_for_first_sample() { + assert_eq!(stats_diffs(None, (100, 200)), (0, 0)); + } + + #[test] + fn stats_diffs_returns_counter_increments() { + assert_eq!(stats_diffs(Some((100, 200)), (150, 275)), (50, 75)); + } + + #[test] + fn stats_diffs_clamps_counter_resets_to_zero() { + assert_eq!(stats_diffs(Some((100, 200)), (50, 125)), (0, 0)); + } +}