###############################################################################
#
# stress.eagle --
#
# Written by Joe Mistachkin.
# Released to the public domain, use at your own risk!
#
###############################################################################
package require Eagle
package require Eagle.Library
package require Eagle.Test
runTestPrologue
###############################################################################
package require System.Data.SQLite.Test
runSQLiteTestPrologue
###############################################################################
#
# NOTE: Make sure that SQLite core library is completely shutdown prior to
# starting any of the tests in this file.
#
shutdownSQLite $test_channel
###############################################################################
runTest {test stress-1.1 {multithreaded stress testing} -setup {
unset -nocomplain result thread index workload noWorkload srcDb db \
fileName compiled options count times logFileName logListener \
connection indicators iterations exitOnFail failures
#############################################################################
proc formatWorkloadResult { index } {
set result [appendArgs "---- iterations for workload (" $index "): "]
append result [expr {[info exists ::iterations($index,ok)] ? \
$::iterations($index,ok) : 0}] " ok, "
append result [expr {[info exists ::iterations($index,error)] ? \
$::iterations($index,error) : 0}] " error, "
#
# NOTE: Translate the workload index to the corresponding iteration
# error indicator so that we can easily lookup the number of
# failures (i.e. "unexpected errors") for the workload.
#
set indicator [string character [expr {[string ordinal a 0] + $index - 1}]]
append result [expr {[info exists ::failures($indicator)] ? \
$::failures($indicator) : 0}] " failed\n"
return $result
}
#############################################################################
proc isExpectedError { error } {
return [expr {[regexp -- {\sno such table: t1\s} $error] || \
[regexp -- {\sdatabase is locked\s} $error]}]
}
#############################################################################
proc showTest { indicator } {
tputs $::test_channel $indicator
append ::indicators $indicator
after [expr {int(rand() * 1000 + $::count(2))}]
}
#############################################################################
proc failTest { indicator error } {
#
# NOTE: Halt all testing and exit the process now
# -OR- just record the failure and continue?
#
if {$::exitOnFail} then {
set level [expr {[info level] - 1}]
tputs $::test_channel [appendArgs \
\n [info level $level] ": " \n\t $error \n]
exit Failure
} else {
tputs $::test_channel $indicator
if {![info exists ::failures($indicator)]} then {
set ::failures($indicator) 0
}
incr ::failures($indicator)
after [expr {int(rand() * 1000 + $::count(2))}]
}
}
#############################################################################
proc setupLogging { fileName } {
if {![info exists ::logListener]} then {
set ::logListener [object create -alias \
System.Diagnostics.TextWriterTraceListener $fileName]
}
object invoke System.Diagnostics.Trace.Listeners Add $::logListener
object invoke System.Data.SQLite.SQLiteLog Initialize
tputs $::test_channel [appendArgs \
"---- enabled SQLite trace logging to file \"" $fileName \"\n]
}
#############################################################################
proc cleanupLogging { fileName } {
if {[info exists ::logListener]} then {
object invoke System.Diagnostics.Trace.Listeners Remove $::logListener
$::logListener Close
}
#
# NOTE: Copy the trace listener log file to the main test log file.
#
tlog "---- BEGIN TRACE LISTENER OUTPUT\n"
tlog [readFile $fileName]
tlog "\n---- END TRACE LISTENER OUTPUT\n"
#
# NOTE: Delete the trace listener log file because its contents have
# been copied to the main test log file.
#
cleanupFile $fileName
tputs $::test_channel [appendArgs \
"---- disabled SQLite trace logging to file \"" $fileName \"\n]
}
#############################################################################
#
# NOTE: The trace listener used with the SQLiteLog class to capture output
# from the core SQLite library requires its own log file because the
# TextWriterTraceListener class opens and locks the log file it uses
# as the basis of the output stream. Before this test is complete,
# the entire contents of this trace log file will be copied into the
# main test log file and then deleted.
#
set logFileName [appendArgs [file rootname $test_log] .trace.log]
setupLogging $logFileName
#############################################################################
#
# NOTE: Setup the default values for the tunable workload parameters. Any,
# all, or none of these may be overriden via the command line.
#
set count(0) 1; # Workload repeat count (i.e. total full runs).
set count(1) 20; # Workload iteration count (i.e. within a run).
set count(2) 500; # Workload iteration delay, in milliseconds.
set count(3) 2000; # Workload "small" data chunk size, in bytes.
set count(4) 10000000; # Workload "big" data chunk size, in bytes.
set noWorkload [list]; # Workloads to be omitted from the run, by index.
set exitOnFail true; # Halt testing and exit process on test failure?
#############################################################################
#
# NOTE: If command line arguments to the test suite are available, process
# them for any options that are applicable to this test (i.e. any of
# the tunable workload parameters listed above).
#
if {[info exists argv] && [llength $argv] > 0} then {
parse options -flags \
{-StopOnUnknownOption +IgnoreOnUnknownOption SkipOnUnknownOption} -- \
[list [list null MustHaveIntegerValue -1 -1 -count0 $count(0)] \
[list null MustHaveIntegerValue -1 -1 -count1 $count(1)] \
[list null MustHaveIntegerValue -1 -1 -count2 $count(2)] \
[list null MustHaveIntegerValue -1 -1 -count3 $count(3)] \
[list null MustHaveIntegerValue -1 -1 -count4 $count(4)] \
[list null MustHaveListValue -1 -1 -noWorkload $noWorkload] \
[list null MustHaveBooleanValue -1 -1 -exitOnFail $exitOnFail]] $argv
set count(0) $options(-count0,value)
set count(1) $options(-count1,value)
set count(2) $options(-count2,value)
set count(3) $options(-count3,value)
set count(4) $options(-count4,value)
set noWorkload $options(-noWorkload,value)
set exitOnFail $options(-exitOnFail,value)
}
#############################################################################
tputs $test_channel [appendArgs \
"---- workloads will repeat " $count(0) " time(s)...\n"]
tputs $test_channel [appendArgs \
"---- workloads will have " $count(1) " iteration(s)...\n"]
tputs $test_channel [appendArgs \
"---- workloads will wait at least " $count(2) " millisecond(s)...\n"]
tputs $test_channel [appendArgs \
"---- workload \"small\" chunk size is " $count(3) " byte(s)...\n"]
tputs $test_channel [appendArgs \
"---- workload \"big\" chunk size is " $count(4) " byte(s)...\n"]
if {[llength $noWorkload] > 0} then {
tputs $test_channel [appendArgs \
"---- workloads to be skipped... " $noWorkload \n]
}
tputs $test_channel [appendArgs \
"---- workload failures " [expr {$exitOnFail ? "will" : "will not"}] \
" halt testing and exit the process...\n"]
#############################################################################
#
# NOTE: Workloads #12 and #13 contain C# code that should be compiled, but
# only once. These variables keep track of that state information.
# An integer value in one of these variables means the compilation was
# completed and the resulting compiled method may be invoked using the
# following command (where ${id} is the value of the variable):
#
# object invoke _Dynamic${id}.Test${id} BackupAndGetData
#
set compiled(12) ""
set compiled(13) ""
#############################################################################
#
# NOTE: This is an in-memory database with shared cache enabled.
#
set fileName(1) file::memory:?cache=shared
#############################################################################
#
# NOTE: This is a normal on-disk database.
#
set fileName(2) [file join [getDatabaseDirectory] [appendArgs \
stress- [pid] - [string trim [clock seconds] -] .db]]
#############################################################################
setupDb $fileName(1) "" "" "" "" "" false false true true srcDb
setupDb $fileName(2)
#############################################################################
#
# NOTE: This serves two purposes. First, it allows us to verify the trace
# logging subsystem is working properly. Second, it places the file
# name for the associated database file into the trace log file.
#
set connection [getDbConnection]
$connection LogMessage 0 [appendArgs \
"starting stress test using database \"" $fileName(2) \"...]
#############################################################################
# WORKLOAD #1 (A) #
#############################################################################
set workload(1) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #1, CREATE TABLE statements.
#
lappend ::times(1) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 2} {$index <= $count1} {incr index} {
if {[catch {
sql execute $db [appendArgs \
"CREATE TABLE IF NOT EXISTS t" \
$index "(x PRIMARY KEY, y, z);"]
showTest A
} error]} then {
if {[isExpectedError $error]} then {
showTest a
} else {
failTest a $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #2 (B) #
#############################################################################
set workload(2) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #2, DROP TABLE statements.
#
lappend ::times(2) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 2} {$index <= $count1} {incr index} {
if {[catch {
sql execute $db [appendArgs \
"DROP TABLE IF EXISTS t" $index \;]
showTest B
} error]} then {
if {[isExpectedError $error]} then {
showTest b
} else {
failTest b $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #3 (C) #
#############################################################################
set workload(3) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #3, "small" SELECT statements.
#
lappend ::times(3) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
set sql [appendArgs \
"SELECT x, y FROM " $table " WHERE z = 'small';"]
set reader [sql execute -execute reader \
-format dataReader -alias $db $sql]
while {[$reader Read]} {
#
# NOTE: Do nothing.
#
}
unset -nocomplain reader
showTest C
} error]} then {
if {[isExpectedError $error]} then {
showTest c
} else {
failTest c $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #4 (D) #
#############################################################################
set workload(4) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #4, "big" SELECT statements.
#
lappend ::times(4) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
set sql [appendArgs \
"SELECT x, y FROM " $table " WHERE z = 'big';"]
set reader [sql execute -execute reader \
-format dataReader -alias $db $sql]
while {[$reader Read]} {
#
# NOTE: Do nothing.
#
}
unset -nocomplain reader
showTest D
} error]} then {
if {[isExpectedError $error]} then {
showTest d
} else {
failTest d $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #5 (E) #
#############################################################################
set workload(5) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #5, "small" INSERT statements.
#
lappend ::times(5) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
sql execute $db [appendArgs \
"INSERT INTO " $table "(x, y, z) VALUES('" \
[format %lX [expr {random()}]] "', '" \
[base64 encode -- [expr {randstr($count2)}]] \
"', 'small');"]
showTest E
} error]} then {
if {[isExpectedError $error]} then {
showTest e
} else {
failTest e $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #6 (F) #
#############################################################################
set workload(6) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #6, "big" INSERT statements.
#
lappend ::times(6) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
sql execute $db [appendArgs \
"INSERT INTO " $table "(x, y, z) VALUES('" \
[format %lX [expr {random()}]] \
"', RANDOMBLOB(" $count3 "), 'big');"]
showTest F
} error]} then {
if {[isExpectedError $error]} then {
showTest f
} else {
failTest f $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #7 (G) #
#############################################################################
set workload(7) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #7, "small" UPDATE statements.
#
lappend ::times(7) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
sql execute $db [appendArgs "UPDATE " $table \
" SET y = '" [base64 encode -- [expr {randstr($count2)}]] \
"' WHERE x LIKE '" [format %X $index] "%' AND z = 'small';"]
showTest G
} error]} then {
if {[isExpectedError $error]} then {
showTest g
} else {
failTest g $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #8 (H) #
#############################################################################
set workload(8) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #8, "big" UPDATE statements.
#
lappend ::times(8) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
sql execute $db [appendArgs "UPDATE " $table \
" SET y = RANDOMBLOB(" $count3 \
") WHERE x LIKE '" [format %X $index] \
"%' AND z = 'big';"]
showTest H
} error]} then {
if {[isExpectedError $error]} then {
showTest h
} else {
failTest h $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #9 (I) #
#############################################################################
set workload(9) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #9, "small" DELETE statements.
#
lappend ::times(9) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
sql execute $db [appendArgs "DELETE FROM " $table \
" WHERE x LIKE '" [format %X $index] "%' AND z = 'small';"]
showTest I
} error]} then {
if {[isExpectedError $error]} then {
showTest i
} else {
failTest i $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #10 (J) #
#############################################################################
set workload(10) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #10, "big" DELETE statements.
#
lappend ::times(10) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
sql execute $db [appendArgs "DELETE FROM " $table \
" WHERE x LIKE '" [format %X $index] "%' AND z = 'big';"]
showTest J
} error]} then {
if {[isExpectedError $error]} then {
showTest j
} else {
failTest j $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #11 (K) #
#############################################################################
set workload(11) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #11, VACUUM statement.
#
lappend ::times(11) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
sql execute $db "VACUUM;"
showTest K
} error]} then {
if {[isExpectedError $error]} then {
showTest k
} else {
failTest k $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #12 (L) #
#############################################################################
set workload(12) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #12, backup to in-memory database.
#
lappend ::times(12) [lindex [time {
for {set index 1} {$index <= $count1} {incr index} {
if {[string is integer -strict $::compiled(12)]} then {
set id $::compiled(12); # NOTE: Already compiled.
if {[catch {
object invoke _Dynamic${id}.Test${id} BackupAndGetData
showTest L
} error]} then {
if {[isExpectedError $error]} then {
showTest l
} else {
failTest l $error
}
}
} else {
set id [object invoke Interpreter.GetActive NextId]
set code [compileCSharpWith [subst {
using System;
using System.Data.SQLite;
using System.Text;
namespace _Dynamic${id}
{
public static class Test${id}
{
public static void BackupAndGetData()
{
using (SQLiteConnection source = new SQLiteConnection(
"FullUri=${dstFileName};"))
{
source.Open();
using (SQLiteConnection destination = new SQLiteConnection(
"FullUri=${srcFileName};"))
{
destination.Open();
source.BackupDatabase(
destination, "main", "main", -1, null, 0);
}
}
}
///////////////////////////////////////////////////////////////
public static void Main()
{
// do nothing.
}
}
}
}] true true true results errors System.Data.SQLite.dll]
if {$code eq "Ok"} then {
set ::compiled(12) $id; # NOTE: Compiled OK.
if {[catch {
object invoke _Dynamic${id}.Test${id} BackupAndGetData
showTest L
} error]} then {
if {[isExpectedError $error]} then {
showTest l
} else {
failTest l $error
}
}
} else {
error $errors
}
}
}
}] 0]
}]
#############################################################################
# WORKLOAD #13 (M) #
#############################################################################
set workload(13) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #13, backup from an in-memory database.
#
lappend ::times(13) [lindex [time {
for {set index 1} {$index <= $count1} {incr index} {
if {[string is integer -strict $::compiled(13)]} then {
set id $::compiled(13); # NOTE: Already compiled.
if {[catch {
object invoke _Dynamic${id}.Test${id} BackupAndGetData
showTest M
} error]} then {
if {[isExpectedError $error]} then {
showTest m
} else {
failTest m $error
}
}
} else {
set id [object invoke Interpreter.GetActive NextId]
set code [compileCSharpWith [subst {
using System;
using System.Data.SQLite;
using System.Text;
namespace _Dynamic${id}
{
public static class Test${id}
{
public static void BackupAndGetData()
{
using (SQLiteConnection source = new SQLiteConnection(
"FullUri=${srcFileName};"))
{
source.Open();
using (SQLiteConnection destination = new SQLiteConnection(
"FullUri=${dstFileName};"))
{
destination.Open();
source.BackupDatabase(
destination, "main", "main", -1, null, 0);
}
}
}
///////////////////////////////////////////////////////////////
public static void Main()
{
// do nothing.
}
}
}
}] true true true results errors System.Data.SQLite.dll]
if {$code eq "Ok"} then {
set ::compiled(13) $id; # NOTE: Compiled OK.
if {[catch {
object invoke _Dynamic${id}.Test${id} BackupAndGetData
showTest M
} error]} then {
if {[isExpectedError $error]} then {
showTest m
} else {
failTest m $error
}
}
} else {
error $errors
}
}
}
}] 0]
}]
#############################################################################
# WORKLOAD #14 (N) #
#############################################################################
set workload(14) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #14, PRAGMA integrity check statement.
#
lappend ::times(14) [lindex [time {
setupDb $dstFileName "" "" "" "" "" true false
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
set result [sql execute -execute scalar $db \
"PRAGMA integrity_check;"]
if {$result eq "ok"} then {
showTest N
} else {
error [appendArgs "integrity check failed: " $result]
}
} error]} then {
if {[isExpectedError $error]} then {
showTest n
} else {
failTest n $error
}
}
}
cleanupDb $dstFileName db false true false
}] 0]
}]
#############################################################################
# WORKLOAD #15 (O) #
#############################################################################
set workload(15) [list \
[list srcFileName dstFileName table count1 count2 count3] {
#
# NOTE: Workload #15, force managed garbage collection
#
lappend ::times(15) [lindex [time {
for {set index 1} {$index <= $count1} {incr index} {
if {[catch {
collectGarbage $::test_channel
showTest O
} error]} then {
if {[isExpectedError $error]} then {
showTest o
} else {
failTest o $error
}
}
}
}] 0]
}]
} -body {
for {set index(0) 0} {$index(0) < $count(0)} {incr index(0)} {
sql execute $db "CREATE TABLE IF NOT EXISTS t1(x PRIMARY KEY, y, z);"
unset -nocomplain thread
foreach index(1) [lsort -integer [array names workload]] {
if {[lsearch -exact $noWorkload $index(1)] == -1} then {
set thread($index(1)) [object create -alias System.Threading.Thread \
[list apply $workload($index(1)) $fileName(1) $fileName(2) t1 \
$count(1) $count(3) $count(4)] 1048576]
}
}
foreach index(1) [list Start Join] {
foreach index(2) [array names thread] {
$thread($index(2)) $index(1)
}
}
unset -nocomplain thread
}
#############################################################################
foreach index(0) [split $indicators ""] {
#
# NOTE: See if this workload iteration raised an error. If so, the
# indicator letter will be in lower case; otherwise, it will
# be in upper case.
#
set index(1) [string is upper -strict $index(0)]
set index(2) [expr {[string ordinal $index(0) 0] - \
[string ordinal [expr {$index(1) ? "A" : "a"}] 0] + 1}]
set index(3) [expr {$index(1) ? "ok" : "error"}]
if {![info exists iterations($index(2),$index(3))]} then {
set iterations($index(2),$index(3)) 0
}
incr iterations($index(2),$index(3))
}
#############################################################################
#
# NOTE: Advance output to the next line due to the workload iteration
# progress indicators.
#
tputs $test_channel \n
#############################################################################
foreach index(0) [lsort -integer [array names times]] {
set times(length) [llength $times($index(0))]
if {$times(length) > 0} then {
tputs $test_channel [appendArgs \
"---- average time for workload (" $index(0) ") is about " \
[expr int(([join $times($index(0)) +])/$times(length)/1000.0)] \
" milliseconds\n"]
} else {
tputs $test_channel [appendArgs \
"---- no times for workload (" $index(0) ")\n"]
}
}
#############################################################################
foreach index(0) [lsort -integer [array names workload]] {
tputs $test_channel [formatWorkloadResult $index(0)]
}
#############################################################################
set result [sql execute -execute scalar $srcDb "PRAGMA integrity_check;"]
if {$result eq "ok"} then {
tputs $test_channel "---- integrity check ok (srcDb)\n"
} else {
error [appendArgs "integrity check failed (srcDb): " $result]
}
#############################################################################
set result [sql execute -execute scalar $db "PRAGMA integrity_check;"]
if {$result eq "ok"} then {
tputs $test_channel "---- integrity check ok (db)\n"
} else {
error [appendArgs "integrity check failed (db): " $result]
}
#############################################################################
#
# NOTE: The overall test result is the total number of failures (i.e.
# "unexpected errors") encountered during a workload iteration.
#
expr {[array size failures] > 0 ? \
[expr [join [array values failures] +]] : 0}
} -cleanup {
rename failTest ""
rename showTest ""
rename isExpectedError ""
rename formatWorkloadResult ""
cleanupDb $fileName(2)
cleanupDb $fileName(1) srcDb
foreach index(0) [array names workload] {
catch {
object removecallback [list apply $workload($index(0)) $fileName(1) \
$fileName(2) t1 $count(1) $count(3) $count(4)]
}
}
freeDbConnection
cleanupLogging $logFileName
rename cleanupLogging ""
rename setupLogging ""
unset -nocomplain result thread index workload noWorkload srcDb db \
fileName compiled options count times logFileName logListener \
connection indicators iterations exitOnFail failures
} -constraints \
{eagle monoBug28 command.sql compile.DATA SQLite System.Data.SQLite} \
-result {0}}
###############################################################################
runSQLiteTestEpilogue
runTestEpilogue