/*
** 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 <string.h>
#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);
}
}
/*
** 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;
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");
}
@ <table class="label-value">
fsize = file_size(g.zRepositoryName, ExtFILE);
@ <tr><th>Repository Size:</th><td>%,lld(fsize) bytes</td>
@ </td></tr>
if( !brief ){
@ <tr><th>Number Of Artifacts:</th><td>
n = db_int(0, "SELECT count(*) FROM blob");
m = db_int(0, "SELECT count(*) FROM delta");
@ %.d(n) (%,d(n-m) fulltext and %,d(m) deltas)
if( g.perm.Write ){
@ <a href='%R/artifact_stats'>Details</a>
}
@ </td></tr>
if( n>0 ){
int a, b;
Stmt q;
@ <tr><th>Uncompressed Artifact Size:</th><td>
db_prepare(&q, "SELECT total(size), avg(size), max(size)"
" FROM blob WHERE size>0 /*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
@ </td></tr>
@ <tr><th>Compression Ratio:</th><td>
if( t/fsize < 5 ){
b = 10;
a = t/(fsize/10);
}else{
b = 1;
a = t/fsize;
}
@ %d(a):%d(b)
@ </td></tr>
}
if( db_table_exists("repository","unversioned") ){
Stmt q;
char zStored[100];
db_prepare(&q,
"SELECT count(*), sum(sz), sum(length(content))"
" FROM unversioned"
" WHERE length(hash)>1"
);
if( db_step(&q)==SQLITE_ROW && (n = db_column_int(&q,0))>0 ){
sqlite3_int64 iStored, pct;
iStored = db_column_int64(&q,2);
pct = (iStored*100 + fsize/2)/fsize;
approxSizeName(sizeof(zStored), zStored, iStored);
@ <tr><th>Unversioned Files:</th><td>
@ %z(href("%R/uvlist"))%d(n) files</a>,
@ %s(zStored) compressed, %d(pct)%% of total repository space
@ </td></tr>
}
db_finalize(&q);
}
@ <tr><th>Number Of Check-ins:</th><td>
n = db_int(0, "SELECT count(*) FROM event WHERE type='ci' /*scan*/");
@ %,d(n)
@ </td></tr>
@ <tr><th>Number Of Files:</th><td>
n = db_int(0, "SELECT count(*) FROM filename /*scan*/");
@ %,d(n)
@ </td></tr>
@ <tr><th>Number Of Wiki Pages:</th><td>
n = db_int(0, "SELECT count(*) FROM tag /*scan*/"
" WHERE +tagname GLOB 'wiki-*'");
@ %,d(n)
@ </td></tr>
@ <tr><th>Number Of Tickets:</th><td>
n = db_int(0, "SELECT count(*) FROM tag /*scan*/"
" WHERE +tagname GLOB 'tkt-*'");
@ %,d(n)
@ </td></tr>
}
@ <tr><th>Duration Of Project:</th><td>
n = db_int(0, "SELECT julianday('now') - (SELECT min(mtime) FROM event)"
" + 0.99");
@ %,d(n) days or approximately %.2f(n/365.2425) years.
@ </td></tr>
p = db_get("project-code", 0);
if( p ){
@ <tr><th>Project ID:</th>
@ <td>%h(p) %h(db_get("project-name",""))</td></tr>
}
p = db_get("parent-project-code", 0);
if( p ){
@ <tr><th>Parent Project ID:</th>
@ <td>%h(p) %h(db_get("parent-project-name",""))</td></tr>
}
/* @ <tr><th>Server ID:</th><td>%h(db_get("server-code",""))</td></tr> */
@ <tr><th>Fossil Version:</th><td>
@ %h(MANIFEST_DATE) %h(MANIFEST_VERSION)
@ (%h(RELEASE_VERSION)) <a href='version?verbose'>(details)</a>
@ </td></tr>
@ <tr><th>SQLite Version:</th><td>%.19s(sqlite3_sourceid())
@ [%.10s(&sqlite3_sourceid()[20])] (%s(sqlite3_libversion()))
@ <a href='version?verbose'>(details)</a></td></tr>
if( g.eHashPolicy!=HPOLICY_AUTO ){
@ <tr><th>Schema Version:</th><td>%h(g.zAuxSchema),
@ %s(hpolicy_name())</td></tr>
}else{
@ <tr><th>Schema Version:</th><td>%h(g.zAuxSchema)</td></tr>
}
@ <tr><th>Repository Rebuilt:</th><td>
@ %h(db_get_mtime("rebuilt","%Y-%m-%d %H:%M:%S","Never"))
@ By Fossil %h(db_get("rebuilt","Unknown"))</td></tr>
@ <tr><th>Database Stats:</th><td>
@ %,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
@ </td></tr>
@ </table>
style_footer();
}
/*
** COMMAND: dbstat*
**
** Usage: %fossil dbstat OPTIONS
**
** Shows statistics and global information about the repository.
**
** Options:
**
** --brief|-b Only show essential elements
** --db-check Run a PRAGMA quick_check on the repository database
** --omit-version-info Omit the SQLite and Fossil version information
*/
void dbstat_cmd(void){
i64 t, fsize;
int n, m;
int szMax, szAvg;
int brief;
int omitVers; /* Omit Fossil and SQLite 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;
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");
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);
}
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 tag /*scan*/"
" WHERE tagname GLOB 'wiki-*'");
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);
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);
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%,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 ){
fossil_print("%*s%s\n", colWidth, "database-check:",
db_text(0, "PRAGMA quick_check(1)"));
}
}
/*
** WEBPAGE: urllist
**
** Show ways in which this repository has been accessed
*/
void urllist_page(void){
Stmt q;
int cnt;
int showAll = P("all")!=0;
int nOmitted;
sqlite3_int64 iNow;
char *zRemote;
login_check_credentials();
if( !g.perm.Admin ){ login_needed(0); return; }
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')");
@ <div class="section">URLs</div>
@ <table border="0" width='100%%'>
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( !showAll && db_column_int64(&q,2)<(iNow - 3600*24*30) && cnt>8 ){
nOmitted++;
}else{
@ <tr><td width='100%%'>%h(db_column_text(&q,0))</td>
@ <td><nobr>%h(db_column_text(&q,1))</nobr></td></tr>
}
cnt++;
}
db_finalize(&q);
if( cnt==0 ){
@ <tr><td>(none)</td>
}else if( nOmitted ){
@ <tr><td><a href="urllist?all"><i>Show %d(nOmitted) more...</i></a>
}
@ </table>
@ <div class="section">Checkouts</div>
@ <table border="0" width='100%%'>
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) ){
@ <tr><td width='100%%'>%h(zPath)</td>
@ <td><nobr>%h(db_column_text(&q,1))</nobr></td></tr>
}
cnt++;
}
db_finalize(&q);
if( cnt==0 ){
@ <tr><td>(none)</td>
}
@ </table>
zRemote = db_text(0, "SELECT value FROM config WHERE name='last-sync-url'");
if( zRemote ){
@ <div class="section">Last Sync URL</div>
if( sqlite3_strlike("http%", zRemote, 0)==0 ){
UrlData x;
url_parse_local(zRemote, URL_OMIT_USER, &x);
@ <p><a href='%h(x.canonical)'>%h(zRemote)</a>
}else{
@ <p>%h(zRemote)</p>
}
@ </div>
}
style_footer();
}
/*
** 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; }
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_master 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);
@ <pre>
while( db_step(&q)==SQLITE_ROW ){
@ %h(db_column_text(&q, 0));
}
@ </pre>
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);
@ <hr>
@ <pre>
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)');
}
@ </pre>
db_finalize(&q);
}else{
style_submenu_element("Stat1","repo_stat1");
}
}
style_footer();
}
/*
** WEBPAGE: repo_stat1
**
** Show the sqlite_stat1 table for the repository schema
*/
void repo_stat1_page(void){
login_check_credentials();
if( !g.perm.Admin ){ login_needed(0); return; }
style_header("Repository STAT1 Table");
style_adunit_config(ADUNIT_RIGHT_OK);
style_submenu_element("Stat", "stat");
style_submenu_element("Schema", "repo_schema");
if( db_table_exists("repository","sqlite_stat1") ){
Stmt q;
db_prepare(&q,
"SELECT tbl, idx, stat FROM repository.sqlite_stat1"
" ORDER BY tbl, idx");
@ <pre>
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);
@ INSERT INTO sqlite_stat1 VALUES('%z(zUrl)%h(zTab)</a>','%h(zIdx)','%h(zStat)');
}
@ </pre>
db_finalize(&q);
}
style_footer();
}
/*
** WEBPAGE: repo-tabsize
**
** Show relative sizes of tables in the repository database.
*/
void repo_tabsize_page(void){
int nPageFree;
sqlite3_int64 fsize;
char zBuf[100];
login_check_credentials();
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
style_header("Repository Table Sizes");
style_adunit_config(ADUNIT_RIGHT_OK);
style_submenu_element("Stat", "stat");
if( g.perm.Admin ){
style_submenu_element("Schema", "repo_schema");
}
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_master;"
"CREATE TEMP TABLE piechart(amt REAL, label TEXT);"
"INSERT INTO piechart(amt,label)"
" SELECT count(*), "
" coalesce((SELECT tabname FROM trans WHERE trans.name=dbstat.name),name)"
" FROM dbstat('repository')"
" 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);
@ <h2>Repository Size: %s(zBuf)</h2>
@ <center><svg width='800' height='500'>
piechart_render(800,500,PIE_OTHER|PIE_PERCENT);
@ </svg></center>
if( g.localOpen ){
db_multi_exec(
"DELETE FROM trans;"
"INSERT INTO trans(name,tabname)"
" SELECT name, tbl_name FROM localdb.sqlite_master;"
"DELETE FROM piechart;"
"INSERT INTO piechart(amt,label)"
" SELECT count(*), "
" coalesce((SELECT tabname FROM trans WHERE trans.name=dbstat.name),name)"
" FROM dbstat('localdb')"
" 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);
@ <h2>%h(file_tail(g.zLocalDbName)) Size: %s(zBuf)</h2>
@ <center><svg width='800' height='500'>
piechart_render(800,500,PIE_OTHER|PIE_PERCENT);
@ </svg></center>
}
style_footer();
}
/*
** Gather statistics on artifact types, counts, and sizes.
**
** Only populate the artstat.atype field if the bWithTypes parameter is true.
*/
static 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,
@ EXISTS(SELECT 1 FROM delta WHERE delta.rid=blob.rid),
@ size, length(content)
@ FROM blob
@ WHERE content IS NOT NULL;
;
static const char zSql2[] =
@ UPDATE artstat SET atype='file'
@ WHERE id IN (SELECT fid FROM mlink)
@ AND atype IS NULL;
@ UPDATE artstat SET atype='manifest'
@ WHERE id IN (SELECT objid FROM event WHERE type='ci') 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 <a href='%R/bigbloblist?n=%d(N)'>largest %d(N) artifacts</a>)
}
}
/*
** 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 ){
login_needed(g.anon.Admin);
return;
}
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_footer();
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 = r;
if( n==(nTotal+9)/10 ) sz10pct = r;
if( n==(nTotal+4)/5 ) sz25pct = r;
if( n==(nTotal+1)/2 ){ sz50pct = r; medCmpr = db_column_int(&q,0); }
n++;
}
db_finalize(&q);
@ <h1>Overall Artifact Size Statistics:</h1>
@ <table class="label-value">
@ <tr><th>Number of artifacts:</th><td>%,d(nTotal)</td></tr>
@ <tr><th>Number of deltas:</th>\
@ <td>%,d(nDelta) (%d(nDelta*100/nTotal)%%)</td></tr>
@ <tr><th>Number of full-text:</th><td>%,d(nFull) \
@ (%d(nFull*100/nTotal)%%)</td></tr>
medExp = db_int(0, "SELECT szExp FROM artstat ORDER BY szExp"
" LIMIT 1 OFFSET %d", nTotal/2);
@ <tr><th>Uncompressed artifact sizes:</th>\
@ <td>largest: %,d(mxExp), average: %,d((int)avgExp), median: %,d(medExp)</td>
@ <tr><th>Compressed artifact sizes:</th>\
@ <td>largest: %,d(mxCmpr), average: %,d((int)avgCmpr), \
@ median: %,d(medCmpr)</td>
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);
@ <tr><th>Delta artifact sizes:</th>\
@ <td>largest: %,d(mxDelta), average: %,d((int)avgDelta), \
@ median: %,d(med)</td>
}
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);
@ <tr><th>Full-text artifact sizes:</th>
@ <td>largest: %,d(mxCmpr), average: %,d((int)r), median: %,d(med)</td>
@ </table>
@ <h1>Artifact size distribution facts:</h1>
@ <ol>
@ <li><p>The largest %.2f(n50pct*100.0/nTotal)%% of artifacts
largest_n_artifacts(n50pct);
@ use 50%% of the total artifact space.
@ <li><p>The largest 1%% of artifacts
largest_n_artifacts((nTotal+99)/100);
@ use %lld(sz1pct*100/sumCmpr)%% of the total artifact space.
@ <li><p>The largest 10%% of artifacts
largest_n_artifacts((nTotal+9)/10);
@ use %lld(sz10pct*100/sumCmpr)%% of the total artifact space.
@ <li><p>The largest 25%% of artifacts
largest_n_artifacts((nTotal+4)/5);
@ use %lld(sz25pct*100/sumCmpr)%% of the total artifact space.
@ <li><p>The largest 50%% of artifacts
largest_n_artifacts((nTotal+1)/2);
@ use %lld(sz50pct*100/sumCmpr)%% of the total artifact space.
@ </ol>
@ <h1>Artifact Sizes By Type:</h1>
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;"
);
@ <table class='sortable' border='1' \
@ data-column-types='tkkkkk' data-init-sort='5'>
@ <thead><tr>
@ <th>Artifact Type</th>
@ <th>Count</th>
@ <th>Full-Text</th>
@ <th>Delta</th>
@ <th>Compressed Size</th>
@ <th>Uncompressed Size</th>
@ </tr></thead><tbody>
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);
@ <tr><td>%h(zType)</td>
@ <td data-sortkey='%08x(nTotal)' align='right'>%,d(nTotal)</td>
@ <td data-sortkey='%08x(nFull)' align='right'>%,d(nFull)</td>
@ <td data-sortkey='%08x(nDelta)' align='right'>%,d(nDelta)</td>
@ <td data-sortkey='%016x(szCmpr)' align='right'>%,lld(szCmpr)</td>
@ <td data-sortkey='%016x(szExp)' align='right'>%,lld(szExp)</td>
}
@ </tbody></table>
db_finalize(&q);
if( db_exists("SELECT 1 FROM artstat WHERE atype='unused'") ){
@ <h1>Unused Artifacts:</h1>
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'"
);
@ <table class='sortable' border='1' \
@ data-column-types='ntttt' data-init-sort='0'>
@ <thead><tr>
@ <th>RecordID</th>
@ <th>Hash</th>
@ <th>User</th>
@ <th>Date</th>
@ <th>RcvID</th>
@ </tr></thead><tbody>
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);
@ <tr><td>%d(rid)</td>
@ <td>%z(href("%R/info/%!S",zHash))%S(zHash)</a></td>
@ <td>%h(zUser)</td>
@ <td>%h(zDate)</td>
@ <td>%z(href("%R/rcvfrom?rcvid=%d",iRcvid))%d(iRcvid)</a></td></tr>
}
@ </tbody></table></div>
db_finalize(&q);
}
style_table_sorter();
style_footer();
}