From 4f49b16079b27d7ce633619e4cddfdf2bffbc952 Mon Sep 17 00:00:00 2001 From: cashmere Date: Fri, 4 Sep 2026 09:05:24 +0200 Subject: initial commit --- src/stat.c | 1266 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1266 insertions(+) create mode 100644 src/stat.c (limited to 'src/stat.c') diff --git a/src/stat.c b/src/stat.c new file mode 100644 index 0000000..2e33010 --- /dev/null +++ b/src/stat.c @@ -0,0 +1,1266 @@ +/* +** Copyright (c) 2007 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) + +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file contains code to implement the stat web page +** +*/ +#include "VERSION.h" +#include "config.h" +#include +#include "stat.h" + +/* +** For a sufficiently large integer, provide an alternative +** representation as MB or GB or TB. +*/ +void bigSizeName(int nOut, char *zOut, sqlite3_int64 v){ + if( v<100000 ){ + sqlite3_snprintf(nOut, zOut, "%,lld bytes", v); + }else if( v<1000000000 ){ + sqlite3_snprintf(nOut, zOut, "%,lld bytes (%.1fMB)", + v, (double)v/1000000.0); + }else{ + sqlite3_snprintf(nOut, zOut, "%,lld bytes (%.1fGB)", + v, (double)v/1000000000.0); + } +} + +/* +** Return the approximate size as KB, MB, GB, or TB. +*/ +void approxSizeName(int nOut, char *zOut, sqlite3_int64 v){ + if( v<1000 ){ + sqlite3_snprintf(nOut, zOut, "%,lld bytes", v); + }else if( v<1000000 ){ + sqlite3_snprintf(nOut, zOut, "%.1fKB", (double)v/1000.0); + }else if( v<1000000000 ){ + sqlite3_snprintf(nOut, zOut, "%.1fMB", (double)v/1000000.0); + }else{ + sqlite3_snprintf(nOut, zOut, "%.1fGB", (double)v/1000000000.0); + } +} + +/* +** Generate stats for the email notification subsystem. +*/ +void stats_for_email(void){ + const char *zDest = db_get("email-send-method",0); + int nSub, nASub, nPend, nDPend; + const char *zDir, *zDb, *zCmd, *zRelay; + int iCutoff; + double rDigest; + @ Outgoing Email: + if( fossil_strcmp(zDest,"pipe")==0 + && (zCmd = db_get("email-send-command",0))!=0 + ){ + @ Piped to command "%h(zCmd)" + }else + if( fossil_strcmp(zDest,"db")==0 + && (zDb = db_get("email-send-db",0))!=0 + ){ + sqlite3 *db; + sqlite3_stmt *pStmt; + int rc; + @ Queued to database "%h(zDb)" + g.dbIgnoreErrors++; + rc = sqlite3_open(zDb, &db); + if( rc==SQLITE_OK ){ + rc = sqlite3_prepare_v2(db, "SELECT count(*) FROM email",-1,&pStmt,0); + if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ + @ (%,d(sqlite3_column_int(pStmt,0)) messages, + @ %,d(file_size(zDb,ExtFILE)) bytes) + } + sqlite3_finalize(pStmt); + } + g.dbIgnoreErrors--; + if( rc ){ + @ ← cannot access database! + } + sqlite3_close(db); + }else + if( fossil_strcmp(zDest,"dir")==0 + && (zDir = db_get("email-send-dir",0))!=0 + ){ + @ Written to files in "%h(zDir)" + @ (%,d(file_directory_list(zDir,0,1,0,0)) messages) + }else + if( fossil_strcmp(zDest,"relay")==0 + && (zRelay = db_get("email-send-relayhost",0))!=0 + ){ + @ Relay to %h(zRelay) using SMTP + } + else{ + @ Off + } + @ + nPend = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep"); + nDPend = db_int(0,"SELECT count(*) FROM pending_alert" + " WHERE NOT sentDigest"); + @ Pending Alerts: + @ %,d(nPend) normal, %,d(nDPend) digest + @ + if( g.perm.Admin ){ + @ Subscribers: + }else{ + @ Subscribers: + } + nSub = db_int(0, "SELECT count(*) FROM subscriber"); + iCutoff = db_get_int("email-renew-cutoff",0); + nASub = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified" + " AND NOT sdonotcall AND octet_length(ssub)>1" + " AND lastContact>=%d;", iCutoff); + @ %,d(nASub) active, %,d(nSub) total + @ + rDigest = db_double(-1.0, "SELECT (julianday('now') - value)*24.0" + " FROM config WHERE name='email-last-digest'"); + if( rDigest>0.0 ){ + @ Last Digest:Approximately \ + if( rDigest>48.0 ){ + @ %.1f(rDigest/24.0) days ago + }else{ + @ %.1f(rDigest) hours ago + } + } +} + +/* +** WEBPAGE: stat +** +** Show statistics and global information about the repository. +*/ +void stat_page(void){ + i64 t, fsize; + int n, m; + int szMax, szAvg; + int brief; + const char *p; + char *z; + int Y, M, D; + + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + brief = P("brief")!=0; + style_header("Repository Statistics"); + style_adunit_config(ADUNIT_RIGHT_OK); + if( g.perm.Admin ){ + style_submenu_element("URLs", "urllist"); + style_submenu_element("Schema", "repo_schema"); + style_submenu_element("Web-Cache", "cachestat"); + } + style_submenu_element("Activity Reports", "reports"); + style_submenu_element("Hash Collisions", "hash-collisions"); + style_submenu_element("Artifacts", "bloblist"); + if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){ + style_submenu_element("Table Sizes", "repo-tabsize"); + } + if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){ + style_submenu_element("Environment", "test-env"); + } + @ + fsize = file_size(g.zRepositoryName, ExtFILE); + @ + @ + if( !brief ){ + @ + if( n>0 ){ + int a, b; + Stmt q; + @ + @ + } + { /* Unversioned Files stat (if any) */ + sqlite3_int64 iStored = unversioned_stat(&n); + if( n>0 ){ + sqlite3_int64 pct; + char zStored[100]; + pct = (iStored*100 + fsize/2)/fsize; + approxSizeName(sizeof(zStored), zStored, iStored); + @ + } + } + @ + @ + @ + if( g.perm.Chat && db_table_exists("repository","chat") ){ + sqlite3_int64 sz = 0; + char zSz[100]; + n = db_int(0, "SELECT max(msgid) FROM chat"); + m = db_int(0, "SELECT count(*) FROM chat WHERE mdel IS NOT TRUE"); + sz = db_int64(0, "SELECT sum(coalesce(octet_length(xmsg),0)+" + "coalesce(octet_length(file),0)) FROM chat"); + approxSizeName(sizeof(zSz), zSz, sz); + @ + @ + } + n = db_int(0, "SELECT count(*) FROM tag /*scan*/" + " WHERE +tagname GLOB 'tkt-*'"); + if( n>0 ){ + @ + } + if( db_table_exists("repository","forumpost") ){ + n = db_int(0, "SELECT count(*) FROM forumpost/*scan*/"); + if( n>0 ){ + int nThread = db_int(0, "SELECT count(*) FROM forumpost" + " WHERE froot=fpid"); + @ + @ + } + } + } + @ + p = db_get("project-code", 0); + if( p ){ + @ + @ + } + p = db_get("parent-project-code", 0); + if( p ){ + @ + @ + } + /* @ */ + @ + @ + @ + if( g.perm.Admin ){ + const char *zCgi = P("SERVER_SOFTWARE"); + @ + @ + if( zCgi ){ + @ + } + } + if( g.eHashPolicy!=HPOLICY_AUTO ){ + @ + }else{ + @ + } + @ + @ + if( g.perm.Admin && g.zErrlog && g.zErrlog[0] ){ + i64 szFile = file_size(g.zErrlog, ExtFILE); + if( szFile>=0 ){ + @ + @ + } + if( g.perm.Admin ){ + @ + @ + } + if( g.perm.Admin && alert_enabled() ){ + stats_for_email(); + } + + @
Repository Size:%,lld(fsize) bytes
Number Of Artifacts: + n = db_int(0, "SELECT count(*) FROM blob WHERE content IS NOT NULL"); + m = db_int(0, "SELECT count(*) FROM delta"); + @ %,d(n) (%,d(n-m) fulltext and %,d(m) deltas) + if( g.perm.Write ){ + @ Details + } + @
Uncompressed Artifact Size: + db_prepare(&q, "SELECT total(size), avg(size), max(size)" + " FROM blob WHERE content IS NOT NULL /*scan*/"); + db_step(&q); + t = db_column_int64(&q, 0); + szAvg = db_column_int(&q, 1); + szMax = db_column_int(&q, 2); + db_finalize(&q); + @ %,d(szAvg) bytes average, %,d(szMax) bytes max, %,lld(t) total + @
Compression Ratio: + if( t/fsize < 5 ){ + b = 10; + a = t/(fsize/10); + }else{ + b = 1; + a = t/fsize; + } + @ %d(a):%d(b) + @
Unversioned Files: + @ %z(href("%R/uvlist"))%d(n) files, + @ %s(zStored) compressed, %d(pct)%% of total repository space + @
Number Of Check-ins: + n = db_int(0, "SELECT count(*) FROM event WHERE type='ci' /*scan*/"); + @ %,d(n) + @
Number Of Files: + n = db_int(0, "SELECT count(*) FROM filename /*scan*/"); + @ %,d(n) + @
Number Of Wiki Pages: + n = db_int(0, "SELECT count(*) FROM tag /*scan*/" + " WHERE +tagname GLOB 'wiki-*'"); + @ %,d(n) + @
Number Of Chat Messages:%,d(n) (%,d(m) still alive, %s(zSz) in size)
Number Of Tickets:%,d(n)
Number Of Forum Posts:%,d(n) on %d(nThread) threads
Project Age: + z = db_text(0, "SELECT timediff('now',(SELECT min(mtime) FROM event));"); + sscanf(z, "+%d-%d-%d", &Y, &M, &D); + if( Y>0 ){ + @ %d(Y) year%s(Y==1?"":"s") \ + } + if( M>0 ){ + @ %d(M) month%s(M==1?"":"s") \ + } + if( D>0 || (Y==0 && M==0) ){ + @ %d(D) day%s(D==1?"":"s") + } + @
Project ID:%h(p) %h(db_get("project-name",""))
Parent Project ID:%h(p) %h(db_get("parent-project-name",""))
Server ID:%h(db_get("server-code",""))
Fossil Version: + @ %h(MANIFEST_DATE) %h(MANIFEST_VERSION) + @ (%h(RELEASE_VERSION)) (details) + @
SQLite Version:%.19s(sqlite3_sourceid()) + @ [%.10s(&sqlite3_sourceid()[20])] (%s(sqlite3_libversion())) + @ (details)
Pikchr Version:%s(pikchr_version())
OpenSSL Version:%z(fossil_openssl_version())
Web Server:%s(zCgi)
Schema Version:%h(g.zAuxSchema), + @ %s(hpolicy_name())
Schema Version:%h(g.zAuxSchema)
Repository Rebuilt: + @ %h(db_get_mtime("rebuilt","%Y-%m-%d %H:%M:%S","Never")) + @ By Fossil %h(db_get("rebuilt","Unknown"))
Database Stats: + @ %,d(db_int(0, "PRAGMA repository.page_count")) pages, + @ %d(db_int(0, "PRAGMA repository.page_size")) bytes/page, + @ %,d(db_int(0, "PRAGMA repository.freelist_count")) free pages, + @ %s(db_text(0, "PRAGMA repository.encoding")), + @ %s(db_text(0, "PRAGMA repository.journal_mode")) mode + @
Error Log:%h(g.zErrlog) (%,lld(szFile) bytes) + } + @
Backoffice:Last run: %z(backoffice_last_run())
+ style_finish_page(); +} + +/* +** COMMAND: dbstat +** +** Usage: %fossil dbstat OPTIONS +** +** Shows statistics and global information about the repository and/or +** verify the integrity of a repository. +** +** Options: +** -b|--brief Only show essential elements +** --db-check Run "PRAGMA quick_check" on the repository database +** --db-verify Run a full verification of the repository integrity. +** This involves decoding and reparsing all artifacts +** and can take significant time. +** --omit-version-info Omit the SQLite, Fossil, Pikchr version information +*/ +void dbstat_cmd(void){ + i64 t, fsize; + int n, m; + int szMax, szAvg; + int brief; + int omitVers; /* Omit Fossil, SQLite, Pikchr version information */ + int dbCheck; /* True for the --db-check option */ + const int colWidth = -19 /* printf alignment/width for left column */; + const char *p, *z; + + brief = find_option("brief", "b",0)!=0; + omitVers = find_option("omit-version-info", 0, 0)!=0; + dbCheck = find_option("db-check",0,0)!=0; + if( find_option("db-verify",0,0)!=0 ) dbCheck = 2; + db_find_and_open_repository(0,0); + + /* We should be done with options.. */ + verify_all_options(); + + if( (z = db_get("project-name",0))!=0 + || (z = db_get("short-project-name",0))!=0 + ){ + fossil_print("%*s%s\n", colWidth, "project-name:", z); + } + fsize = file_size(g.zRepositoryName, ExtFILE); + fossil_print( "%*s%,lld bytes\n", colWidth, "repository-size:", fsize); + if( !brief ){ + n = db_int(0, "SELECT count(*) FROM blob WHERE content IS NOT NULL"); + m = db_int(0, "SELECT count(*) FROM delta"); + fossil_print("%*s%,d (stored as %,d full text and %,d deltas)\n", + colWidth, "artifact-count:", + n, n-m, m); + if( n>0 ){ + int a, b; + Stmt q; + db_prepare(&q, "SELECT total(size), avg(size), max(size)" + " FROM blob WHERE size>0"); + db_step(&q); + t = db_column_int64(&q, 0); + szAvg = db_column_int(&q, 1); + szMax = db_column_int(&q, 2); + db_finalize(&q); + fossil_print( "%*s%,d average, " + "%,d max, %,lld total\n", + colWidth, "artifact-sizes:", + szAvg, szMax, t); + if( t/fsize < 5 ){ + b = 10; + fsize /= 10; + }else{ + b = 1; + } + a = t/fsize; + fossil_print("%*s%d:%d\n", colWidth, "compression-ratio:", a, b); + } + { /* Unversioned Files stat (if any) */ + sqlite3_int64 iStored = unversioned_stat(&n); + if( n>0 ){ + sqlite3_int64 pct; + char zStored[100]; + pct = (iStored*100 + fsize/2)/fsize; + approxSizeName(sizeof(zStored), zStored, iStored); + fossil_print("%*s%d files, %s compressed, %d%% of total repository space\n", + colWidth, "unversioned-files:", n, zStored, pct); + } + } + n = db_int(0, "SELECT COUNT(*) FROM event e WHERE e.type='ci'"); + fossil_print("%*s%,d\n", colWidth, "check-ins:", n); + n = db_int(0, "SELECT count(*) FROM filename /*scan*/"); + fossil_print("%*s%,d across all branches\n", colWidth, "files:", n); + n = db_int(0, "SELECT count(*) FROM (" + "SELECT DISTINCT substr(tagname,6) " + "FROM tag JOIN tagxref USING('tagid')" + " WHERE tagname GLOB 'wiki-*'" + " AND TYPEOF(tagxref.value+0)='integer'" + ")"); + m = db_int(0, "SELECT COUNT(*) FROM event WHERE type='w'"); + fossil_print("%*s%,d (%,d changes)\n", colWidth, "wiki-pages:", n, m); + n = db_int(0, "SELECT count(*) FROM tag /*scan*/" + " WHERE tagname GLOB 'tkt-*'"); + m = db_int(0, "SELECT COUNT(*) FROM event WHERE type='t'"); + fossil_print("%*s%,d (%,d changes)\n", colWidth, "tickets:", n, m); + n = db_int(0, "SELECT COUNT(*) FROM event WHERE type='e'"); + fossil_print("%*s%,d\n", colWidth, "events:", n); + if( db_table_exists("repository","forumpost") ){ + n = db_int(0, "SELECT count(*) FROM forumpost/*scan*/"); + if( n>0 ){ + int nThread = db_int(0, "SELECT count(*) FROM forumpost" + " WHERE froot=fpid"); + fossil_print("%*s%,d (on %,d threads)\n", colWidth, "forum-posts:", + n, nThread); + } + } + n = db_int(0, "SELECT COUNT(*) FROM event WHERE type='g'"); + fossil_print("%*s%,d\n", colWidth, "tag-changes:", n); + z = db_text(0, "SELECT datetime(mtime) || ' - about ' ||" + " CAST(julianday('now') - mtime AS INTEGER)" + " || ' days ago' FROM event " + " ORDER BY mtime DESC LIMIT 1"); + fossil_print("%*s%s\n", colWidth, "latest-change:", z); + } + n = db_int(0, "SELECT julianday('now') - (SELECT min(mtime) FROM event)" + " + 0.99"); + fossil_print("%*s%,d days or approximately %.2f years.\n", + colWidth, "project-age:", n, n/365.2425); + if( !brief ){ + p = db_get("project-code", 0); + if( p ){ + fossil_print("%*s%s\n", colWidth, "project-id:", p); + } + } +#if 0 + /* Server-id is not useful information any more */ + fossil_print("%*s%s\n", colWidth, "server-id:", db_get("server-code", 0)); +#endif + fossil_print("%*s%s\n", colWidth, "schema-version:", g.zAuxSchema); + if( !omitVers ){ + fossil_print("%*s%s %s [%s] (%s)\n", + colWidth, "fossil-version:", + MANIFEST_DATE, MANIFEST_VERSION, RELEASE_VERSION, + COMPILER_NAME); + fossil_print("%*s%.19s [%.10s] (%s)\n", + colWidth, "sqlite-version:", + sqlite3_sourceid(), &sqlite3_sourceid()[20], + sqlite3_libversion()); + fossil_print("%*s%s\n", + colWidth, "pikchr-version:", + pikchr_version()); + } + fossil_print("%*s%,d pages, %d bytes/pg, %,d free pages, " + "%s, %s mode\n", + colWidth, "database-stats:", + db_int(0, "PRAGMA repository.page_count"), + db_int(0, "PRAGMA repository.page_size"), + db_int(0, "PRAGMA repository.freelist_count"), + db_text(0, "PRAGMA repository.encoding"), + db_text(0, "PRAGMA repository.journal_mode")); + if( dbCheck ){ + if( dbCheck<2 ){ + char *zRes = db_text(0, "PRAGMA repository.quick_check(1)"); + fossil_print("%*s%s\n", colWidth, "database-check:", zRes); + }else{ + char *newArgv[3]; + newArgv[0] = g.argv[0]; + newArgv[1] = "test-integrity"; + newArgv[2] = 0; + g.argv = newArgv; + g.argc = 2; + fossil_print("Full repository verification follows:\n"); + test_integrity(); + } + } +} + +/* +** Return a string which is the public URL used to access this repository. +** Or return a NULL pointer if this repository does not have a public +** access URL. +** +** Algorithm: +** +** The public URL is given by the email-url property. But it is only +** returned if there have been one or more accesses (as recorded by +** "baseurl:URL" entries in the CONFIG table). +*/ +const char *public_url(void){ + const char *zUrl = db_get("email-url", 0); + if( zUrl==0 ) return 0; + if( !db_exists("SELECT 1 FROM config WHERE name='baseurl:%q'", zUrl) ){ + return 0; + } + return zUrl; +} + + +/* +** WEBPAGE: urllist +** +** Show ways in which this repository has been accessed +*/ +void urllist_page(void){ + Stmt q; + int cnt; + int total = 0; + int showAll = P("all")!=0; + int nOmitted; + sqlite3_int64 iNow; + char *zPriorRepo = 0; + + login_check_credentials(); + if( !g.perm.Admin ){ login_needed(0); return; } + + style_set_current_feature("stat"); + style_header("URLs and Checkouts"); + style_adunit_config(ADUNIT_RIGHT_OK); + style_submenu_element("Stat", "stat"); + style_submenu_element("Schema", "repo_schema"); + iNow = db_int64(0, "SELECT strftime('%%s','now')"); + + + db_prepare(&q, "SELECT substr(name,9), datetime(mtime,'unixepoch'), mtime" + " FROM config WHERE name GLOB 'baseurl:*' ORDER BY 3 DESC"); + cnt = 0; + nOmitted = 0; + while( db_step(&q)==SQLITE_ROW ){ + if( cnt==0 ){ + @
URLs used to access this repository
+ @ + } + if( !showAll && db_column_int64(&q,2)<(iNow - 3600*24*30) && cnt>8 ){ + nOmitted++; + }else{ + @ + @ + } + cnt++; + } + db_finalize(&q); + + if( nOmitted ){ + @
%h(db_column_text(&q,0))%h(db_column_text(&q,1))
Show %d(nOmitted) more... + } + if( cnt ){ + @
+ total += cnt; + } + if( P("urlonly") ){ + style_finish_page(); + return; + } + + + db_prepare(&q, "SELECT substr(name,7), datetime(mtime,'unixepoch')" + " FROM config WHERE name GLOB 'ckout:*' ORDER BY 2 DESC"); + cnt = 0; + while( db_step(&q)==SQLITE_ROW ){ + const char *zPath = db_column_text(&q,0); + if( vfile_top_of_checkout(zPath) ){ + if( cnt==0 ){ + @
Checkouts
+ @ + } + @ + @ + cnt++; + } + } + db_finalize(&q); + if( cnt ){ + @
%h(zPath)%h(db_column_text(&q,1))
+ total += cnt; + } + + cnt = 0; + db_prepare(&q, + "SELECT substr(name,10), datetime(mtime,'unixepoch')" + " FROM config WHERE name GLOB 'syncwith:*'" + "UNION ALL " + "SELECT substr(name,10), datetime(mtime,'unixepoch')" + " FROM config WHERE name GLOB 'syncfrom:*'" + "UNION ALL " + "SELECT substr(name,9), datetime(mtime,'unixepoch')" + " FROM config WHERE name GLOB 'gitpush:*'" + "GROUP BY 1 ORDER BY 2 DESC" + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zURL = db_column_text(&q,0); + UrlData x; + if( cnt==0 ){ + @
Recently synced with these URLs
+ @ + } + memset(&x, 0, sizeof(x)); + url_parse_local(zURL, URL_OMIT_USER, &x); + @ + cnt++; + url_unparse(&x); + } + db_finalize(&q); + if( cnt ){ + @
%h(x.canonical) + @ %h(db_column_text(&q,1))
+ total += cnt; + } + + cnt = 0; + db_prepare(&q, + "SELECT" + " substr(name,6)," + " datetime(mtime,'unixepoch')," + " value->>'type'," + " value->>'src'\n" + "FROM config\n" + "WHERE name GLOB 'link:*'\n" + "AND json_valid(value)\n" + "ORDER BY 4, 2 DESC" + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zUrl = db_column_text(&q, 0); + const char *zType = db_column_text(&q, 2); + const char *zSrc = db_column_text(&q, 3); + if( zUrl==0 || zSrc==0 ) continue; + if( cnt++==0 ){ + @
Links from other repositories
+ @ + } + if( zPriorRepo==0 || strcmp(zPriorRepo,zSrc)!=0 ){ + fossil_free(zPriorRepo); + zPriorRepo = fossil_strdup(zSrc); + @ + } + @ + @ + if( zType ){ + @ + }else{ + @ + } + @ + } + db_finalize(&q); + fossil_free(zPriorRepo); + if( cnt ){ + @
\ + @ From %h(zSrc)...
     %h(zUrl) (%h(zType))  %h(db_column_text(&q,1))
+ total += cnt; + } + + cnt = 0; + db_prepare(&q, + "SELECT" + " value," + " url_nouser(value)," + " substr(name,10)," + " datetime(mtime,'unixepoch')" + "FROM config\n" + "WHERE name GLOB 'sync-url:*'\n" + "ORDER BY 2" + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zUrl = db_column_text(&q, 0); + const char *zLink = db_column_text(&q, 1); + const char *zName = db_column_text(&q, 2); + if( cnt++==0 ){ + @
Defined sync targets
+ @ + } + @ + @ + @ + } + db_finalize(&q); + if( cnt ){ + @
%h(zName)  %h(zUrl)%h(db_column_text(&q,3))
+ total += cnt; + } + + + if( total==0 ){ + @

No record of any URLs or checkouts

+ } + style_finish_page(); +} + +/* +** WEBPAGE: repo_schema +** +** Show the repository schema +*/ +void repo_schema_page(void){ + Stmt q; + Blob sql; + const char *zArg = P("n"); + login_check_credentials(); + if( !g.perm.Admin ){ login_needed(0); return; } + + if( zArg!=0 + && db_table_exists("repository",zArg) + && cgi_csrf_safe(1) + ){ + if( P("analyze")!=0 ){ + db_multi_exec("ANALYZE \"%w\"", zArg); + }else if( P("analyze200")!=0 ){ + db_multi_exec("PRAGMA analysis_limit=200; ANALYZE \"%w\"", zArg); + }else if( P("deanalyze")!=0 ){ + db_unprotect(PROTECT_ALL); + db_multi_exec("DELETE FROM repository.sqlite_stat1" + " WHERE tbl LIKE %Q", zArg); + db_protect_pop(); + } + } + + style_set_current_feature("stat"); + style_header("Repository Schema"); + style_adunit_config(ADUNIT_RIGHT_OK); + style_submenu_element("Stat", "stat"); + style_submenu_element("URLs", "urllist"); + if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){ + style_submenu_element("Table Sizes", "repo-tabsize"); + } + blob_init(&sql, + "SELECT sql FROM repository.sqlite_schema WHERE sql IS NOT NULL", -1); + if( zArg ){ + style_submenu_element("All", "repo_schema"); + blob_appendf(&sql, " AND (tbl_name=%Q OR name=%Q)", zArg, zArg); + } + blob_appendf(&sql, " ORDER BY tbl_name, type<>'table', name"); + db_prepare(&q, "%s", blob_str(&sql)/*safe-for-%s*/); + blob_reset(&sql); + @
+  while( db_step(&q)==SQLITE_ROW ){
+    @ %h(db_column_text(&q, 0));
+  }
+  @ 
+ db_finalize(&q); + if( db_table_exists("repository","sqlite_stat1") ){ + if( zArg ){ + db_prepare(&q, + "SELECT tbl, idx, stat FROM repository.sqlite_stat1" + " WHERE tbl LIKE %Q OR idx LIKE %Q" + " ORDER BY tbl, idx", zArg, zArg); + + @
+ @
+      while( db_step(&q)==SQLITE_ROW ){
+        const char *zTab = db_column_text(&q,0);
+        const char *zIdx = db_column_text(&q,1);
+        const char *zStat = db_column_text(&q,2);
+        @ INSERT INTO sqlite_stat1 VALUES('%h(zTab)','%h(zIdx)','%h(zStat)');
+      }
+      @ 
+ db_finalize(&q); + }else{ + style_submenu_element("Stat1","repo_stat1"); + } + } + @
+ @
+ @
+ @ + @
+ + style_finish_page(); +} + +/* +** WEBPAGE: repo_stat1 +** +** Show the sqlite_stat1 table for the repository schema +*/ +void repo_stat1_page(void){ + int bTabular; + login_check_credentials(); + if( !g.perm.Admin ){ login_needed(0); return; } + bTabular = PB("tabular"); + + if( P("analyze")!=0 && cgi_csrf_safe(1) ){ + db_multi_exec("ANALYZE"); + }else if( P("analyze200")!=0 && cgi_csrf_safe(1) ){ + db_multi_exec("PRAGMA analysis_limit=200; ANALYZE;"); + }else if( P("deanalyze")!=0 && cgi_csrf_safe(1) ){ + db_unprotect(PROTECT_ALL); + db_multi_exec("DELETE FROM repository.sqlite_stat1;"); + db_protect_pop(); + } + style_set_current_feature("stat"); + style_header("Repository STAT1 Table"); + style_adunit_config(ADUNIT_RIGHT_OK); + style_submenu_element("Stat", "stat"); + style_submenu_element("Schema", "repo_schema"); + style_submenu_checkbox("tabular", "Tabular", 0, 0); + if( db_table_exists("repository","sqlite_stat1") ){ + Stmt q; + db_prepare(&q, + "SELECT tbl, idx, stat FROM repository.sqlite_stat1" + " ORDER BY tbl, idx"); + if( bTabular ){ + @ + @
TableIndexStat + }else{ + @
+    }
+    while( db_step(&q)==SQLITE_ROW ){
+      const char *zTab = db_column_text(&q,0);
+      const char *zIdx = db_column_text(&q,1);
+      const char *zStat = db_column_text(&q,2);
+      char *zUrl = href("%R/repo_schema?n=%t",zTab);
+      if( bTabular ){
+        @ 
%z(zUrl)%h(zTab)%h(zIdx)%h(zStat) + }else{ + @ INSERT INTO sqlite_stat1 \ + @ VALUES('%z(zUrl)%h(zTab)','%h(zIdx)','%h(zStat)'); + } + } + if( bTabular ){ + @
+ }else{ + @ + } + db_finalize(&q); + } + @

+ if( bTabular ){ + @ + } + @
+ @
+ @ + @
+ style_finish_page(); +} + +/* +** WEBPAGE: repo-tabsize +** +** Show relative sizes of tables in the repository database. +*/ +void repo_tabsize_page(void){ + int nPageFree; + sqlite3_int64 fsize; + int bDetail = 0; + char zBuf[100]; + + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + cgi_check_for_malice(); +#if defined(SQLITE_VERSION_NUMBER) && SQLITE_VERSION_NUMBER>=3054000 + if( g.perm.Admin && (bDetail = atoi(PD("detail","0")))!=0 ){ + extern int sqlite3_diskused_init( + sqlite3*, + char**, + const sqlite3_api_routines* + ); + sqlite3_diskused_init(g.db,0,0); + } +#endif + style_set_current_feature("stat"); + style_header("Repository Table Sizes"); + style_adunit_config(ADUNIT_RIGHT_OK); + style_submenu_element("Stat", "stat"); + if( bDetail ){ + const char *zFName; + Stmt q; + zFName = sqlite3_db_filename(g.db, "repository"); + @

%h(zFName)

+ db_prepare(&q, "SELECT diskused('repository')"); + if( db_step(&q)==SQLITE_ROW ){ + @
%h(db_column_text(&q,0))
+ } + db_finalize(&q); + if( g.localOpen ){ + zFName = sqlite3_db_filename(g.db, "localdb"); + @

%h(zFName)

+ db_prepare(&q, "SELECT diskused('localdb')"); + if( db_step(&q)==SQLITE_ROW ){ + @
%h(db_column_text(&q,0))
+ } + db_finalize(&q); + } + style_finish_page(); + return; + } + if( g.perm.Admin ){ + style_submenu_element("Schema", "repo_schema"); + style_submenu_element("Details", "repo-tabsize?detail=1"); + } + db_multi_exec( + "CREATE TEMP TABLE trans(name TEXT PRIMARY KEY,tabname TEXT)WITHOUT ROWID;" + "INSERT INTO trans(name,tabname)" + " SELECT name, tbl_name FROM repository.sqlite_schema;" + "CREATE TEMP TABLE piechart(amt REAL, label TEXT);" + "INSERT INTO piechart(amt,label)" + " SELECT sum(pageno)," + " coalesce((SELECT tabname FROM trans WHERE trans.name=dbstat.name),name)" + " FROM dbstat('repository',TRUE)" + " GROUP BY 2 ORDER BY 2;" + ); + nPageFree = db_int(0, "PRAGMA repository.freelist_count"); + if( nPageFree>0 ){ + db_multi_exec( + "INSERT INTO piechart(amt,label) VALUES(%d,'freelist')", + nPageFree + ); + } + fsize = file_size(g.zRepositoryName, ExtFILE); + approxSizeName(sizeof(zBuf), zBuf, fsize); + @

Repository Size: %s(zBuf)

+ @
+ piechart_render(800,500,PIE_OTHER|PIE_PERCENT); + @
+ + if( g.localOpen ){ + db_multi_exec( + "DELETE FROM trans;" + "INSERT INTO trans(name,tabname)" + " SELECT name, tbl_name FROM localdb.sqlite_schema;" + "DELETE FROM piechart;" + "INSERT INTO piechart(amt,label)" + " SELECT sum(pageno), " + " coalesce((SELECT tabname FROM trans WHERE trans.name=dbstat.name),name)" + " FROM dbstat('localdb',TRUE)" + " GROUP BY 2 ORDER BY 2;" + ); + nPageFree = db_int(0, "PRAGMA localdb.freelist_count"); + if( nPageFree>0 ){ + db_multi_exec( + "INSERT INTO piechart(amt,label) VALUES(%d,'freelist')", + nPageFree + ); + } + fsize = file_size(g.zLocalDbName, ExtFILE); + approxSizeName(sizeof(zBuf), zBuf, fsize); + @

%h(file_tail(g.zLocalDbName)) Size: %s(zBuf)

+ @
+ piechart_render(800,500,PIE_OTHER|PIE_PERCENT); + @
+ } + style_finish_page(); +} + +/* +** Gather statistics on artifact types, counts, and sizes. +** +** Only populate the artstat.atype field if the bWithTypes parameter is true. +*/ +void gather_artifact_stats(int bWithTypes){ + static const char zSql[] = + @ CREATE TEMP TABLE artstat( + @ id INTEGER PRIMARY KEY, -- Corresponds to BLOB.RID + @ atype TEXT, -- 'data', 'manifest', 'tag', 'wiki', etc. + @ isDelta BOOLEAN, -- true if stored as a delta + @ szExp, -- expanded, uncompressed size + @ szCmpr -- size as stored on disk + @ ); + @ INSERT INTO artstat(id,atype,isDelta,szExp,szCmpr) + @ SELECT blob.rid, NULL, + @ delta.rid IS NOT NULL, + @ size, octet_length(content) + @ FROM blob LEFT JOIN delta ON blob.rid=delta.rid + @ WHERE content IS NOT NULL; + ; + static const char zSql2[] = + @ UPDATE artstat SET atype='file' + @ WHERE +id IN (SELECT fid FROM mlink); + @ UPDATE artstat SET atype='manifest' + @ WHERE id IN (SELECT objid FROM event WHERE type='ci') AND atype IS NULL; + @ UPDATE artstat SET atype='forum' + @ WHERE id IN (SELECT objid FROM event WHERE type='f') AND atype IS NULL; + @ UPDATE artstat SET atype='cluster' + @ WHERE atype IS NULL + @ AND id IN (SELECT rid FROM tagxref + @ WHERE tagid=(SELECT tagid FROM tag + @ WHERE tagname='cluster')); + @ UPDATE artstat SET atype='ticket' + @ WHERE atype IS NULL + @ AND id IN (SELECT rid FROM tagxref + @ WHERE tagid IN (SELECT tagid FROM tag + @ WHERE tagname GLOB 'tkt-*')); + @ UPDATE artstat SET atype='wiki' + @ WHERE atype IS NULL + @ AND id IN (SELECT rid FROM tagxref + @ WHERE tagid IN (SELECT tagid FROM tag + @ WHERE tagname GLOB 'wiki-*')); + @ UPDATE artstat SET atype='technote' + @ WHERE atype IS NULL + @ AND id IN (SELECT rid FROM tagxref + @ WHERE tagid IN (SELECT tagid FROM tag + @ WHERE tagname GLOB 'event-*')); + @ UPDATE artstat SET atype='attachment' + @ WHERE atype IS NULL + @ AND id IN (SELECT attachid FROM attachment UNION + @ SELECT blob.rid FROM attachment JOIN blob ON uuid=src); + @ UPDATE artstat SET atype='tag' + @ WHERE atype IS NULL + @ AND id IN (SELECT srcid FROM tagxref); + @ UPDATE artstat SET atype='tag' + @ WHERE atype IS NULL + @ AND id IN (SELECT objid FROM event WHERE type='g'); + @ UPDATE artstat SET atype='unused' WHERE atype IS NULL; + ; + db_multi_exec("%s", zSql/*safe-for-%s*/); + if( bWithTypes ){ + db_multi_exec("%s", zSql2/*safe-for-%s*/); + } +} + +/* +** Output text "the largest N artifacts". Make this text a hyperlink +** to bigbloblist if N is not too big. +*/ +static void largest_n_artifacts(int N){ + if( N>250 ){ + @ (the largest %,d(N) artifacts) + }else{ + @ (the largest %d(N) artifacts) + } +} + +/* +** WEBPAGE: artifact_stats +** +** Show information about the sizes of artifacts in this repository +*/ +void artifact_stats_page(void){ + Stmt q; + int nTotal = 0; /* Total number of artifacts */ + int nDelta = 0; /* Total number of deltas */ + int nFull = 0; /* Total number of full-texts */ + double avgCmpr = 0.0; /* Average size of an artifact after compression */ + double avgExp = 0.0; /* Average size of an uncompressed artifact */ + int mxCmpr = 0; /* Maximum compressed artifact size */ + int mxExp = 0; /* Maximum uncompressed artifact size */ + sqlite3_int64 sumCmpr = 0; /* Total size of all compressed artifacts */ + sqlite3_int64 sumExp = 0; /* Total size of all expanded artifacts */ + sqlite3_int64 sz1pct = 0; /* Space used by largest 1% */ + sqlite3_int64 sz10pct = 0; /* Space used by largest 10% */ + sqlite3_int64 sz25pct = 0; /* Space used by largest 25% */ + sqlite3_int64 sz50pct = 0; /* Space used by largest 50% */ + int n50pct = 0; /* Artifacts using the first 50% of space */ + int n; /* Loop counter */ + int medCmpr = 0; /* Median compressed artifact size */ + int medExp = 0; /* Median expanded artifact size */ + int med; + double r; + + login_check_credentials(); + + /* These stats are expensive to compute. To disable them for + ** user without check-in privileges, to prevent excessive usage by + ** robots and random passers-by on the internet + */ + if( !g.perm.Write && !db_get_boolean("artifact_stats_enable",0) ){ + login_needed(g.anon.Write); + return; + } + cgi_check_for_malice(); + fossil_nice_default(); + + style_set_current_feature("stat"); + style_header("Artifact Statistics"); + style_submenu_element("Repository Stats", "stat"); + style_submenu_element("Artifact List", "bloblist"); + gather_artifact_stats(1); + + db_prepare(&q, + "SELECT count(*), sum(isDelta), max(szCmpr)," + " max(szExp), sum(szCmpr), sum(szExp)" + " FROM artstat" + ); + db_step(&q); + nTotal = db_column_int(&q,0); + nDelta = db_column_int(&q,1); + nFull = nTotal - nDelta; + mxCmpr = db_column_int(&q, 2); + mxExp = db_column_int(&q, 3); + sumCmpr = db_column_int64(&q, 4); + sumExp = db_column_int64(&q, 5); + db_finalize(&q); + if( nTotal==0 ){ + @ No artifacts in this repository! + style_finish_page(); + return; + } + avgCmpr = (double)sumCmpr/nTotal; + avgExp = (double)sumExp/nTotal; + + db_prepare(&q, "SELECT szCmpr FROM artstat ORDER BY 1 DESC"); + r = 0; + n = 0; + while( db_step(&q)==SQLITE_ROW ){ + r += db_column_int(&q, 0); + if( n50pct==0 && r>=sumCmpr/2 ) n50pct = n; + if( n==(nTotal+99)/100 ) sz1pct = (sqlite3_int64)r; + if( n==(nTotal+9)/10 ) sz10pct = (sqlite3_int64)r; + if( n==(nTotal+4)/5 ) sz25pct = (sqlite3_int64)r; + if( n==(nTotal+1)/2 ){ + sz50pct = (sqlite3_int64)r; + medCmpr = db_column_int(&q,0); + } + n++; + } + db_finalize(&q); + + @

Overall Artifact Size Statistics:

+ @ + @ + @ \ + @ + @ + medExp = db_int(0, "SELECT szExp FROM artstat ORDER BY szExp" + " LIMIT 1 OFFSET %d", nTotal/2); + @ \ + @ + @ \ + @ + + db_prepare(&q, + "SELECT avg(szCmpr), max(szCmpr) FROM artstat WHERE isDelta" + ); + if( db_step(&q)==SQLITE_ROW ){ + int mxDelta = db_column_int(&q,1); + double avgDelta = db_column_double(&q,0); + med = db_int(0, "SELECT szCmpr FROM artstat WHERE isDelta ORDER BY szCmpr" + " LIMIT 1 OFFSET %d", nDelta/2); + @ \ + @ + } + db_finalize(&q); + r = db_double(0.0, "SELECT avg(szCmpr) FROM artstat WHERE NOT isDelta;"); + med = db_int(0, "SELECT szCmpr FROM artstat WHERE NOT isDelta ORDER BY szCmpr" + " LIMIT 1 OFFSET %d", nFull/2); + @ + @ + @
Number of artifacts:%,d(nTotal)
Number of deltas:%,d(nDelta) (%d(nDelta*100/nTotal)%%)
Number of full-text:%,d(nFull) \ + @ (%d(nFull*100/nTotal)%%)
Uncompressed artifact sizes:largest: %,d(mxExp), average: %,d((int)avgExp), median: %,d(medExp)
Compressed artifact sizes:largest: %,d(mxCmpr), average: %,d((int)avgCmpr), \ + @ median: %,d(medCmpr)
Delta artifact sizes:largest: %,d(mxDelta), average: %,d((int)avgDelta), \ + @ median: %,d(med)
Full-text artifact sizes:largest: %,d(mxCmpr), average: %,d((int)r), median: %,d(med)
+ + @

Artifact Size Distribution Facts:

+ @
    + @
  1. The largest %.2f(n50pct*100.0/nTotal)%% of artifacts + largest_n_artifacts(n50pct); + @ use 50%% of the total artifact space. + @

  2. The largest 1%% of artifacts + largest_n_artifacts((nTotal+99)/100); + @ use %lld(sz1pct*100/sumCmpr)%% of the total artifact space. + @

  3. The largest 10%% of artifacts + largest_n_artifacts((nTotal+9)/10); + @ use %lld(sz10pct*100/sumCmpr)%% of the total artifact space. + @

  4. The largest 25%% of artifacts + largest_n_artifacts((nTotal+4)/5); + @ use %lld(sz25pct*100/sumCmpr)%% of the total artifact space. + @

  5. The largest 50%% of artifacts + largest_n_artifacts((nTotal+1)/2); + @ use %lld(sz50pct*100/sumCmpr)%% of the total artifact space. + @

+ + @

Artifact Sizes By Type:

+ db_prepare(&q, + "SELECT atype, count(*), sum(isDelta), sum(szCmpr), sum(szExp)" + " FROM artstat GROUP BY 1" + " UNION ALL " + "SELECT 'ALL', count(*), sum(isDelta), sum(szCmpr), sum(szExp)" + " FROM artstat" + " ORDER BY 4;" + ); + @ + @ + @ + @ + @ + @ + @ + @ + @ + while( db_step(&q)==SQLITE_ROW ){ + const char *zType = db_column_text(&q, 0); + int nTotal = db_column_int(&q, 1); + int nDelta = db_column_int(&q, 2); + int nFull = nTotal - nDelta; + sqlite3_int64 szCmpr = db_column_int64(&q, 3); + sqlite3_int64 szExp = db_column_int64(&q, 4); + @ + @ + @ + @ + @ + @ + } + @
Artifact TypeCountFull-TextDeltaCompressed SizeUncompressed Size
%h(zType)%,d(nTotal)%,d(nFull)%,d(nDelta)%,lld(szCmpr)%,lld(szExp)
+ db_finalize(&q); + + if( db_exists("SELECT 1 FROM artstat WHERE atype='unused'") ){ + @

Unused Artifacts:

+ db_prepare(&q, + "SELECT artstat.id, blob.uuid, user.login," + " datetime(rcvfrom.mtime), rcvfrom.rcvid" + " FROM artstat JOIN blob ON artstat.id=blob.rid" + " LEFT JOIN rcvfrom USING(rcvid)" + " LEFT JOIN user USING(uid)" + " WHERE atype='unused'" + ); + @ + @ + @ + @ + @ + @ + @ + @ + while( db_step(&q)==SQLITE_ROW ){ + int rid = db_column_int(&q, 0); + const char *zHash = db_column_text(&q, 1); + const char *zUser = db_column_text(&q, 2); + const char *zDate = db_column_text(&q, 3); + int iRcvid = db_column_int(&q, 4); + @ + @ + @ + @ + @ + } + @
RecordIDHashUserDateRcvID
%d(rid)%z(href("%R/info/%!S",zHash))%S(zHash)%h(zUser)%h(zDate)%z(href("%R/rcvfrom?rcvid=%d",iRcvid))%d(iRcvid)
+ db_finalize(&q); + } + style_table_sorter(); + style_finish_page(); +} -- cgit v1.3.1