You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by pb...@apache.org on 2012/07/12 22:24:57 UTC

svn commit: r1360909 [14/14] - in /subversion/branches/inheritable-props: ./ build/generator/ build/generator/templates/ notes/wc-ng/ subversion/bindings/javahl/native/ subversion/bindings/javahl/tests/org/apache/subversion/javahl/ subversion/bindings/...

Modified: subversion/branches/inheritable-props/subversion/tests/libsvn_fs_fs/fs-pack-test.c
URL: http://svn.apache.org/viewvc/subversion/branches/inheritable-props/subversion/tests/libsvn_fs_fs/fs-pack-test.c?rev=1360909&r1=1360908&r2=1360909&view=diff
==============================================================================
--- subversion/branches/inheritable-props/subversion/tests/libsvn_fs_fs/fs-pack-test.c (original)
+++ subversion/branches/inheritable-props/subversion/tests/libsvn_fs_fs/fs-pack-test.c Thu Jul 12 20:24:53 2012
@@ -30,6 +30,7 @@
 #include "svn_pools.h"
 #include "svn_props.h"
 #include "svn_fs.h"
+#include "private/svn_string_private.h"
 
 #include "../svn_test_fs.h"
 
@@ -199,6 +200,77 @@ create_packed_filesystem(const char *dir
   return svn_fs_pack(dir, pack_notify, &pnb, NULL, NULL, pool);
 }
 
+/* Create a packed FSFS filesystem for revprop tests at REPO_NAME with
+ * MAX_REV revisions and the given SHARD_SIZE and OPTS.  Return it in *FS.
+ * Use POOL for allocations.
+ */
+static svn_error_t *
+prepare_revprop_repo(svn_fs_t **fs,
+                     const char *repo_name,
+                     int max_rev,
+                     int shard_size,
+                     const svn_test_opts_t *opts,
+                     apr_pool_t *pool)
+{
+  svn_fs_txn_t *txn;
+  svn_fs_root_t *txn_root;
+  const char *conflict;
+  svn_revnum_t after_rev;
+  apr_pool_t *subpool;
+
+  /* Create the packed FS and open it. */
+  SVN_ERR(create_packed_filesystem(repo_name, opts, max_rev, shard_size, pool));
+  SVN_ERR(svn_fs_open(fs, repo_name, NULL, pool));
+
+  subpool = svn_pool_create(pool);
+  /* Do a commit to trigger packing. */
+  SVN_ERR(svn_fs_begin_txn(&txn, *fs, max_rev, subpool));
+  SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool));
+  SVN_ERR(svn_test__set_file_contents(txn_root, "iota", "new-iota",  subpool));
+  SVN_ERR(svn_fs_commit_txn(&conflict, &after_rev, txn, subpool));
+  SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(after_rev));
+  svn_pool_destroy(subpool);
+
+  /* Pack the repository. */
+  SVN_ERR(svn_fs_pack(repo_name, NULL, NULL, NULL, NULL, pool));
+
+  return SVN_NO_ERROR;
+}
+
+/* For revision REV, return a short log message allocated in POOL.
+ */
+static svn_string_t *
+default_log(svn_revnum_t rev, apr_pool_t *pool)
+{
+  return svn_string_createf(pool, "Default message for rev %ld", rev);
+}
+
+/* For revision REV, return a long log message allocated in POOL.
+ */
+static svn_string_t *
+large_log(svn_revnum_t rev, apr_size_t length, apr_pool_t *pool)
+{
+  svn_stringbuf_t *temp = svn_stringbuf_create_ensure(100000, pool);
+  int i, count = (int)(length - 50) / 6;
+
+  svn_stringbuf_appendcstr(temp, "A ");
+  for (i = 0; i < count; ++i)
+    svn_stringbuf_appendcstr(temp, "very, ");
+  
+  svn_stringbuf_appendcstr(temp,
+    apr_psprintf(pool, "very long message for rev %ld, indeed", rev));
+
+  return svn_stringbuf__morph_into_string(temp);
+}
+
+/* For revision REV, return a long log message allocated in POOL.
+ */
+static svn_string_t *
+huge_log(svn_revnum_t rev, apr_pool_t *pool)
+{
+  return large_log(rev, 90000, pool);
+}
+
 
 /*** Tests ***/
 
@@ -404,18 +476,13 @@ commit_packed_fs(const svn_test_opts_t *
 /* ------------------------------------------------------------------------ */
 #define REPO_NAME "test-repo-get-set-revprop-packed-fs"
 #define SHARD_SIZE 4
-#define MAX_REV 1
+#define MAX_REV 10
 static svn_error_t *
 get_set_revprop_packed_fs(const svn_test_opts_t *opts,
                           apr_pool_t *pool)
 {
   svn_fs_t *fs;
-  svn_fs_txn_t *txn;
-  svn_fs_root_t *txn_root;
-  const char *conflict;
-  svn_revnum_t after_rev;
   svn_string_t *prop_value;
-  apr_pool_t *subpool;
 
   /* Bail (with success) on known-untestable scenarios */
   if ((strcmp(opts->fs_type, "fsfs") != 0)
@@ -423,30 +490,179 @@ get_set_revprop_packed_fs(const svn_test
     return SVN_NO_ERROR;
 
   /* Create the packed FS and open it. */
-  SVN_ERR(create_packed_filesystem(REPO_NAME, opts, MAX_REV, SHARD_SIZE, pool));
-  SVN_ERR(svn_fs_open(&fs, REPO_NAME, NULL, pool));
-
-  subpool = svn_pool_create(pool);
-  /* Do a commit to trigger packing. */
-  SVN_ERR(svn_fs_begin_txn(&txn, fs, MAX_REV, subpool));
-  SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool));
-  SVN_ERR(svn_test__set_file_contents(txn_root, "iota", "new-iota",  subpool));
-  SVN_ERR(svn_fs_commit_txn(&conflict, &after_rev, txn, subpool));
-  SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(after_rev));
-  svn_pool_clear(subpool);
-
-  /* Pack the repository. */
-  SVN_ERR(svn_fs_pack(REPO_NAME, NULL, NULL, NULL, NULL, pool));
+  SVN_ERR(prepare_revprop_repo(&fs, REPO_NAME, MAX_REV, SHARD_SIZE, opts,
+                               pool));
 
-  /* Try to get revprop for revision 0. */
+  /* Try to get revprop for revision 0
+   * (non-packed due to special handling). */
   SVN_ERR(svn_fs_revision_prop(&prop_value, fs, 0, SVN_PROP_REVISION_AUTHOR,
                                pool));
 
-  /* Try to change revprop for revision 0. */
+  /* Try to change revprop for revision 0
+   * (non-packed due to special handling). */
   SVN_ERR(svn_fs_change_rev_prop(fs, 0, SVN_PROP_REVISION_AUTHOR,
                                  svn_string_create("tweaked-author", pool),
                                  pool));
 
+  /* verify */
+  SVN_ERR(svn_fs_revision_prop(&prop_value, fs, 0, SVN_PROP_REVISION_AUTHOR,
+                               pool));
+  SVN_TEST_STRING_ASSERT(prop_value->data, "tweaked-author");
+
+  /* Try to get packed revprop for revision 5. */
+  SVN_ERR(svn_fs_revision_prop(&prop_value, fs, 5, SVN_PROP_REVISION_AUTHOR,
+                               pool));
+
+  /* Try to change packed revprop for revision 5. */
+  SVN_ERR(svn_fs_change_rev_prop(fs, 5, SVN_PROP_REVISION_AUTHOR,
+                                 svn_string_create("tweaked-author2", pool),
+                                 pool));
+
+  /* verify */
+  SVN_ERR(svn_fs_revision_prop(&prop_value, fs, 5, SVN_PROP_REVISION_AUTHOR,
+                               pool));
+  SVN_TEST_STRING_ASSERT(prop_value->data, "tweaked-author2");
+
+  return SVN_NO_ERROR;
+}
+#undef REPO_NAME
+#undef MAX_REV
+#undef SHARD_SIZE
+
+/* ------------------------------------------------------------------------ */
+#define REPO_NAME "test-repo-get-set-large-revprop-packed-fs"
+#define SHARD_SIZE 4
+#define MAX_REV 11
+static svn_error_t *
+get_set_large_revprop_packed_fs(const svn_test_opts_t *opts,
+                                apr_pool_t *pool)
+{
+  svn_fs_t *fs;
+  svn_string_t *prop_value;
+  svn_revnum_t rev;
+
+  /* Bail (with success) on known-untestable scenarios */
+  if ((strcmp(opts->fs_type, "fsfs") != 0)
+      || (opts->server_minor_version && (opts->server_minor_version < 7)))
+    return SVN_NO_ERROR;
+
+  /* Create the packed FS and open it. */
+  SVN_ERR(prepare_revprop_repo(&fs, REPO_NAME, MAX_REV, SHARD_SIZE, opts,
+                               pool));
+
+  /* Set commit messages to different, large values that fill the pack
+   * files but do not exceed the pack size limit. */
+  for (rev = 0; rev <= MAX_REV; ++rev)
+    SVN_ERR(svn_fs_change_rev_prop(fs, rev, SVN_PROP_REVISION_LOG,
+                                   large_log(rev, 15000, pool),
+                                   pool));
+
+  /* verify */
+  for (rev = 0; rev <= MAX_REV; ++rev)
+    {
+      SVN_ERR(svn_fs_revision_prop(&prop_value, fs, rev,
+                                   SVN_PROP_REVISION_LOG, pool));
+      SVN_TEST_STRING_ASSERT(prop_value->data,
+                             large_log(rev, 15000, pool)->data);
+    }
+
+  /* Put a larger revprop into the last, some middle and the first revision
+   * of a pack.  This should cause the packs to split in the middle. */
+  SVN_ERR(svn_fs_change_rev_prop(fs, 3, SVN_PROP_REVISION_LOG,
+                                 /* rev 0 is not packed */
+                                 large_log(3, 37000, pool),
+                                 pool));
+  SVN_ERR(svn_fs_change_rev_prop(fs, 5, SVN_PROP_REVISION_LOG,
+                                 large_log(5, 25000, pool),
+                                 pool));
+  SVN_ERR(svn_fs_change_rev_prop(fs, 8, SVN_PROP_REVISION_LOG,
+                                 large_log(8, 25000, pool),
+                                 pool));
+
+  /* verify */
+  for (rev = 0; rev <= MAX_REV; ++rev)
+    {
+      SVN_ERR(svn_fs_revision_prop(&prop_value, fs, rev,
+                                   SVN_PROP_REVISION_LOG, pool));
+
+      if (rev == 3)
+        SVN_TEST_STRING_ASSERT(prop_value->data,
+                               large_log(rev, 37000, pool)->data);
+      else if (rev == 5 || rev == 8)
+        SVN_TEST_STRING_ASSERT(prop_value->data,
+                               large_log(rev, 25000, pool)->data);
+      else
+        SVN_TEST_STRING_ASSERT(prop_value->data,
+                               large_log(rev, 15000, pool)->data);
+    }
+
+  return SVN_NO_ERROR;
+}
+#undef REPO_NAME
+#undef MAX_REV
+#undef SHARD_SIZE
+
+/* ------------------------------------------------------------------------ */
+#define REPO_NAME "test-repo-get-set-huge-revprop-packed-fs"
+#define SHARD_SIZE 4
+#define MAX_REV 10
+static svn_error_t *
+get_set_huge_revprop_packed_fs(const svn_test_opts_t *opts,
+                               apr_pool_t *pool)
+{
+  svn_fs_t *fs;
+  svn_string_t *prop_value;
+  svn_revnum_t rev;
+
+  /* Bail (with success) on known-untestable scenarios */
+  if ((strcmp(opts->fs_type, "fsfs") != 0)
+      || (opts->server_minor_version && (opts->server_minor_version < 7)))
+    return SVN_NO_ERROR;
+
+  /* Create the packed FS and open it. */
+  SVN_ERR(prepare_revprop_repo(&fs, REPO_NAME, MAX_REV, SHARD_SIZE, opts,
+                               pool));
+
+  /* Set commit messages to different values */
+  for (rev = 0; rev <= MAX_REV; ++rev)
+    SVN_ERR(svn_fs_change_rev_prop(fs, rev, SVN_PROP_REVISION_LOG,
+                                   default_log(rev, pool),
+                                   pool));
+
+  /* verify */
+  for (rev = 0; rev <= MAX_REV; ++rev)
+    {
+      SVN_ERR(svn_fs_revision_prop(&prop_value, fs, rev,
+                                   SVN_PROP_REVISION_LOG, pool));
+      SVN_TEST_STRING_ASSERT(prop_value->data, default_log(rev, pool)->data);
+    }
+
+  /* Put a huge revprop into the last, some middle and the first revision
+   * of a pack.  They will cause the pack files to split accordingly. */
+  SVN_ERR(svn_fs_change_rev_prop(fs, 3, SVN_PROP_REVISION_LOG,
+                                 huge_log(3, pool),
+                                 pool));
+  SVN_ERR(svn_fs_change_rev_prop(fs, 5, SVN_PROP_REVISION_LOG,
+                                 huge_log(5, pool),
+                                 pool));
+  SVN_ERR(svn_fs_change_rev_prop(fs, 8, SVN_PROP_REVISION_LOG,
+                                 huge_log(8, pool),
+                                 pool));
+
+  /* verify */
+  for (rev = 0; rev <= MAX_REV; ++rev)
+    {
+      SVN_ERR(svn_fs_revision_prop(&prop_value, fs, rev,
+                                   SVN_PROP_REVISION_LOG, pool));
+
+      if (rev == 3 || rev == 5 || rev == 8)
+        SVN_TEST_STRING_ASSERT(prop_value->data,
+                               huge_log(rev, pool)->data);
+      else
+        SVN_TEST_STRING_ASSERT(prop_value->data,
+                               default_log(rev, pool)->data);
+    }
+
   return SVN_NO_ERROR;
 }
 #undef REPO_NAME
@@ -533,6 +749,10 @@ struct svn_test_descriptor_t test_funcs[
                        "commit to a packed FSFS filesystem"),
     SVN_TEST_OPTS_PASS(get_set_revprop_packed_fs,
                        "get/set revprop while packing FSFS filesystem"),
+    SVN_TEST_OPTS_PASS(get_set_large_revprop_packed_fs,
+                       "get/set large packed revprops in FSFS"),
+    SVN_TEST_OPTS_PASS(get_set_huge_revprop_packed_fs,
+                       "get/set huge packed revprops in FSFS"),
     SVN_TEST_OPTS_PASS(recover_fully_packed,
                        "recover a fully packed filesystem"),
     SVN_TEST_NULL

Propchange: subversion/branches/inheritable-props/subversion/tests/libsvn_subr/
------------------------------------------------------------------------------
--- svn:ignore (original)
+++ svn:ignore Thu Jul 12 20:24:53 2012
@@ -38,3 +38,5 @@ subst_translate-test
 spillbuf-test
 named_atomic-test
 named_atomic-proc-test
+io-test
+io-test-temp

Propchange: subversion/branches/inheritable-props/subversion/tests/libsvn_wc/
------------------------------------------------------------------------------
--- svn:ignore (original)
+++ svn:ignore Thu Jul 12 20:24:53 2012
@@ -1,12 +1,13 @@
-tree-conflict-data-test
-.libs
 *.lo
+.libs
+conflict-data-test
 db-test
+entries-compat-test
 fake-wc
+op-depth-test
 pristine-store-test
 svn-test-work
-entries-compat-test
-op-depth-test
-wc-lock-tester
+tree-conflict-data-test
 wc-incomplete-tester
+wc-lock-tester
 wc-queries-test

Modified: subversion/branches/inheritable-props/subversion/tests/libsvn_wc/db-test.c
URL: http://svn.apache.org/viewvc/subversion/branches/inheritable-props/subversion/tests/libsvn_wc/db-test.c?rev=1360909&r1=1360908&r2=1360909&view=diff
==============================================================================
--- subversion/branches/inheritable-props/subversion/tests/libsvn_wc/db-test.c (original)
+++ subversion/branches/inheritable-props/subversion/tests/libsvn_wc/db-test.c Thu Jul 12 20:24:53 2012
@@ -664,7 +664,8 @@ test_inserting_nodes(apr_pool_t *pool)
             props,
             1, TIME_1a, AUTHOR_1,
             checksum,
-            NULL, NULL, FALSE, NULL, FALSE, FALSE, NULL,
+            NULL, FALSE, FALSE, NULL, FALSE, FALSE,
+            NULL, NULL,
             pool));
 
   /* Create a new symlink node. */
@@ -676,7 +677,8 @@ test_inserting_nodes(apr_pool_t *pool)
             props,
             1, TIME_1a, AUTHOR_1,
             "O-target",
-            NULL, NULL, FALSE, NULL, NULL,
+            NULL, FALSE, FALSE, NULL, FALSE, FALSE,
+            NULL, NULL,
             pool));
 
   /* Replace an incomplete node with an absent file node. */
@@ -1416,6 +1418,7 @@ test_externals_store(apr_pool_t *pool)
                                        FALSE, NULL,
                                        FALSE,
                                        NULL,
+                                       NULL,
                                        pool));
 
   SVN_ERR(svn_wc__db_external_add_dir(db,

Modified: subversion/branches/inheritable-props/subversion/tests/libsvn_wc/op-depth-test.c
URL: http://svn.apache.org/viewvc/subversion/branches/inheritable-props/subversion/tests/libsvn_wc/op-depth-test.c?rev=1360909&r1=1360908&r2=1360909&view=diff
==============================================================================
--- subversion/branches/inheritable-props/subversion/tests/libsvn_wc/op-depth-test.c (original)
+++ subversion/branches/inheritable-props/subversion/tests/libsvn_wc/op-depth-test.c Thu Jul 12 20:24:53 2012
@@ -43,6 +43,7 @@
 #include "private/svn_dep_compat.h"
 #include "../../libsvn_wc/wc.h"
 #include "../../libsvn_wc/wc_db.h"
+#include "../../libsvn_wc/workqueue.h"
 #define SVN_WC__I_AM_WC_DB
 #include "../../libsvn_wc/wc_db_private.h"
 
@@ -1295,7 +1296,11 @@ base_dir_insert_remove(svn_test__sandbox
 
   SVN_ERR(check_db_rows(b, "", after));
 
-  SVN_ERR(svn_wc__db_base_remove(b->wc_ctx->db, dir_abspath, b->pool));
+  SVN_ERR(svn_wc__db_base_remove(b->wc_ctx->db, dir_abspath,
+                                 FALSE, SVN_INVALID_REVNUM,
+                                 NULL, NULL, b->pool));
+  SVN_ERR(svn_wc__wq_run(b->wc_ctx->db, dir_abspath,
+                         NULL, NULL, b->pool));
 
   SVN_ERR(check_db_rows(b, "", before));
 
@@ -2839,8 +2844,9 @@ do_delete(svn_test__sandbox_t *b,
   SVN_ERR(check_db_rows(b, "", before));
   SVN_ERR(check_db_actual(b, actual_before));
   SVN_ERR(svn_wc__db_op_delete(b->wc_ctx->db, local_abspath, NULL,
-                               NULL, NULL /* notification */,
+                               NULL /* conflict */, NULL /* work_item */,
                                NULL, NULL /* cancellation */,
+                               NULL, NULL /* notification */,
                                b->pool));
   SVN_ERR(check_db_rows(b, "", after));
   SVN_ERR(check_db_actual(b, actual_after));
@@ -4871,6 +4877,14 @@ test_follow_moved_to(const svn_test_opts
   SVN_ERR(check_moved_to(moved_tos, 3, 5, "A3/B/C/D/E"));
   SVN_TEST_ASSERT(moved_tos->nelts == 4);
 
+  SVN_ERR(wc_delete(&b, "A3/B/C/D/E"));
+  SVN_ERR(svn_wc__db_follow_moved_to(&moved_tos, b.wc_ctx->db,
+                                     wc_path(&b, "A1/B/C/D/E"), pool, pool));
+  SVN_ERR(check_moved_to(moved_tos, 0, 1, "A3/B/C/D/E"));
+  SVN_ERR(check_moved_to(moved_tos, 1, 2, "A2/B/C/D/E"));
+  SVN_ERR(check_moved_to(moved_tos, 2, 4, "A1/B/C/D/E"));
+  SVN_TEST_ASSERT(moved_tos->nelts == 3);
+
   return SVN_NO_ERROR;
 }
 

Modified: subversion/branches/inheritable-props/subversion/tests/libsvn_wc/wc-queries-test.c
URL: http://svn.apache.org/viewvc/subversion/branches/inheritable-props/subversion/tests/libsvn_wc/wc-queries-test.c?rev=1360909&r1=1360908&r2=1360909&view=diff
==============================================================================
--- subversion/branches/inheritable-props/subversion/tests/libsvn_wc/wc-queries-test.c (original)
+++ subversion/branches/inheritable-props/subversion/tests/libsvn_wc/wc-queries-test.c Thu Jul 12 20:24:53 2012
@@ -69,6 +69,7 @@ static const int schema_statements[] =
   /* Memory tables */
   STMT_CREATE_TARGETS_LIST,
   STMT_CREATE_CHANGELIST_LIST,
+  STMT_CREATE_CHANGELIST_TRIGGER,
   STMT_CREATE_TARGET_PROP_CACHE,
   STMT_CREATE_REVERT_LIST,
   STMT_CREATE_DELETE_LIST,
@@ -88,9 +89,6 @@ static const int slow_statements[] =
   STMT_LOOK_FOR_WORK,
   STMT_HAS_WORKING_NODES,
 
-  /* Need index? */
-  STMT_SELECT_PRISTINE_BY_MD5, /* Only used by deprecated api */
-
   /* Full temporary table read */
   STMT_INSERT_ACTUAL_EMPTIES,
   STMT_SELECT_REVERT_LIST_RECURSIVE,

Modified: subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/benchmark.py
URL: http://svn.apache.org/viewvc/subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/benchmark.py?rev=1360909&r1=1360908&r2=1360909&view=diff
==============================================================================
--- subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/benchmark.py (original)
+++ subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/benchmark.py Thu Jul 12 20:24:53 2012
@@ -17,42 +17,139 @@
 # specific language governing permissions and limitations
 # under the License.
 
-"""
-usage: benchmark.py run <run_file> <levels> <spread> [N]
-       benchmark.py show <run_file>
-       benchmark.py compare <run_file1> <run_file2>
-       benchmark.py combine <new_file> <run_file1> <run_file2> ...
+"""Usage: benchmark.py run|list|compare|show|chart ...
 
-Test data is written to run_file.
-If a run_file exists, data is added to it.
+RUN BENCHMARKS
+
+  benchmark.py run <branch>@<revision>,<levels>x<spread> [N] [options]
+
+Test data is added to an sqlite database created automatically, by default
+'benchmark.db' in the current working directory. To specify a different path,
+use option -f <path_to_db>.
+
+<branch_name> is a label of the svn branch you're testing, e.g. "1.7.x".
+<revision> is the last-changed-revision of above branch.
 <levels> is the number of directory levels to create
 <spread> is the number of child trees spreading off each dir level
 If <N> is provided, the run is repeated N times.
-"""
+
+<branch_name> and <revision> are simply used for later reference. You
+should enter labels matching the selected --svn-bin-dir.
+
+<levels> and <spread> control the way the tested working copy is structured:
+  <levels>: number of directory levels to create.
+  <spread>: number of files and subdirectories created in each dir.
+
+
+LIST WHAT IS ON RECORD
+
+  benchmark.py list [ <branch>@<rev>,<levels>x<spread> ]
+
+Find entries in the database for the given constraints. Any arguments can
+be omitted. (To select only a rev, start with a '@', like '@123'; to select
+only spread, start with an 'x', like "x100".)
+
+Omit all args to get a listing of all available distinct entries.
+
+
+COMPARE TIMINGS
+
+  benchmark.py compare B@R,LxS B@R,LxS
+
+Compare two kinds of timings (in text mode). Each B@R,LxS selects
+timings from branch, revision, WC-levels and -spread by the same labels as
+previously given for a 'run' call. Any elements can be omitted. For example:
+  benchmark.py compare 1.7.0 trunk@1349903
+    Compare the total timings of all combined '1.7.0' branch runs to
+    all combined runs of 'trunk'-at-revision-1349903.
+  benchmark.py compare 1.7.0,5x5 trunk@1349903,5x5
+    Same as above, but only compare the working copy types with 5 levels
+    and a spread of 5.
+
+
+SHOW TIMINGS
+
+  benchmark.py show <branch>@<rev>,<levels>x<spread>
+
+Print out a summary of the timings selected from the given constraints.
+Any arguments can be omitted (like for the 'list' command).
+
+
+GENERATE CHARTS
+
+  benchmark.py chart compare B@R,LxS B@R,LxS [ B@R,LxS ... ]
+
+Produce a bar chart that compares any number of sets of timings. Timing sets
+are supplied by B@R,LxS arguments (i.e. <branch>@<rev>,<levels>x<spread> as
+provided for a 'run' call), where any number of elements may be omitted. The
+less constraints you supply, the more timings are included (try it out with
+the 'list' command). The first set is taken as a reference point for 100% and
++0 seconds. Each following dataset produces a set of labeled bar charts.
+So, at least two constraint arguments must be provided.
+
+Use the -c option to limit charts to specific command names.
+
+
+EXAMPLES
+
+# Run 3 benchmarks on svn 1.7.0. Timings are saved in benchmark.db.
+# Provide label '1.7.0' and its Last-Changed-Rev for later reference.
+# (You may also set your $PATH instead of using --svn-bin-dir.)
+./benchmark.py run --svn-bin-dir ~/svn-prefix/1.7.0/bin 1.7.0@1181106,5x5 3
+
+# Record 3 benchmark runs on trunk, again naming its Last-Changed-Rev.
+./benchmark.py run --svn-bin-dir ~/svn-prefix/trunk/bin trunk@1352725,5x5 3
+
+# Work with the results of above two runs
+./benchmark.py list
+./benchmark.py compare 1.7.0 trunk
+./benchmark.py show 1.7.0 trunk
+./benchmark.py chart compare 1.7.0 trunk
+./benchmark.py chart compare 1.7.0 trunk -c "update,commit,TOTAL RUN"
+
+# Rebuild r1352598, run it and chart improvements since 1.7.0.
+svn up -r1352598 ~/src/trunk
+make -C ~/src/trunk dist-clean install
+export PATH="$HOME/svn-prefix/trunk/bin:$PATH"
+which svn
+./benchmark.py run trunk@1352598,5x5 3
+./benchmark.py chart compare 1.7.0 trunk@1352598 trunk@1352725 -o chart.svg
+
+
+GLOBAL OPTIONS"""
 
 import os
-import sys
+import time
+import datetime
+import sqlite3
+import optparse
 import tempfile
 import subprocess
-import datetime
 import random
 import shutil
-import cPickle
-import optparse
 import stat
+import string
 
+IGNORE_COMMANDS = ('--version', )
 TOTAL_RUN = 'TOTAL RUN'
 
-timings = None
+j = os.path.join
 
-def run_cmd(cmd, stdin=None, shell=False):
+def time_str():
+  return time.strftime('%Y-%m-%d %H:%M:%S');
 
-  if shell:
-    printable_cmd = 'CMD: ' + cmd
-  else:
-    printable_cmd = 'CMD: ' + ' '.join(cmd)
+def timedelta_to_seconds(td):
+  return ( float(td.seconds)
+           + float(td.microseconds) / (10**6)
+           + td.days * 24 * 60 * 60 )
+
+def run_cmd(cmd, stdin=None, shell=False, verbose=False):
   if options.verbose:
-    print printable_cmd
+    if shell:
+      printable_cmd = cmd
+    else:
+      printable_cmd = ' '.join(cmd)
+    print 'CMD:', printable_cmd
 
   if stdin:
     stdin_arg = subprocess.PIPE
@@ -66,584 +163,1029 @@ def run_cmd(cmd, stdin=None, shell=False
                        shell=shell)
   stdout,stderr = p.communicate(input=stdin)
 
-  if options.verbose:
+  if verbose:
     if (stdout):
       print "STDOUT: [[[\n%s]]]" % ''.join(stdout)
   if (stderr):
     print "STDERR: [[[\n%s]]]" % ''.join(stderr)
 
-  return stdout,stderr
+  return stdout, stderr
 
-def timedelta_to_seconds(td):
-  return ( float(td.seconds)
-           + float(td.microseconds) / (10**6)
-           + td.days * 24 * 60 * 60 )
 
+_next_unique_basename_count = 0
 
-class Timings:
+def next_unique_basename(prefix):
+  global _next_unique_basename_count
+  _next_unique_basename_count += 1
+  return '_'.join((prefix, str(_next_unique_basename_count)))
+
+
+si_units = [
+    (1000 ** 5, 'P'),
+    (1000 ** 4, 'T'), 
+    (1000 ** 3, 'G'), 
+    (1000 ** 2, 'M'), 
+    (1000 ** 1, 'K'),
+    (1000 ** 0, ''),
+    ]
+def n_label(n):
+    """(stolen from hurry.filesize)"""
+    for factor, suffix in si_units:
+        if n >= factor:
+            break
+    amount = int(n/factor)
+    if isinstance(suffix, tuple):
+        singular, multiple = suffix
+        if amount == 1:
+            suffix = singular
+        else:
+            suffix = multiple
+    return str(amount) + suffix
+
+
+def split_arg_once(l_r, sep):
+  if not l_r:
+    return (None, None)
+  if sep in l_r:
+    l, r = l_r.split(sep)
+  else:
+    l = l_r
+    r = None
+  if not l:
+    l = None
+  if not r:
+    r = None
+  return (l, r)
+
+RUN_KIND_SEPARATORS=('@', ',', 'x')
+
+class RunKind:
+  def __init__(self, b_r_l_s):
+    b_r, l_s = split_arg_once(b_r_l_s, RUN_KIND_SEPARATORS[1])
+    self.branch, self.revision = split_arg_once(b_r, RUN_KIND_SEPARATORS[0])
+    self.levels, self.spread = split_arg_once(l_s, RUN_KIND_SEPARATORS[2])
+    if self.levels: self.levels = int(self.levels)
+    if self.spread: self.spread = int(self.spread)
+
+    label_parts = []
+    if self.branch:
+      label_parts.append(self.branch)
+    if self.revision:
+      label_parts.append(RUN_KIND_SEPARATORS[0])
+      label_parts.append(self.revision)
+    if self.levels or self.spread:
+      label_parts.append(RUN_KIND_SEPARATORS[1])
+      if self.levels:
+        label_parts.append(str(self.levels))
+      if self.spread:
+        label_parts.append(RUN_KIND_SEPARATORS[2])
+        label_parts.append(str(self.spread))
+    self.label = ''.join(label_parts)
+
+  def args(self):
+    return (self.branch, self.revision, self.levels, self.spread)
+
+
+PATHNAME_VALID_CHARS = "-_.,@%s%s" % (string.ascii_letters, string.digits)
+def filesystem_safe_string(s):
+  return ''.join(c for c in s if c in PATHNAME_VALID_CHARS)
+
+def do_div(ref, val):
+  if ref:
+    return float(val) / float(ref)
+  else:
+    return 0.0
 
-  def __init__(self, *ignore_svn_cmds):
-    self.timings = {}
-    self.current_name = None
+def do_diff(ref, val):
+  return float(val) - float(ref)
+
+
+# ------------------------- database -------------------------
+
+class TimingsDb:
+  def __init__(self, db_path):
+    self.db_path = db_path;
+    self.conn = sqlite3.connect(db_path)
+    self.ensure_tables_created()
+
+  def ensure_tables_created(self):
+    c = self.conn.cursor()
+
+    c.execute("""SELECT name FROM sqlite_master WHERE type='table' AND
+              name='batch'""")
+    if c.fetchone():
+      # exists
+      return
+
+    print 'Creating database tables.'
+    c.executescript('''
+        CREATE TABLE batch (
+          batch_id INTEGER PRIMARY KEY AUTOINCREMENT,
+          started TEXT,
+          ended TEXT
+        );
+
+        CREATE TABLE run_kind (
+          run_kind_id INTEGER PRIMARY KEY AUTOINCREMENT,
+          branch TEXT NOT NULL,
+          revision TEXT NOT NULL,
+          wc_levels INTEGER,
+          wc_spread INTEGER,
+          UNIQUE(branch, revision, wc_levels, wc_spread)
+        );
+
+        CREATE TABLE run (
+          run_id INTEGER PRIMARY KEY AUTOINCREMENT,
+          batch_id INTEGER NOT NULL REFERENCES batch(batch_id),
+          run_kind_id INTEGER NOT NULL REFERENCES run_kind(run_kind_id),
+          started TEXT,
+          ended TEXT,
+          aborted INTEGER
+        );
+
+        CREATE TABLE timings (
+          run_id INTEGER NOT NULL REFERENCES run(run_id),
+          command TEXT NOT NULL,
+          sequence INTEGER,
+          timing REAL
+        );'''
+      )
+    self.conn.commit()
+    c.close();
+
+
+class Batch:
+  def __init__(self, db):
+    self.db = db
+    self.started = time_str()
+    c = db.conn.cursor()
+    c.execute("INSERT INTO batch (started) values (?)", (self.started,))
+    db.conn.commit()
+    self.id = c.lastrowid
+    c.close()
+
+  def done(self):
+    conn = self.db.conn
+    c = conn.cursor()
+    c.execute("""
+        UPDATE batch
+        SET ended = ?
+        WHERE batch_id = ?""",
+        (time_str(), self.id))
+    conn.commit()
+    c.close()
+
+class Run:
+  def __init__(self, batch, run_kind):
+    self.batch = batch
+    conn = self.batch.db.conn
+    c = conn.cursor()
+
+    c.execute("""
+        SELECT run_kind_id FROM run_kind
+        WHERE branch = ?
+          AND revision = ?
+          AND wc_levels = ?
+          AND wc_spread = ?""",
+        run_kind.args())
+    kind_ids = c.fetchone()
+    if kind_ids:
+      kind_id = kind_ids[0]
+    else:
+      c.execute("""
+          INSERT INTO run_kind (branch, revision, wc_levels, wc_spread)
+          VALUES (?, ?, ?, ?)""",
+          run_kind.args())
+      conn.commit()
+      kind_id = c.lastrowid
+
+    self.started = time_str()
+    
+    c.execute("""
+        INSERT INTO run
+          (batch_id, run_kind_id, started)
+        VALUES
+          (?, ?, ?)""",
+        (self.batch.id, kind_id, self.started))
+    conn.commit()
+    self.id = c.lastrowid
+    c.close();
     self.tic_at = None
-    self.ignore = ignore_svn_cmds
-    self.name = None
+    self.current_command = None
+    self.timings = []
 
-  def tic(self, name):
-    if name in self.ignore:
+  def tic(self, command):
+    if command in IGNORE_COMMANDS:
       return
     self.toc()
-    self.current_name = name
+    self.current_command = command
     self.tic_at = datetime.datetime.now()
 
   def toc(self):
-    if self.current_name and self.tic_at:
+    if self.current_command and self.tic_at:
       toc_at = datetime.datetime.now()
-      self.submit_timing(self.current_name,
+      self.remember_timing(self.current_command,
                          timedelta_to_seconds(toc_at - self.tic_at))
-    self.current_name = None
+    self.current_command = None
     self.tic_at = None
 
-  def submit_timing(self, name, seconds):
-    times = self.timings.get(name)
-    if not times:
-      times = []
-      self.timings[name] = times
-    times.append(seconds)
-
-  def min_max_avg(self, name):
-    ttimings = self.timings.get(name)
-    return ( min(ttimings),
-             max(ttimings),
-             reduce(lambda x,y: x + y, ttimings) / len(ttimings) )
-
-  def summary(self):
-    s = []
-    if self.name:
-      s.append('Timings for %s' % self.name)
-    s.append('    N   min     max     avg    operation  (unit is seconds)')
-
-    names = sorted(self.timings.keys())
-
-    for name in names:
-      timings = self.timings.get(name)
-      if not name or not timings: continue
+  def remember_timing(self, command, seconds):
+    self.timings.append((command, seconds))
 
-      tmin, tmax, tavg = self.min_max_avg(name)
-
-      s.append('%5d %7.2f %7.2f %7.2f  %s' % (
-                 len(timings),
-                 tmin,
-                 tmax,
-                 tavg,
-                 name))
-
-    return '\n'.join(s)
-
-
-  def compare_to(self, other, verbose):
-    def do_div(a, b):
-      if b:
-        return float(a) / float(b)
+  def submit_timings(self):
+    conn = self.batch.db.conn
+    c = conn.cursor()
+    print 'submitting...'
+
+    c.executemany("""
+      INSERT INTO timings
+        (run_id, command, sequence, timing)
+      VALUES
+        (?, ?, ?, ?)""",
+      [(self.id, t[0], (i + 1), t[1]) for i,t in enumerate(self.timings)])
+
+    conn.commit()
+    c.close()
+
+  def done(self, aborted=False):
+    conn = self.batch.db.conn
+    c = conn.cursor()
+    c.execute("""
+        UPDATE run
+        SET ended = ?, aborted = ?
+        WHERE run_id = ?""",
+        (time_str(), aborted, self.id))
+    conn.commit()
+    c.close()
+
+
+class TimingQuery:
+  def __init__(self, db, run_kind):
+    self.cursor = db.conn.cursor()
+    self.constraints = []
+    self.values = []
+    self.timings = None
+    self.FROM_WHERE = """
+         FROM batch AS b,
+              timings AS t,
+              run AS r,
+              run_kind as k
+         WHERE
+              t.run_id = r.run_id
+              AND k.run_kind_id = r.run_kind_id
+              AND b.batch_id = r.batch_id
+              AND r.aborted = 0
+         """
+    self.append_constraint('k', 'branch', run_kind.branch)
+    self.append_constraint('k', 'revision', run_kind.revision)
+    self.append_constraint('k', 'wc_levels', run_kind.levels)
+    self.append_constraint('k', 'wc_spread', run_kind.spread)
+    self.label = run_kind.label
+
+  def append_constraint(self, table, name, val):
+    if val:
+      self.constraints.append('AND %s.%s = ?' % (table, name))
+      self.values.append(val)
+
+  def remove_last_constraint(self):
+    del self.constraints[-1]
+    del self.values[-1]
+
+  def get_sorted_X(self, x, n=1):
+    query = ['SELECT DISTINCT %s' % x,
+             self.FROM_WHERE ]
+    query.extend(self.constraints)
+    query.append('ORDER BY %s' % x)
+    c = db.conn.cursor()
+    try:
+      #print ' '.join(query)
+      c.execute(' '.join(query), self.values)
+      if n == 1:
+        return [tpl[0] for tpl in c.fetchall()]
       else:
-        return 0.0
-
-    def do_diff(a, b):
-      return float(a) - float(b)
-
-    selfname = self.name
-    if not selfname:
-      selfname = 'unnamed'
-    othername = other.name
-    if not othername:
-      othername = 'the other'
-
-    selftotal = self.min_max_avg(TOTAL_RUN)[2]
-    othertotal = other.min_max_avg(TOTAL_RUN)[2]
-
-    s = ['COMPARE %s to %s' % (othername, selfname)]
-
-    if TOTAL_RUN in self.timings and TOTAL_RUN in other.timings:
-      s.append('  %s timings: %5.1f seconds avg for %s'
-               % (TOTAL_RUN, othertotal, othername))
-      s.append('  %s          %5.1f seconds avg for %s'
-               % (' ' * len(TOTAL_RUN), selftotal, selfname))
-
-
-    if not verbose:
-      s.append('      avg         operation')
-    else:
-      s.append('      min              max              avg         operation')
+        return c.fetchall()
+    finally:
+      c.close()
 
-    names = sorted(self.timings.keys())
+  def get_sorted_command_names(self):
+    return self.get_sorted_X('t.command')
 
-    for name in names:
-      if not name in other.timings:
-        continue
+  def get_sorted_branches(self):
+    return self.get_sorted_X('k.branch')
 
+  def get_sorted_revisions(self):
+    return self.get_sorted_X('k.revision')
+
+  def get_sorted_levels_spread(self):
+    return self.get_sorted_X('k.wc_levels,k.wc_spread', n = 2)
+
+  def count_runs_batches(self):
+    query = ["""SELECT
+                  count(DISTINCT r.run_id),
+                  count(DISTINCT b.batch_id)""",
+             self.FROM_WHERE ]
+    query.extend(self.constraints)
+    c = db.conn.cursor()
+    try:
+      #print ' '.join(query)
+      c.execute(' '.join(query), self.values)
+      return c.fetchone()
+    finally:
+      c.close()
 
-      min_me, max_me, avg_me = self.min_max_avg(name)
-      min_other, max_other, avg_other = other.min_max_avg(name)
+  def get_command_timings(self, command):
+    query = ["""SELECT
+                  count(t.timing),
+                  min(t.timing),
+                  max(t.timing),
+                  avg(t.timing)""",
+             self.FROM_WHERE ]
+    self.append_constraint('t', 'command', command)
+    try:
+      query.extend(self.constraints)
+      c = db.conn.cursor()
+      try:
+        c.execute(' '.join(query), self.values)
+        return c.fetchone()
+      finally:
+        c.close()
+    finally:
+      self.remove_last_constraint()
 
-      avg_str = '%7.2f|%+7.3f' % (do_div(avg_me, avg_other),
-                                  do_diff(avg_me, avg_other))
+  def get_timings(self):
+    if self.timings:
+      return self.timings
+    self.timings = {}
+    for command_name in self.get_sorted_command_names():
+      self.timings[command_name] = self.get_command_timings(command_name)
+    return self.timings
+      
 
-      if not verbose:
-        s.append('%-16s  %s' % (avg_str, name))
-      else:
-        min_str = '%7.2f|%+7.3f' % (do_div(min_me, min_other),
-                                    do_diff(min_me, min_other))
-        max_str = '%7.2f|%+7.3f' % (do_div(max_me, max_other),
-                                    do_diff(max_me, max_other))
-
-        s.append('%-16s %-16s %-16s  %s' % (min_str, max_str, avg_str, name))
-
-    s.extend([
-      '(legend: "1.23|+0.45" means: slower by factor 1.23 and by 0.45 seconds;',
-      ' factor < 1 and difference < 0 means \'%s\' is faster than \'%s\')'
-      % (self.name, othername)])
-
-    return '\n'.join(s)
-
-
-  def add(self, other):
-    for name, other_times in other.timings.items():
-      my_times = self.timings.get(name)
-      if not my_times:
-        my_times = []
-        self.timings[name] = my_times
-      my_times.extend(other_times)
+# ------------------------------------------------------------ run tests
 
 
+def perform_run(batch, run_kind,
+                svn_bin, svnadmin_bin, verbose):
 
+  run = Run(batch, run_kind)
 
-j = os.path.join
+  def create_tree(in_dir, _levels, _spread):
+    try:
+      os.mkdir(in_dir)
+    except:
+      pass
+
+    for i in range(_spread):
+      # files
+      fn = j(in_dir, next_unique_basename('file'))
+      f = open(fn, 'w')
+      f.write('This is %s\n' % fn)
+      f.close()
+
+      # dirs
+      if (_levels > 1):
+        dn = j(in_dir, next_unique_basename('dir'))
+        create_tree(dn, _levels - 1, _spread)
+
+  def svn(*args):
+    name = args[0]
+
+    cmd = [ svn_bin ]
+    cmd.extend( list(args) )
+    if verbose:
+      print 'svn cmd:', ' '.join(cmd)
+
+    stdin = None
+    if stdin:
+      stdin_arg = subprocess.PIPE
+    else:
+      stdin_arg = None
 
-_create_count = 0
+    run.tic(name)
+    try:
+      p = subprocess.Popen(cmd,
+                           stdin=stdin_arg,
+                           stdout=subprocess.PIPE,
+                           stderr=subprocess.PIPE,
+                           shell=False)
+      stdout,stderr = p.communicate(input=stdin)
+    except OSError:
+      stdout = stderr = None
+    finally:
+      run.toc()
 
-def next_name(prefix):
-  global _create_count
-  _create_count += 1
-  return '_'.join((prefix, str(_create_count)))
+    if verbose:
+      if (stdout):
+        print "STDOUT: [[[\n%s]]]" % ''.join(stdout)
+      if (stderr):
+        print "STDERR: [[[\n%s]]]" % ''.join(stderr)
 
-def create_tree(in_dir, levels, spread=5):
-  try:
-    os.mkdir(in_dir)
-  except:
-    pass
-
-  for i in range(spread):
-    # files
-    fn = j(in_dir, next_name('file'))
-    f = open(fn, 'w')
-    f.write('This is %s\n' % fn)
-    f.close()
+    return stdout,stderr
 
-    # dirs
-    if (levels > 1):
-      dn = j(in_dir, next_name('dir'))
-      create_tree(dn, levels - 1, spread)
 
+  def add(*args):
+    return svn('add', *args)
 
-def svn(*args):
-  name = args[0]
+  def ci(*args):
+    return svn('commit', '-mm', *args)
 
-  ### options comes from the global namespace; it should be passed
-  cmd = [options.svn] + list(args)
-  if options.verbose:
-    print 'svn cmd:', ' '.join(cmd)
+  def up(*args):
+    return svn('update', *args)
 
-  stdin = None
-  if stdin:
-    stdin_arg = subprocess.PIPE
-  else:
-    stdin_arg = None
+  def st(*args):
+    return svn('status', *args)
 
-  ### timings comes from the global namespace; it should be passed
-  timings.tic(name)
-  try:
-    p = subprocess.Popen(cmd,
-                         stdin=stdin_arg,
-                         stdout=subprocess.PIPE,
-                         stderr=subprocess.PIPE,
-                         shell=False)
-    stdout,stderr = p.communicate(input=stdin)
-  except OSError:
-    stdout = stderr = None
-  finally:
-    timings.toc()
+  def info(*args):
+    return svn('info', *args)
 
-  if options.verbose:
-    if (stdout):
-      print "STDOUT: [[[\n%s]]]" % ''.join(stdout)
-    if (stderr):
-      print "STDERR: [[[\n%s]]]" % ''.join(stderr)
+  _chars = [chr(x) for x in range(ord('a'), ord('z') +1)]
 
-  return stdout,stderr
+  def randstr(len=8):
+    return ''.join( [random.choice(_chars) for i in range(len)] )
 
+  def _copy(path):
+    dest = next_unique_basename(path + '_copied')
+    svn('copy', path, dest)
 
-def add(*args):
-  return svn('add', *args)
+  def _move(path):
+    dest = path + '_moved'
+    svn('move', path, dest)
 
-def ci(*args):
-  return svn('commit', '-mm', *args)
+  def _propmod(path):
+    so, se = svn('proplist', path)
+    propnames = [line.strip() for line in so.strip().split('\n')[1:]]
 
-def up(*args):
-  return svn('update', *args)
+    # modify?
+    if len(propnames):
+      svn('ps', propnames[len(propnames) / 2], randstr(), path)
 
-def st(*args):
-  return svn('status', *args)
+    # del?
+    if len(propnames) > 1:
+      svn('propdel', propnames[len(propnames) / 2], path)
 
-def info(*args):
-  return svn('info', *args)
+  def _propadd(path):
+    # set a new one.
+    svn('propset', randstr(), randstr(), path)
 
-_chars = [chr(x) for x in range(ord('a'), ord('z') +1)]
+  def _mod(path):
+    if os.path.isdir(path):
+      _propmod(path)
+      return
 
-def randstr(len=8):
-  return ''.join( [random.choice(_chars) for i in range(len)] )
+    f = open(path, 'a')
+    f.write('\n%s\n' % randstr())
+    f.close()
 
-def _copy(path):
-  dest = next_name(path + '_copied')
-  svn('copy', path, dest)
+  def _add(path):
+    if os.path.isfile(path):
+      return _mod(path)
+
+    if random.choice((True, False)):
+      # create a dir
+      svn('mkdir', j(path, next_unique_basename('new_dir')))
+    else:
+      # create a file
+      new_path = j(path, next_unique_basename('new_file'))
+      f = open(new_path, 'w')
+      f.write(randstr())
+      f.close()
+      svn('add', new_path)
+
+  def _del(path):
+    svn('delete', path)
+
+  _mod_funcs = (_mod, _add, _propmod, _propadd, )#_copy,) # _move, _del)
+
+  def modify_tree(in_dir, fraction):
+    child_names = os.listdir(in_dir)
+    for child_name in child_names:
+      if child_name[0] == '.':
+        continue
+      if random.random() < fraction:
+        path = j(in_dir, child_name)
+        random.choice(_mod_funcs)(path)
 
-def _move(path):
-  dest = path + '_moved'
-  svn('move', path, dest)
+    for child_name in child_names:
+      if child_name[0] == '.': continue
+      path = j(in_dir, child_name)
+      if os.path.isdir(path):
+        modify_tree(path, fraction)
 
-def _propmod(path):
-  so, se = svn('proplist', path)
-  propnames = [line.strip() for line in so.strip().split('\n')[1:]]
+  def propadd_tree(in_dir, fraction):
+    for child_name in os.listdir(in_dir):
+      if child_name[0] == '.': continue
+      path = j(in_dir, child_name)
+      if random.random() < fraction:
+        _propadd(path)
+      if os.path.isdir(path):
+        propadd_tree(path, fraction)
+
+
+  def rmtree_onerror(func, path, exc_info):
+    """Error handler for ``shutil.rmtree``.
+
+    If the error is due to an access error (read only file)
+    it attempts to add write permission and then retries.
+
+    If the error is for another reason it re-raises the error.
+
+    Usage : ``shutil.rmtree(path, onerror=onerror)``
+    """
+    if not os.access(path, os.W_OK):
+      # Is the error an access error ?
+      os.chmod(path, stat.S_IWUSR)
+      func(path)
+    else:
+      raise
 
-  # modify?
-  if len(propnames):
-    svn('ps', propnames[len(propnames) / 2], randstr(), path)
+  base = tempfile.mkdtemp()
 
-  # del?
-  if len(propnames) > 1:
-    svn('propdel', propnames[len(propnames) / 2], path)
+  # ensure identical modifications for every run
+  random.seed(0)
 
+  aborted = True
 
-def _propadd(path):
-  # set a new one.
-  svn('propset', randstr(), randstr(), path)
+  try:
+    repos = j(base, 'repos')
+    repos = repos.replace('\\', '/')
+    wc = j(base, 'wc')
+    wc2 = j(base, 'wc2')
 
+    if repos.startswith('/'):
+      file_url = 'file://%s' % repos
+    else:
+      file_url = 'file:///%s' % repos
 
-def _mod(path):
-  if os.path.isdir(path):
-    return _propmod(path)
+    print '\nRunning svn benchmark in', base
+    print 'dir levels: %s; new files and dirs per leaf: %s' %(
+          run_kind.levels, run_kind.spread)
 
-  f = open(path, 'a')
-  f.write('\n%s\n' % randstr())
-  f.close()
+    started = datetime.datetime.now()
 
-def _add(path):
-  if os.path.isfile(path):
-    return _mod(path)
+    try:
+      run_cmd([svnadmin_bin, 'create', repos])
+      svn('checkout', file_url, wc)
 
-  if random.choice((True, False)):
-    # create a dir
-    svn('mkdir', j(path, next_name('new_dir')))
-  else:
-    # create a file
-    new_path = j(path, next_name('new_file'))
-    f = open(new_path, 'w')
-    f.write(randstr())
-    f.close()
-    svn('add', new_path)
+      trunk = j(wc, 'trunk')
+      create_tree(trunk, run_kind.levels, run_kind.spread)
+      add(trunk)
+      st(wc)
+      ci(wc)
+      up(wc)
+      propadd_tree(trunk, 0.05)
+      ci(wc)
+      up(wc)
+      st(wc)
+      info('-R', wc)
+
+      trunk_url = file_url + '/trunk'
+      branch_url = file_url + '/branch'
+
+      svn('copy', '-mm', trunk_url, branch_url)
+      st(wc)
+
+      up(wc)
+      st(wc)
+      info('-R', wc)
+
+      svn('checkout', trunk_url, wc2)
+      st(wc2)
+      modify_tree(wc2, 0.5)
+      st(wc2)
+      ci(wc2)
+      up(wc2)
+      up(wc)
+
+      svn('switch', branch_url, wc2)
+      modify_tree(wc2, 0.5)
+      st(wc2)
+      info('-R', wc2)
+      ci(wc2)
+      up(wc2)
+      up(wc)
+
+      modify_tree(trunk, 0.5)
+      st(wc)
+      ci(wc)
+      up(wc2)
+      up(wc)
+
+      svn('merge', '--accept=postpone', trunk_url, wc2)
+      st(wc2)
+      info('-R', wc2)
+      svn('resolve', '--accept=mine-conflict', wc2)
+      st(wc2)
+      svn('resolved', '-R', wc2)
+      st(wc2)
+      info('-R', wc2)
+      ci(wc2)
+      up(wc2)
+      up(wc)
+
+      svn('merge', '--accept=postpone', '--reintegrate', branch_url, trunk)
+      st(wc)
+      svn('resolve', '--accept=mine-conflict', wc)
+      st(wc)
+      svn('resolved', '-R', wc)
+      st(wc)
+      ci(wc)
+      up(wc2)
+      up(wc)
+
+      svn('delete', j(wc, 'branch'))
+      ci(wc)
+      up(wc)
 
-def _del(path):
-  svn('delete', path)
+      aborted = False
 
-_mod_funcs = (_mod, _add, _propmod, _propadd, )#_copy,) # _move, _del)
+    finally:
+      stopped = datetime.datetime.now()
+      print '\nDone with svn benchmark in', (stopped - started)
 
-def modify_tree(in_dir, fraction):
-  child_names = os.listdir(in_dir)
-  for child_name in child_names:
-    if child_name[0] == '.':
-      continue
-    if random.random() < fraction:
-      path = j(in_dir, child_name)
-      random.choice(_mod_funcs)(path)
+      run.remember_timing(TOTAL_RUN,
+                        timedelta_to_seconds(stopped - started))
+  finally:
+    run.done(aborted)
+    run.submit_timings()
+    shutil.rmtree(base, onerror=rmtree_onerror)
 
-  for child_name in child_names:
-    if child_name[0] == '.': continue
-    path = j(in_dir, child_name)
-    if os.path.isdir(path):
-      modify_tree(path, fraction)
+  return aborted
 
-def propadd_tree(in_dir, fraction):
-  for child_name in os.listdir(in_dir):
-    if child_name[0] == '.': continue
-    path = j(in_dir, child_name)
-    if random.random() < fraction:
-      _propadd(path)
-    if os.path.isdir(path):
-      propadd_tree(path, fraction)
 
+# ---------------------------------------------------------------------
 
-def rmtree_onerror(func, path, exc_info):
-  """Error handler for ``shutil.rmtree``.
+    
+def cmdline_run(db, options, run_kind_str, N=1):
+  run_kind = RunKind(run_kind_str)
+  N = int(N)
 
-  If the error is due to an access error (read only file)
-  it attempts to add write permission and then retries.
+  print 'Hi, going to run a Subversion benchmark series of %d runs...' % N
+  print 'Label is %s' % run_kind.label
 
-  If the error is for another reason it re-raises the error.
+  # can we run the svn binaries?
+  svn_bin = j(options.svn_bin_dir, 'svn')
+  svnadmin_bin = j(options.svn_bin_dir, 'svnadmin')
+
+  for b in (svn_bin, svnadmin_bin):
+    so,se = run_cmd([b, '--version'])
+    if not so:
+      print "Can't run", b
+      exit(1)
 
-  Usage : ``shutil.rmtree(path, onerror=onerror)``
-  """
-  if not os.access(path, os.W_OK):
-    # Is the error an access error ?
-    os.chmod(path, stat.S_IWUSR)
-    func(path)
-  else:
-    raise
+    print ', '.join([s.strip() for s in so.split('\n')[:2]])
 
+  batch = Batch(db)
 
-def run(levels, spread, N):
   for i in range(N):
-    base = tempfile.mkdtemp()
+    print 'Run %d of %d' % (i + 1, N)
+    perform_run(batch, run_kind,
+                svn_bin, svnadmin_bin, options.verbose)
+
+  batch.done()
+
+
+def cmdline_list(db, options, run_kind_str=None):
+  run_kind = RunKind(run_kind_str)
+
+  constraints = []
+  def add_if_not_none(name, val):
+    if val:
+      constraints.append('  %s = %s' % (name, val))
+  add_if_not_none('branch', run_kind.branch)
+  add_if_not_none('revision', run_kind.revision)
+  add_if_not_none('levels', run_kind.levels)
+  add_if_not_none('spread', run_kind.spread)
+  if constraints:
+    print 'For\n', '\n'.join(constraints)
+  print 'I found:'
+
+  d = TimingQuery(db, run_kind)
+  
+  cmd_names = d.get_sorted_command_names()
+  if cmd_names:
+    print '\n%d command names:\n ' % len(cmd_names), '\n  '.join(cmd_names)
+
+  branches = d.get_sorted_branches()
+  if branches and (len(branches) > 1 or branches[0] != run_kind.branch):
+    print '\n%d branches:\n ' % len(branches), '\n  '.join(branches)
+
+  revisions = d.get_sorted_revisions()
+  if revisions and (len(revisions) > 1 or revisions[0] != run_kind.revision):
+    print '\n%d revisions:\n ' % len(revisions), '\n  '.join(revisions)
+
+  levels_spread = d.get_sorted_levels_spread()
+  if levels_spread and (
+       len(levels_spread) > 1
+       or levels_spread[0] != (run_kind.levels, run_kind.spread)):
+    print '\n%d kinds of levels x spread:\n ' % len(levels_spread), '\n  '.join(
+            [ ('%dx%d' % (l, s)) for l,s in levels_spread ])
+
+  print "\n%d runs in %d batches.\n" % (d.count_runs_batches())
+
+
+def cmdline_show(db, options, *run_kind_strings):
+  for run_kind_str in run_kind_strings:
+    run_kind = RunKind(run_kind_str)
 
-    # ensure identical modifications for every run
-    random.seed(0)
+    q = TimingQuery(db, run_kind)
+    timings = q.get_timings()
 
-    try:
-      repos = j(base, 'repos')
-      repos = repos.replace('\\', '/')
-      wc = j(base, 'wc')
-      wc2 = j(base, 'wc2')
-
-      if repos.startswith('/'):
-        file_url = 'file://%s' % repos
-      else:
-        file_url = 'file:///%s' % repos
-
-      so, se = svn('--version')
-      if not so:
-        ### options comes from the global namespace; it should be passed
-        print "Can't find svn at", options.svn
-        exit(1)
-      version = ', '.join([s.strip() for s in so.split('\n')[:2]])
-
-      print '\nRunning svn benchmark in', base
-      print 'dir levels: %s; new files and dirs per leaf: %s; run %d of %d' %(
-            levels, spread, i + 1, N)
-
-      print version
-      started = datetime.datetime.now()
-
-      try:
-        run_cmd(['svnadmin', 'create', repos])
-        svn('checkout', file_url, wc)
-
-        trunk = j(wc, 'trunk')
-        create_tree(trunk, levels, spread)
-        add(trunk)
-        st(wc)
-        ci(wc)
-        up(wc)
-        propadd_tree(trunk, 0.5)
-        ci(wc)
-        up(wc)
-        st(wc)
-        info('-R', wc)
-
-        trunk_url = file_url + '/trunk'
-        branch_url = file_url + '/branch'
-
-        svn('copy', '-mm', trunk_url, branch_url)
-        st(wc)
-
-        up(wc)
-        st(wc)
-        info('-R', wc)
-
-        svn('checkout', trunk_url, wc2)
-        st(wc2)
-        modify_tree(wc2, 0.5)
-        st(wc2)
-        ci(wc2)
-        up(wc2)
-        up(wc)
-
-        svn('switch', branch_url, wc2)
-        modify_tree(wc2, 0.5)
-        st(wc2)
-        info('-R', wc2)
-        ci(wc2)
-        up(wc2)
-        up(wc)
-
-        modify_tree(trunk, 0.5)
-        st(wc)
-        ci(wc)
-        up(wc2)
-        up(wc)
-
-        svn('merge', '--accept=postpone', trunk_url, wc2)
-        st(wc2)
-        info('-R', wc2)
-        svn('resolve', '--accept=mine-conflict', wc2)
-        st(wc2)
-        svn('resolved', '-R', wc2)
-        st(wc2)
-        info('-R', wc2)
-        ci(wc2)
-        up(wc2)
-        up(wc)
-
-        svn('merge', '--accept=postpone', '--reintegrate', branch_url, trunk)
-        st(wc)
-        svn('resolve', '--accept=mine-conflict', wc)
-        st(wc)
-        svn('resolved', '-R', wc)
-        st(wc)
-        ci(wc)
-        up(wc2)
-        up(wc)
-
-        svn('delete', j(wc, 'branch'))
-        ci(wc)
-        up(wc2)
-        up(wc)
+    s = []
+    s.append('Timings for %s' % run_kind.label)
+    s.append('   N    min     max     avg   operation  (unit is seconds)')
 
+    for command_name in q.get_sorted_command_names():
+      if options.command_names and command_name not in options.command_names:
+        continue
+      n, tmin, tmax, tavg = timings[command_name]
 
-      finally:
-        stopped = datetime.datetime.now()
-        print '\nDone with svn benchmark in', (stopped - started)
+      s.append('%4s %7.2f %7.2f %7.2f  %s' % (
+                 n_label(n),
+                 tmin,
+                 tmax,
+                 tavg,
+                 command_name))
 
-        ### timings comes from the global namespace; it should be passed
-        timings.submit_timing(TOTAL_RUN,
-                              timedelta_to_seconds(stopped - started))
-
-        # rename ps to prop mod
-        if timings.timings.get('ps'):
-          has = timings.timings.get('prop mod')
-          if not has:
-            has = []
-            timings.timings['prop mod'] = has
-          has.extend( timings.timings['ps'] )
-          del timings.timings['ps']
+    print '\n'.join(s)
 
-        print timings.summary()
-    finally:
-      shutil.rmtree(base, onerror=rmtree_onerror)
 
+def cmdline_compare(db, options, left_str, right_str):
+  left_kind = RunKind(left_str)
+  right_kind = RunKind(right_str)
 
-def read_from_file(file_path):
-  f = open(file_path, 'rb')
-  try:
-    instance = cPickle.load(f)
-    instance.name = os.path.basename(file_path)
-  finally:
-    f.close()
-  return instance
+  leftq = TimingQuery(db, left_kind)
+  left = leftq.get_timings()
+  if not left:
+    print "No timings for", left_kind.label
+    exit(1)
 
+  rightq = TimingQuery(db, right_kind)
+  right = rightq.get_timings()
+  if not right:
+    print "No timings for", right_kind.label
+    exit(1)
 
-def write_to_file(file_path, instance):
-  f = open(file_path, 'wb')
-  cPickle.dump(instance, f)
-  f.close()
+  label = 'Compare %s to %s' % (left_kind.label, right_kind.label)
 
-def cmd_compare(path1, path2):
-  t1 = read_from_file(path1)
-  t2 = read_from_file(path2)
+  s = [label]
 
-  if options.verbose:
-    print t1.summary()
-    print '---'
-    print t2.summary()
-    print '---'
-  print t2.compare_to(t1, options.verbose)
-
-def cmd_combine(dest, *paths):
-  total = Timings('--version');
-
-  for path in paths:
-    t = read_from_file(path)
-    total.add(t)
-
-  print total.summary()
-  write_to_file(dest, total)
-
-def cmd_run(timings_path, levels, spread, N=1):
-  levels = int(levels)
-  spread = int(spread)
-  N = int(N)
-
-  print '\n\nHi, going to run a Subversion benchmark series of %d runs...' % N
-
-  ### UGH! should pass to run()
-  ### neels: Today I contemplated doing that, but at the end of the day
-  ###        it merely blows up the code without much benefit. If this
-  ###        ever becomes part of an imported python package, call again.
-  global timings
-
-  if os.path.isfile(timings_path):
-    print 'Going to add results to existing file', timings_path
-    timings = read_from_file(timings_path)
+  verbose = options.verbose
+  if not verbose:
+    s.append('       N        avg         operation')
   else:
-    print 'Going to write results to new file', timings_path
-    timings = Timings('--version')
-
-  run(levels, spread, N)
+    s.append('       N        min              max              avg         operation')
 
-  write_to_file(timings_path, timings)
+  command_names = [name for name in leftq.get_sorted_command_names()
+                   if name in right]
+  if options.command_names:
+    command_names = [name for name in command_names
+                     if name in options.command_names]
+
+  for command_name in command_names:
+    left_N, left_min, left_max, left_avg = left[command_name]
+    right_N, right_min, right_max, right_avg = right[command_name]
+
+    N_str = '%s/%s' % (n_label(left_N), n_label(right_N))
+    avg_str = '%7.2f|%+7.3f' % (do_div(left_avg, right_avg),
+                                do_diff(left_avg, right_avg))
 
-def cmd_show(*paths):
-  for timings_path in paths:
-    timings = read_from_file(timings_path)
-    print '---\n%s' % timings_path
-    print timings.summary()
+    if not verbose:
+      s.append('%9s %-16s  %s' % (N_str, avg_str, command_name))
+    else:
+      min_str = '%7.2f|%+7.3f' % (do_div(left_min, right_min),
+                                  do_diff(left_min, right_min))
+      max_str = '%7.2f|%+7.3f' % (do_div(left_max, right_max),
+                                  do_diff(left_max, right_max))
+
+      s.append('%9s %-16s %-16s %-16s  %s' % (N_str, min_str, max_str, avg_str,
+                                          command_name))
+
+  s.extend([
+    '(legend: "1.23|+0.45" means: slower by factor 1.23 and by 0.45 seconds;',
+    ' factor < 1 and seconds < 0 means \'%s\' is faster.'
+    % right_kind.label,
+    ' "2/3" means: \'%s\' has 2 timings on record, the other has 3.)'
+    % left_kind.label
+    ])
+
+
+  print '\n'.join(s)
+
+
+# ------------------------------------------------------- charts
+
+def cmdline_chart_compare(db, options, *args):
+  import numpy as np
+  import matplotlib.pyplot as plt
+
+  labels = []
+  timing_sets = []
+  command_names = None
+
+  for arg in args:
+    run_kind = RunKind(arg)
+    query = TimingQuery(db, run_kind)
+    timings = query.get_timings()
+    if not timings:
+      print "No timings for", run_kind.label
+      exit(1)
+    labels.append(run_kind.label)
+    timing_sets.append(timings)
 
+    if command_names:
+      for i in range(len(command_names)):
+        if not command_names[i] in timings:
+          del command_names[i]
+    else:
+      command_names = query.get_sorted_command_names()
 
-def usage():
-  print __doc__
+  if options.command_names:
+    command_names = [name for name in command_names
+                     if name in options.command_names]
+
+  chart_path = options.chart_path
+  if not chart_path:
+    chart_path = 'compare_' + '_'.join(
+      [ filesystem_safe_string(l) for l in labels ]
+      ) + '.svg'
+                  
+  print '\nwriting chart file:', chart_path
+
+  N = len(command_names)
+  M = len(timing_sets) - 1
+
+  ind = np.arange(N)  # the x locations for the groups
+  width = 1. / (1.2 + M)     # the width of the bars
+  dist = 0.15
+
+  fig = plt.figure(figsize=(0.33*N*M,12))
+  plot1 = fig.add_subplot(211)
+  plot2 = fig.add_subplot(212)
+
+  # invisible lines that make sure the scale doesn't get minuscule
+  plot1.axhline(y=101, color='white', linewidth=0.01)
+  plot1.axhline(y=95.0, color='white', linewidth=0.01)
+  plot2.axhline(y=0.1, color='white', linewidth=0.01)
+  plot2.axhline(y=-0.5, color='white', linewidth=0.01)
+
+  reference = timing_sets[0]
+
+  ofs = 0
+
+  for label_i in range(1, len(labels)):
+    timings = timing_sets[label_i]
+    divs = []
+    diffs = []
+    divs_color = []
+    deviations = []
+    for command_name in command_names:
+      ref_N, ref_min, ref_max, ref_avg = reference[command_name]
+      this_N, this_min, this_max, this_avg = timings[command_name]
+
+      val = 100. * (do_div(ref_avg, this_avg) - 1.0)
+      if val < 0:
+        col = '#55dd55'
+      else:
+        col = '#dd5555'
+      divs.append(val)
+      divs_color.append(col)
+      diffs.append( do_diff(ref_avg, this_avg) )
+      deviations.append(this_max / this_min)
+
+    rects = plot1.bar(ind + ofs, divs, width * (1.0 - dist),
+                      color=divs_color, bottom=100.0, edgecolor='none')
+
+    for i in range(len(rects)):
+      x = rects[i].get_x() + width / 2.2
+      div = divs[i]
+      label = labels[label_i]
+
+      plot1.text(x, 100.,
+                 ' %+5.1f%% %s' % (div,label),
+                 ha='center', va='top', size='small',
+                 rotation=-90, family='monospace')
+
+    rects = plot2.bar(ind + ofs, diffs, width * 0.9,
+                   color=divs_color, bottom=0.0, edgecolor='none')
+
+    for i in range(len(rects)):
+      x = rects[i].get_x() + width / 2.2
+      diff = diffs[i]
+      label = labels[label_i]
+
+      plot2.text(x, 0.,
+                 ' %+5.2fs %s' % (diff,label),
+                 ha='center', va='top', size='small',
+                 rotation=-90, family='monospace')
+
+    ofs += width
+
+  plot1.set_title('Speed change compared to %s [%%]' % labels[0])
+  plot1.set_xticks(ind + (width / 2.))
+  plot1.set_xticklabels(command_names, rotation=-55,
+                        horizontalalignment='left',
+                        size='x-small', weight='bold')
+  plot1.axhline(y=100.0, color='#555555', linewidth=0.2)
+  plot2.set_title('[seconds]')
+  plot2.set_xticks(ind + (width / 2.))
+  plot2.set_xticklabels(command_names, rotation=-55,
+                        horizontalalignment='left',
+                        size='medium', weight='bold')
+  plot2.axhline(y=0.0, color='#555555', linewidth=0.2)
+
+  margin = 1.5/(N*M)
+  fig.subplots_adjust(bottom=0.1, top=0.97,
+                      left=margin,
+                      right=1.0-(margin / 2.))
+
+  #plot1.legend( (rects1[0], rects2[0]), (left_label, right_label) )
+
+  #plt.show()
+  plt.savefig(chart_path)
+
+# ------------------------------------------------------------ main
+
+
+# Custom option formatter, keeping newlines in the description.
+# adapted from:
+# http://groups.google.com/group/comp.lang.python/msg/09f28e26af0699b1
+import textwrap
+class IndentedHelpFormatterWithNL(optparse.IndentedHelpFormatter):
+  def format_description(self, description):
+    if not description: return ""
+    desc_width = self.width - self.current_indent
+    indent = " "*self.current_indent
+    bits = description.split('\n')
+    formatted_bits = [
+      textwrap.fill(bit,
+        desc_width,
+        initial_indent=indent,
+        subsequent_indent=indent)
+      for bit in bits]
+    result = "\n".join(formatted_bits) + "\n"
+    return result 
 
 if __name__ == '__main__':
-  parser = optparse.OptionParser()
+  parser = optparse.OptionParser(formatter=IndentedHelpFormatterWithNL())
   # -h is automatically added.
   ### should probably expand the help for that. and see about -?
   parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
                     help='Verbose operation')
-  parser.add_option('--svn', action='store', dest='svn', default='svn',
-                    help='Specify Subversion executable to use')
+  parser.add_option('-b', '--svn-bin-dir', action='store', dest='svn_bin_dir',
+                    default='',
+                    help='Specify directory to find Subversion binaries in')
+  parser.add_option('-f', '--db-path', action='store', dest='db_path',
+                    default='benchmark.db',
+                    help='Specify path to SQLite database file')
+  parser.add_option('-o', '--chart-path', action='store', dest='chart_path',
+                    help='Supply a path for chart output.')
+  parser.add_option('-c', '--command-names', action='store',
+                    dest='command_names',
+                    help='Comma separated list of command names to limit to.')
+
+  parser.set_description(__doc__)
+  parser.set_usage('')
 
-  ### should start passing this, but for now: make it global
-  global options
 
   options, args = parser.parse_args()
 
+  def usage(msg=None):
+    parser.print_help()
+    if msg:
+      print
+      print msg
+    exit(1)
+
   # there should be at least one arg left: the sub-command
   if not args:
-    usage()
-    exit(1)
+    usage('No command argument supplied.')
 
   cmd = args[0]
   del args[0]
 
-  if cmd == 'compare':
-    if len(args) != 2:
-      usage()
-      exit(1)
-    cmd_compare(*args)
+  db = TimingsDb(options.db_path)
 
-  elif cmd == 'combine':
-    if len(args) < 3:
+  if cmd == 'run':
+    if len(args) < 1 or len(args) > 2:
       usage()
-      exit(1)
-    cmd_combine(*args)
+    cmdline_run(db, options, *args)
 
-  elif cmd == 'run':
-    if len(args) < 3 or len(args) > 4:
+  elif cmd == 'compare':
+    if len(args) < 2:
       usage()
-      exit(1)
-    cmd_run(*args)
+    cmdline_compare(db, options, *args)
+
+  elif cmd == 'list':
+    cmdline_list(db, options, *args)
 
   elif cmd == 'show':
-    if not args:
+    cmdline_show(db, options, *args)
+
+  elif cmd == 'chart':
+    if 'compare'.startswith(args[0]):
+      cmdline_chart_compare(db, options, *args[1:])
+    else:
       usage()
-      exit(1)
-    cmd_show(*args)
 
   else:
-    usage()
+    usage('Unknown command argument: %s' % cmd)

Modified: subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/run
URL: http://svn.apache.org/viewvc/subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/run?rev=1360909&r1=1360908&r2=1360909&view=diff
==============================================================================
--- subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/run (original)
+++ subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/run Thu Jul 12 20:24:53 2012
@@ -17,31 +17,53 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# Where are the svn binaries you want to benchmark?
-SVN_A_NAME="1.7.x"
-SVN_A="$HOME/pat/bench/prefix/bin/svn"
+# debug? Just uncomment.
+#SVNBENCH_DEBUG=DEBUG_
+if [ -n "$SVNBENCH_DEBUG" ]; then
+  SVNBENCH_DEBUG="DEBUG_"
+fi
+
+# Subversion bin-dir used for maintenance of working copies
+SVN_STABLE="$HOME/pat/stable/prefix/bin/"
+
+# Where to find the svn binaries you want to benchmark, what are their labels
+# and Last Changed Revisions?
+# side A
+SVN_A_NAME="1.7.0"
+SVN_A="$HOME/pat/bench/prefix/bin"
+SVN_A_REV="$("$SVN_STABLE"/svnversion -c "$HOME/pat/bench/src" | sed 's/.*://')"
+
+# side B
 SVN_B_NAME="trunk"
-SVN_B="$HOME/pat/trunk/prefix/bin/svn"
+SVN_B="$HOME/pat/trunk/prefix/bin"
+SVN_B_REV="$("$SVN_STABLE"/svnversion -c "$HOME/pat/trunk/src" | sed 's/.*://')"
 
-benchmark="$PWD/benchmark.py"
+echo "$SVN_A_NAME@$SVN_A_REV vs. $SVN_B_NAME@$SVN_B_REV"
 
-parent="$(date +"%Y%m%d-%H%M%S")"
-inital_workdir="$PWD"
-mkdir "$parent"
-cd "$parent"
-pwd
+# benchmark script and parameters...
+benchmark="$PWD/benchmark.py"
 
+db="$PWD/${SVNBENCH_DEBUG}benchmark.db"
 
 batch(){
   levels="$1"
   spread="$2"
   N="$3"
-  pre="${levels}x${spread}_"
-  "$benchmark" "--svn=$SVN_A" run "${pre}$SVN_A_NAME" $levels $spread $N >/dev/null
-  "$benchmark" "--svn=$SVN_B" run "${pre}$SVN_B_NAME" $levels $spread $N >/dev/null
+
+  # SVN_A is a fixed tag, currently 1.7.0. For each call, run this once.
+  # It will be called again and again for each trunk build being tested,
+  # that's why we don't really need to run it $N times every time.
+  N_for_A=1
+  "$benchmark" "--db-path=$db" "--svn-bin-dir=$SVN_A" \
+      run "$SVN_A_NAME@$SVN_A_REV,${levels}x$spread" "$N_for_A" >/dev/null
+
+  # SVN_B is a branch, i.e. the moving target, benchmarked at a specific
+  # point in history each time this script is called. Run this $N times.
+  "$benchmark" "--db-path=$db" "--svn-bin-dir=$SVN_B" \
+      run "$SVN_B_NAME@$SVN_B_REV,${levels}x$spread" $N >/dev/null
 }
 
-N=6
+N=3
 al=5
 as=5
 bl=100
@@ -49,15 +71,16 @@ bs=1
 cl=1
 cs=100
 
-##DEBUG
-#N=1
-#al=1
-#as=1
-#bl=2
-#bs=1
-#cl=1
-#cs=2
-##DEBUG
+if [ -n "$SVNBENCH_DEBUG" ]; then
+  echo "DEBUG"
+  N=1
+  al=1
+  as=1
+  bl=2
+  bs=1
+  cl=1
+  cs=2
+fi
 
 
 {
@@ -65,22 +88,24 @@ started="$(date)"
 echo "Started at $started"
 
 echo "
-*Disclaimer:* this tests only file://-URL access on a GNU/Linux VM.
+*Disclaimer* - This tests only file://-URL access on a GNU/Linux VM.
 This is intended to measure changes in performance of the local working
 copy layer, *only*. These results are *not* generally true for everyone."
 
-batch $al $as $N
-batch $bl $bs $N
-batch $cl $cs $N
-
-"$benchmark" combine "total_$SVN_A_NAME" *x*"_$SVN_A_NAME" >/dev/null
-"$benchmark" combine "total_$SVN_B_NAME" *x*"_$SVN_B_NAME" >/dev/null
+if [ -z "$SVNBENCH_SUMMARY_ONLY" ]; then
+  batch $al $as $N
+  batch $bl $bs $N
+  batch $cl $cs $N
+else
+  echo "(not running benchmarks, just printing results on record.)"
+fi
 
 echo ""
 echo "Averaged-total results across all runs:"
 echo "---------------------------------------"
 echo ""
-"$benchmark" compare "total_$SVN_A_NAME" "total_$SVN_B_NAME"
+"$benchmark" "--db-path=$db" \
+    compare "$SVN_A_NAME" "$SVN_B_NAME@$SVN_B_REV"
 
 echo ""
 echo ""
@@ -88,8 +113,9 @@ echo "Above totals split into separate <
 echo "----------------------------------------------------------------"
 echo ""
 
-for pre in "${al}x${as}_" "${bl}x${bs}_" "${cl}x${cs}_"; do
-  "$benchmark" compare "${pre}$SVN_A_NAME" "${pre}$SVN_B_NAME"
+for lvlspr in "${al}x${as}" "${bl}x${bs}" "${cl}x${cs}"; do
+  "$benchmark" "--db-path=$db" \
+      compare "$SVN_A_NAME,$lvlspr" "$SVN_B_NAME@$SVN_B_REV,$lvlspr"
   echo ""
 done
 
@@ -99,8 +125,13 @@ echo "More detail:"
 echo "------------"
 echo ""
 
-for pre in "${al}x${as}_" "${bl}x${bs}_" "${cl}x${cs}_" "total_"; do
-  "$benchmark" compare -v "${pre}$SVN_A_NAME" "${pre}$SVN_B_NAME"
+for lvlspr in "${al}x${as}" "${bl}x${bs}" "${cl}x${cs}" "" ; do
+  "$benchmark" "--db-path=$db" show "$SVN_A_NAME,$lvlspr"
+  echo --
+  "$benchmark" "--db-path=$db" show "$SVN_B_NAME@$SVN_B_REV,$lvlspr"
+  echo --
+  "$benchmark" "--db-path=$db" \
+      compare -v "$SVN_A_NAME,$lvlspr" "$SVN_B_NAME@$SVN_B_REV,$lvlspr"
   echo ""
   echo ""
 done
@@ -111,7 +142,3 @@ echo "       done at $(date)"
 pwd
 } 2>&1 | tee results.txt
 
-cd "$inital_workdir"
-if [ -f "$parent/total_trunk" ]; then
-  rm -rf "$parent"
-fi

Modified: subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/run.bat
URL: http://svn.apache.org/viewvc/subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/run.bat?rev=1360909&r1=1360908&r2=1360909&view=diff
==============================================================================
--- subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/run.bat (original)
+++ subversion/branches/inheritable-props/tools/dev/benchmarks/suite1/run.bat Thu Jul 12 20:24:53 2012
@@ -16,6 +16,10 @@
 :: under the License.
 
 @ECHO OFF
+
+ECHO.THIS SCRIPT IS CURRENTLY OUTDATED.
+GOTO :EOF
+
 SETLOCAL EnableDelayedExpansion
 
 :: Where are the svn binaries you want to benchmark?