mirror of
https://github.com/sqlite/sqlite.git
synced 2025-08-08 14:02:16 +03:00
More work on batch-runner.html/js to facilitate speed comparisons between various VFSes and WebSQL.
FossilOrigin-Name: 3bd1bc240676e56da87fc49f3c67a1edc4fafcf2a2416298d19ae4f80b676a72
This commit is contained in:
@@ -41,25 +41,27 @@
|
|||||||
SQL statements, and not used within string literals or the like.
|
SQL statements, and not used within string literals or the like.
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<div id='toolbar'>
|
<fieldset id='toolbar'>
|
||||||
<select class='disable-during-eval' id='sql-select'>
|
<div>
|
||||||
<option disabled selected>Populated via script code</option>
|
<select class='disable-during-eval' id='sql-select'>
|
||||||
</select>
|
<option disabled selected>Populated via script code</option>
|
||||||
<button class='disable-during-eval' id='sql-run'>Run selected SQL</button>
|
|
||||||
<button class='disable-during-eval' id='sql-run-next'>Run next...</button>
|
|
||||||
<button class='disable-during-eval' id='sql-run-remaining'>Run all remaining...</button>
|
|
||||||
<button class='disable-during-eval' id='export-metrics'>Export metrics (WIP)</button>
|
|
||||||
<button class='disable-during-eval' id='db-reset'>Reset db</button>
|
|
||||||
<button id='output-clear'>Clear output</button>
|
|
||||||
<span class='input-wrapper flex-col'>
|
|
||||||
<label for='select-impl'>Storage impl:</label>
|
|
||||||
<select id='select-impl'>
|
|
||||||
<option value='virtualfs'>Virtual filesystem</option>
|
|
||||||
<option value='memdb'>:memory:</option>
|
|
||||||
<option value='wasmfs-opfs'>WASMFS OPFS</option>
|
|
||||||
<option value='websql'>WebSQL</option>
|
|
||||||
</select>
|
</select>
|
||||||
</span>
|
<button class='disable-during-eval' id='sql-run'>Run selected SQL</button>
|
||||||
|
<button class='disable-during-eval' id='sql-run-next'>Run next...</button>
|
||||||
|
<button class='disable-during-eval' id='sql-run-remaining'>Run all remaining...</button>
|
||||||
|
<button class='disable-during-eval' id='export-metrics' disabled>Export metrics (WIP)<br>(broken by refactoring)</button>
|
||||||
|
<button class='disable-during-eval' id='db-reset'>Reset db</button>
|
||||||
|
<button id='output-clear'>Clear output</button>
|
||||||
|
<span class='input-wrapper flex-col'>
|
||||||
|
<label for='select-impl'>Storage impl:</label>
|
||||||
|
<select id='select-impl'>
|
||||||
|
<option value='virtualfs'>Virtual filesystem</option>
|
||||||
|
<option value='memdb'>:memory:</option>
|
||||||
|
<option value='wasmfs-opfs'>WASMFS OPFS</option>
|
||||||
|
<option value='websql'>WebSQL</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<span class='input-wrapper'>
|
<span class='input-wrapper'>
|
||||||
@@ -77,12 +79,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
#toolbar {
|
#toolbar > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
#toolbar > * {
|
#toolbar > div > * {
|
||||||
margin: 0.25em;
|
margin: 0.25em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -42,14 +42,14 @@
|
|||||||
const name = JSON.stringify(row.name);
|
const name = JSON.stringify(row.name);
|
||||||
const type = row.type;
|
const type = row.type;
|
||||||
switch(type){
|
switch(type){
|
||||||
case 'table': case 'view': case 'trigger':{
|
case 'index': case 'table':
|
||||||
|
case 'trigger': case 'view': {
|
||||||
const sql2 = 'DROP '+type+' '+name;
|
const sql2 = 'DROP '+type+' '+name;
|
||||||
warn(db.id,':',sql2);
|
tx.executeSql(sql2, [], ()=>{}, onErr);
|
||||||
tx.executeSql(sql2, [], atEnd, onErr);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
warn("Unhandled db entry type:",type,name);
|
warn("Unhandled db entry type:",type,'name =',name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,17 +78,24 @@
|
|||||||
}
|
}
|
||||||
capi.sqlite3_finalize(pStmt);
|
capi.sqlite3_finalize(pStmt);
|
||||||
pStmt = 0;
|
pStmt = 0;
|
||||||
while(toDrop.length){
|
let doBreak = !toDrop.length;
|
||||||
|
while(!doBreak){
|
||||||
const name = toDrop.pop();
|
const name = toDrop.pop();
|
||||||
const type = toDrop.pop();
|
const type = toDrop.pop();
|
||||||
let sql2;
|
let sql2;
|
||||||
switch(type){
|
if(name){
|
||||||
case 'table': case 'view': case 'trigger':
|
switch(type){
|
||||||
sql2 = 'DROP '+type+' '+name;
|
case 'table': case 'view': case 'trigger': case 'index':
|
||||||
break;
|
sql2 = 'DROP '+type+' '+name;
|
||||||
default:
|
break;
|
||||||
warn("Unhandled db entry type:",type,name);
|
default:
|
||||||
continue;
|
warn("Unhandled db entry type:",type,name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
sql2 = "VACUUM";
|
||||||
|
doBreak = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
wasm.setPtrValue(ppStmt, 0);
|
wasm.setPtrValue(ppStmt, 0);
|
||||||
warn(db.id,':',sql2);
|
warn(db.id,':',sql2);
|
||||||
@@ -118,7 +125,8 @@
|
|||||||
btnClear: E('#output-clear'),
|
btnClear: E('#output-clear'),
|
||||||
btnReset: E('#db-reset'),
|
btnReset: E('#db-reset'),
|
||||||
cbReverseLog: E('#cb-reverse-log-order'),
|
cbReverseLog: E('#cb-reverse-log-order'),
|
||||||
selImpl: E('#select-impl')
|
selImpl: E('#select-impl'),
|
||||||
|
fsToolbar: E('#toolbar')
|
||||||
},
|
},
|
||||||
db: Object.create(null),
|
db: Object.create(null),
|
||||||
dbs: Object.create(null),
|
dbs: Object.create(null),
|
||||||
@@ -161,10 +169,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Run this async so that the UI can be updated for the above header...
|
// Run this async so that the UI can be updated for the above header...
|
||||||
const dumpMetrics = ()=>{
|
const endRun = ()=>{
|
||||||
metrics.evalSqlEnd = performance.now();
|
metrics.evalSqlEnd = performance.now();
|
||||||
metrics.evalTimeTotal = (metrics.evalSqlEnd - metrics.evalSqlStart);
|
metrics.evalTimeTotal = (metrics.evalSqlEnd - metrics.evalSqlStart);
|
||||||
this.logHtml(db.id,"metrics:");//,JSON.stringify(metrics, undefined, ' '));
|
this.logHtml(db.id,"metrics:",JSON.stringify(metrics, undefined, ' '));
|
||||||
this.logHtml("prepare() count:",metrics.stmtCount);
|
this.logHtml("prepare() count:",metrics.stmtCount);
|
||||||
this.logHtml("Time in prepare_v2():",metrics.prepTotal,"ms",
|
this.logHtml("Time in prepare_v2():",metrics.prepTotal,"ms",
|
||||||
"("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())");
|
"("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())");
|
||||||
@@ -182,35 +190,59 @@
|
|||||||
/* WebSQL cannot execute multiple statements, nor can it execute SQL without
|
/* WebSQL cannot execute multiple statements, nor can it execute SQL without
|
||||||
an explicit transaction. Thus we have to do some fragile surgery on the
|
an explicit transaction. Thus we have to do some fragile surgery on the
|
||||||
input SQL. Since we're only expecting carefully curated inputs, the hope is
|
input SQL. Since we're only expecting carefully curated inputs, the hope is
|
||||||
that this will suffice. */
|
that this will suffice. PS: it also can't run most SQL functions, e.g. even
|
||||||
metrics.evalSqlStart = performance.now();
|
instr() results in "not authorized". */
|
||||||
|
if('string'!==typeof sql){ // assume TypedArray
|
||||||
|
sql = new TextDecoder().decode(sql);
|
||||||
|
}
|
||||||
|
sql = sql.replace(/-- [^\n]+\n/g,''); // comment lines interfere with our split()
|
||||||
const sqls = sql.split(/;+\n/);
|
const sqls = sql.split(/;+\n/);
|
||||||
const rxBegin = /^BEGIN/i, rxCommit = /^COMMIT/i, rxComment = /^\s*--/;
|
const rxBegin = /^BEGIN/i, rxCommit = /^COMMIT/i;
|
||||||
try {
|
try {
|
||||||
const nextSql = ()=>{
|
const nextSql = ()=>{
|
||||||
let x = sqls.shift();
|
let x = sqls.shift();
|
||||||
while(x && rxComment.test(x)) x = sqls.shift();
|
while(sqls.length && !x) x = sqls.shift();
|
||||||
return x && x.trim();
|
return x && x.trim();
|
||||||
};
|
};
|
||||||
|
const who = this;
|
||||||
const transaction = function(tx){
|
const transaction = function(tx){
|
||||||
let s;
|
try {
|
||||||
for(;s = nextSql(); s = s.nextSql()){
|
let s;
|
||||||
if(rxBegin.test(s)) continue;
|
/* Try to approximate the spirit of the input scripts
|
||||||
else if(rxCommit.test(s)) break;
|
by running batches bound by BEGIN/COMMIT statements. */
|
||||||
++metrics.stmtCount;
|
for(s = nextSql(); !!s; s = nextSql()){
|
||||||
const t = performance.now();
|
if(rxBegin.test(s)) continue;
|
||||||
tx.executeSql(s);
|
else if(rxCommit.test(s)) break;
|
||||||
metrics.stepTotal += performance.now() - t;
|
//console.log("websql sql again",sqls.length, s);
|
||||||
|
++metrics.stmtCount;
|
||||||
|
const t = performance.now();
|
||||||
|
tx.executeSql(s,[], ()=>{}, (t,e)=>{
|
||||||
|
console.error("WebSQL error",e,"SQL =",s);
|
||||||
|
who.logErr(e.message);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
metrics.stepTotal += performance.now() - t;
|
||||||
|
}
|
||||||
|
}catch(e){
|
||||||
|
who.logErr("transaction():",e.message);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
while(sqls.length){
|
const n = sqls.length;
|
||||||
db.handle.transaction(transaction);
|
const nextBatch = function(){
|
||||||
}
|
if(sqls.length){
|
||||||
resolve(this);
|
console.log("websql sqls.length",sqls.length,'of',n);
|
||||||
|
db.handle.transaction(transaction, reject, nextBatch);
|
||||||
|
}else{
|
||||||
|
resolve(who);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
metrics.evalSqlStart = performance.now();
|
||||||
|
nextBatch();
|
||||||
}catch(e){
|
}catch(e){
|
||||||
this.gotErr = e;
|
//this.gotErr = e;
|
||||||
|
console.error("websql error:",e);
|
||||||
reject(e);
|
reject(e);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
}else{/*sqlite3 db...*/
|
}else{/*sqlite3 db...*/
|
||||||
@@ -259,9 +291,8 @@
|
|||||||
resolve(this);
|
resolve(this);
|
||||||
}catch(e){
|
}catch(e){
|
||||||
if(pStmt) capi.sqlite3_finalize(pStmt);
|
if(pStmt) capi.sqlite3_finalize(pStmt);
|
||||||
this.gotErr = e;
|
//this.gotErr = e;
|
||||||
reject(e);
|
reject(e);
|
||||||
return;
|
|
||||||
}finally{
|
}finally{
|
||||||
wasm.scopedAllocPop(stack);
|
wasm.scopedAllocPop(stack);
|
||||||
}
|
}
|
||||||
@@ -278,7 +309,7 @@
|
|||||||
return p.catch(
|
return p.catch(
|
||||||
(e)=>this.logErr("Error via execSql("+name+",...):",e.message)
|
(e)=>this.logErr("Error via execSql("+name+",...):",e.message)
|
||||||
).finally(()=>{
|
).finally(()=>{
|
||||||
dumpMetrics();
|
endRun();
|
||||||
this.blockControls(false);
|
this.blockControls(false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -363,7 +394,8 @@
|
|||||||
|
|
||||||
/** Disable or enable certain UI controls. */
|
/** Disable or enable certain UI controls. */
|
||||||
blockControls: function(disable){
|
blockControls: function(disable){
|
||||||
document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable);
|
//document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable);
|
||||||
|
this.e.fsToolbar.disabled = disable;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -374,8 +406,9 @@
|
|||||||
*/
|
*/
|
||||||
metricsToArrays: function(){
|
metricsToArrays: function(){
|
||||||
const rc = [];
|
const rc = [];
|
||||||
Object.keys(this.metrics).sort().forEach((k)=>{
|
Object.keys(this.dbs).sort().forEach((k)=>{
|
||||||
const m = this.metrics[k];
|
const d = this.dbs[k];
|
||||||
|
const m = d.metrics;
|
||||||
delete m.evalSqlStart;
|
delete m.evalSqlStart;
|
||||||
delete m.evalSqlEnd;
|
delete m.evalSqlEnd;
|
||||||
const mk = Object.keys(m).sort();
|
const mk = Object.keys(m).sort();
|
||||||
@@ -384,16 +417,12 @@
|
|||||||
}
|
}
|
||||||
const row = [k.split('/').pop()/*remove dir prefix from filename*/];
|
const row = [k.split('/').pop()/*remove dir prefix from filename*/];
|
||||||
rc.push(row);
|
rc.push(row);
|
||||||
mk.forEach((kk)=>row.push(m[kk]));
|
row.push(...mk.map((kk)=>m[kk]));
|
||||||
});
|
});
|
||||||
return rc;
|
return rc;
|
||||||
},
|
},
|
||||||
|
|
||||||
metricsToBlob: function(colSeparator='\t'){
|
metricsToBlob: function(colSeparator='\t'){
|
||||||
if(1){
|
|
||||||
this.logErr("TODO: re-do metrics dump");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ar = [], ma = this.metricsToArrays();
|
const ar = [], ma = this.metricsToArrays();
|
||||||
if(!ma.length){
|
if(!ma.length){
|
||||||
this.logErr("Metrics are empty. Run something.");
|
this.logErr("Metrics are empty. Run something.");
|
||||||
@@ -437,15 +466,15 @@
|
|||||||
databases to clean them up.
|
databases to clean them up.
|
||||||
*/
|
*/
|
||||||
clearStorage: function(onlySelectedDb=false){
|
clearStorage: function(onlySelectedDb=false){
|
||||||
const list = onlySelectDb
|
const list = onlySelectedDb
|
||||||
? [('boolean'===typeof onlySelectDb)
|
? [('boolean'===typeof onlySelectedDb)
|
||||||
? this.dbs[this.e.selImpl.value]
|
? this.dbs[this.e.selImpl.value]
|
||||||
: onlySelectDb]
|
: onlySelectedDb]
|
||||||
: Object.values(this.dbs);
|
: Object.values(this.dbs);
|
||||||
for(let db of list){
|
for(let db of list){
|
||||||
if(db && db.handle){
|
if(db && db.handle){
|
||||||
this.logHtml("Clearing db",db.id);
|
this.logHtml("Clearing db",db.id);
|
||||||
d.clear();
|
db.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -467,7 +496,9 @@
|
|||||||
d.filename = ':memory:';
|
d.filename = ':memory:';
|
||||||
break;
|
break;
|
||||||
case 'wasmfs-opfs':
|
case 'wasmfs-opfs':
|
||||||
d.filename = 'file:'+(this.sqlite3.capi.sqlite3_wasmfs_opfs_dir())+'/wasmfs-opfs.sqlite3';
|
d.filename = 'file:'+(
|
||||||
|
this.sqlite3.capi.sqlite3_wasmfs_opfs_dir()
|
||||||
|
)+'/wasmfs-opfs.sqlite3b';
|
||||||
break;
|
break;
|
||||||
case 'websql':
|
case 'websql':
|
||||||
d.filename = 'websql.db';
|
d.filename = 'websql.db';
|
||||||
@@ -579,6 +610,7 @@
|
|||||||
self._MODULE = theEmccModule /* this is only to facilitate testing from the console */;
|
self._MODULE = theEmccModule /* this is only to facilitate testing from the console */;
|
||||||
sqlite3 = theEmccModule.sqlite3;
|
sqlite3 = theEmccModule.sqlite3;
|
||||||
console.log("App",App);
|
console.log("App",App);
|
||||||
|
self.App = App;
|
||||||
App.run(theEmccModule.sqlite3);
|
App.run(theEmccModule.sqlite3);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
14
manifest
14
manifest
@@ -1,5 +1,5 @@
|
|||||||
C Correct\sduplicate\scopies\sof\ssqlite3-api.js\sbeing\sembedded\sin\sthe\swasmfs-based\sbuilds.
|
C More\swork\son\sbatch-runner.html/js\sto\sfacilitate\sspeed\scomparisons\sbetween\svarious\sVFSes\sand\sWebSQL.
|
||||||
D 2022-09-28T13:01:49.194
|
D 2022-09-28T17:52:52.224
|
||||||
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
|
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
|
||||||
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
|
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
|
||||||
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
|
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
|
||||||
@@ -489,8 +489,8 @@ F ext/wasm/api/sqlite3-api-prologue.js 2a0dedb8127e8983d3199edea55151a45186b4626
|
|||||||
F ext/wasm/api/sqlite3-api-worker1.js d5d5b7fac4c4731c38c7e03f4f404b2a95c388a2a1d8bcf361caada572f107e0
|
F ext/wasm/api/sqlite3-api-worker1.js d5d5b7fac4c4731c38c7e03f4f404b2a95c388a2a1d8bcf361caada572f107e0
|
||||||
F ext/wasm/api/sqlite3-wasi.h 25356084cfe0d40458a902afb465df8c21fc4152c1d0a59b563a3fba59a068f9
|
F ext/wasm/api/sqlite3-wasi.h 25356084cfe0d40458a902afb465df8c21fc4152c1d0a59b563a3fba59a068f9
|
||||||
F ext/wasm/api/sqlite3-wasm.c b756b9c1fee9d0598f715e6df6bf089b750da24aa91bb7ef9277a037d81e7612
|
F ext/wasm/api/sqlite3-wasm.c b756b9c1fee9d0598f715e6df6bf089b750da24aa91bb7ef9277a037d81e7612
|
||||||
F ext/wasm/batch-runner.html 168fda0f66369edec6427991683af3ed3919f3713158b70571d296f9a269a281
|
F ext/wasm/batch-runner.html c363032aba7a525920f61f8be112a29459f73f07e46f0ba3b7730081a617826e
|
||||||
F ext/wasm/batch-runner.js c8d13f673a68e2094264a0284f8775dbda4a12e609d20c501316fe641a05c760
|
F ext/wasm/batch-runner.js 756528ff41c0a2d81607fe82d088f305c379279cc84661c9422a7993b3ac544d
|
||||||
F ext/wasm/common/SqliteTestUtil.js c997c12188c97109f344701a58dd627b9c0f98f32cc6a88413f6171f2191531c
|
F ext/wasm/common/SqliteTestUtil.js c997c12188c97109f344701a58dd627b9c0f98f32cc6a88413f6171f2191531c
|
||||||
F ext/wasm/common/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f
|
F ext/wasm/common/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f
|
||||||
F ext/wasm/common/testing.css 3a5143699c2b73a85b962271e1a9b3241b30d90e30d895e4f55665e648572962
|
F ext/wasm/common/testing.css 3a5143699c2b73a85b962271e1a9b3241b30d90e30d895e4f55665e648572962
|
||||||
@@ -2026,8 +2026,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
|
|||||||
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
|
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
|
||||||
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
|
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
|
||||||
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
|
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
|
||||||
P f5d6bf8616341037fa3e229edf820d19acef3e0a6207a652b2b143de0a493214
|
P bbfcfba260f39a9c91e82d87e06b1c2cb297c03498b4530aa3e7e01ef9916012
|
||||||
R 2d5aa67e77362d00ccd0491d8ca53ec4
|
R 0e9f94b500e19b6ceb4224cc315ebb8b
|
||||||
U stephan
|
U stephan
|
||||||
Z c9260f6deeb302437c703eb12e2e08ac
|
Z cfbdefed825643b9f5c5b839e4caa73f
|
||||||
# Remove this line to create a well-formed Fossil manifest.
|
# Remove this line to create a well-formed Fossil manifest.
|
||||||
|
@@ -1 +1 @@
|
|||||||
bbfcfba260f39a9c91e82d87e06b1c2cb297c03498b4530aa3e7e01ef9916012
|
3bd1bc240676e56da87fc49f3c67a1edc4fafcf2a2416298d19ae4f80b676a72
|
Reference in New Issue
Block a user