Report a revision's size on disk.

This adds an 'svnadmin rev-size' CLI and a libsvn_fs ioctl API to report
the total size in bytes of the representation on disk of a revision,
including rev-props, excluding FSFS indexes.

    $ svnadmin rev-size /path/to/repo -r1
            1337 bytes in revision 1


* subversion/svnadmin/svnadmin.c
  (cmd_table): Add and document the 'rev-size' command.
  (revision_size,
   subcommand_rev_size): New.

* subversion/include/private/svn_fs_fs_private.h
  (svn_fs_fs__ioctl_revision_size_input_t,
   svn_fs_fs__ioctl_revision_size_output_t,
   SVN_FS_FS__IOCTL_REVISION_SIZE): New.

* subversion/libsvn_fs_fs/fs.c
  (fs_ioctl): Handle SVN_FS_FS__IOCTL_REVISION_SIZE.

* subversion/libsvn_fs_fs/fs_fs.h
* subversion/libsvn_fs_fs/stats.c
  (svn_fs_fs__revision_size,
   rev_size_index_entry_cb,
   rev_size_baton_t): New.

* subversion/libsvn_fs_fs/revprops.h
* subversion/libsvn_fs_fs/revprops.c
  (svn_fs_fs__get_revision_props_size): New.

* tools/client-side/bash_completion
  (_svnadmin): Add 'rev-size'.


git-svn-id: https://svn.apache.org/repos/asf/subversion/trunk@1857624 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/subversion/include/private/svn_fs_fs_private.h b/subversion/include/private/svn_fs_fs_private.h
index dfc03ce..de57558 100644
--- a/subversion/include/private/svn_fs_fs_private.h
+++ b/subversion/include/private/svn_fs_fs_private.h
@@ -340,6 +340,19 @@
 
 SVN_FS_DECLARE_IOCTL_CODE(SVN_FS_FS__IOCTL_LOAD_INDEX, SVN_FS_TYPE_FSFS, 1002);
 
+typedef struct svn_fs_fs__ioctl_revision_size_input_t
+{
+  svn_revnum_t revision;
+} svn_fs_fs__ioctl_revision_size_input_t;
+
+typedef struct svn_fs_fs__ioctl_revision_size_output_t
+{
+  apr_off_t rev_size;
+} svn_fs_fs__ioctl_revision_size_output_t;
+
+/* See svn_fs_fs__revision_size(). */
+SVN_FS_DECLARE_IOCTL_CODE(SVN_FS_FS__IOCTL_REVISION_SIZE, SVN_FS_TYPE_FSFS, 1003);
+
 #ifdef __cplusplus
 }
 #endif /* __cplusplus */
diff --git a/subversion/libsvn_fs_fs/fs.c b/subversion/libsvn_fs_fs/fs.c
index 3c0133e..c1dd42a 100644
--- a/subversion/libsvn_fs_fs/fs.c
+++ b/subversion/libsvn_fs_fs/fs.c
@@ -297,6 +297,17 @@
                                         scratch_pool));
           *output_p = NULL;
         }
+      else if (ctlcode.code == SVN_FS_FS__IOCTL_REVISION_SIZE.code)
+        {
+          svn_fs_fs__ioctl_revision_size_input_t *input = input_void;
+          svn_fs_fs__ioctl_revision_size_output_t *output
+            = apr_pcalloc(result_pool, sizeof(*output));
+
+          SVN_ERR(svn_fs_fs__revision_size(&output->rev_size,
+                                           fs, input->revision,
+                                           scratch_pool));
+          *output_p = output;
+        }
       else
         return svn_error_create(SVN_ERR_FS_UNRECOGNIZED_IOCTL_CODE, NULL, NULL);
     }
diff --git a/subversion/libsvn_fs_fs/fs_fs.h b/subversion/libsvn_fs_fs/fs_fs.h
index 0e981a3..f3e356b 100644
--- a/subversion/libsvn_fs_fs/fs_fs.h
+++ b/subversion/libsvn_fs_fs/fs_fs.h
@@ -347,4 +347,13 @@
                       apr_array_header_t *entries,
                       apr_pool_t *scratch_pool);
 
+/* Set *REV_SIZE to the total size of objects belonging to revision REVISION
+ * in FS. The size includes revision properties and excludes indexes.
+ */
+svn_error_t *
+svn_fs_fs__revision_size(apr_off_t *rev_size,
+                         svn_fs_t *fs,
+                         svn_revnum_t revision,
+                         apr_pool_t *scratch_pool);
+
 #endif
diff --git a/subversion/libsvn_fs_fs/revprops.c b/subversion/libsvn_fs_fs/revprops.c
index 6d41fd8..da11727 100644
--- a/subversion/libsvn_fs_fs/revprops.c
+++ b/subversion/libsvn_fs_fs/revprops.c
@@ -672,6 +672,64 @@
   return SVN_NO_ERROR;
 }
 
+svn_error_t *
+svn_fs_fs__get_revision_props_size(apr_off_t *props_size_p,
+                                   svn_fs_t *fs,
+                                   svn_revnum_t rev,
+                                   apr_pool_t *scratch_pool)
+{
+  fs_fs_data_t *ffd = fs->fsap_data;
+
+  /* should they be available at all? */
+  SVN_ERR(svn_fs_fs__ensure_revision_exists(rev, fs, scratch_pool));
+
+  /* if REV had not been packed when we began, try reading it from the
+   * non-packed shard.  If that fails, we will fall through to packed
+   * shard reads. */
+  if (!svn_fs_fs__is_packed_revprop(fs, rev))
+    {
+      const char *path = svn_fs_fs__path_revprops(fs, rev, scratch_pool);
+      svn_error_t *err;
+      apr_file_t *file;
+      svn_filesize_t file_size;
+
+      err = svn_io_file_open(&file, path, APR_FOPEN_READ, APR_OS_DEFAULT,
+                             scratch_pool);
+      if (!err)
+        err = svn_io_file_size_get(&file_size, file, scratch_pool);
+      if (!err)
+        {
+          *props_size_p = (apr_off_t)file_size;
+          return SVN_NO_ERROR;
+        }
+      else if (!APR_STATUS_IS_ENOENT(err->apr_err)
+               || ffd->format < SVN_FS_FS__MIN_PACKED_REVPROP_FORMAT)
+        {
+          return svn_error_trace(err);
+        }
+
+      /* fall through: maybe the revision got packed while we were looking */
+      svn_error_clear(err);
+    }
+
+  /* Try reading packed revprops.  If that fails, REV is most
+   * likely invalid (or its revprops highly contested). */
+  {
+    packed_revprops_t *revprops;
+
+    /* ### This is inefficient -- reading all the revprops in a pack. We
+       should just read the index. */
+    SVN_ERR(read_pack_revprop(&revprops, fs, rev,
+                              TRUE /*read_all*/, FALSE /*populate_cache*/,
+                              scratch_pool));
+    *props_size_p = (apr_off_t)APR_ARRAY_IDX(revprops->sizes,
+                                             rev - revprops->start_revision,
+                                             apr_size_t);
+  }
+
+  return SVN_NO_ERROR;
+}
+
 /* Read the revprops for revision REV in FS and return them in *PROPERTIES_P.
  *
  * Allocations will be done in POOL.
diff --git a/subversion/libsvn_fs_fs/revprops.h b/subversion/libsvn_fs_fs/revprops.h
index 37063f9..32df183 100644
--- a/subversion/libsvn_fs_fs/revprops.h
+++ b/subversion/libsvn_fs_fs/revprops.h
@@ -62,6 +62,15 @@
 void
 svn_fs_fs__reset_revprop_cache(svn_fs_t *fs);
 
+/* Set *PROPS_SIZE_P to the size in bytes on disk of the revprops for
+ * revision REV in FS. The size excludes indexes.
+ */
+svn_error_t *
+svn_fs_fs__get_revision_props_size(apr_off_t *props_size_p,
+                                   svn_fs_t *fs,
+                                   svn_revnum_t rev,
+                                   apr_pool_t *scratch_pool);
+
 /* Read the revprops for revision REV in FS and return them in *PROPERTIES_P.
  * If REFRESH is set, clear the revprop cache before accessing the data.
  *
diff --git a/subversion/libsvn_fs_fs/stats.c b/subversion/libsvn_fs_fs/stats.c
index 9377b33..a26a79d 100644
--- a/subversion/libsvn_fs_fs/stats.c
+++ b/subversion/libsvn_fs_fs/stats.c
@@ -36,6 +36,7 @@
 #include "fs_fs.h"
 #include "cached_data.h"
 #include "low_level.h"
+#include "revprops.h"
 
 #include "../libsvn_fs/fs-loader.h"
 
@@ -1396,3 +1397,96 @@
 
   return SVN_NO_ERROR;
 }
+
+/* Baton for rev_size_index_entry_cb. */
+struct rev_size_baton_t {
+  svn_revnum_t revision;
+  apr_off_t rev_size;
+};
+
+/* Implements svn_fs_fs__dump_index_func_t, summing object sizes for
+ * revision BATON->revision into BATON->rev_size.
+ */
+static svn_error_t *
+rev_size_index_entry_cb(const svn_fs_fs__p2l_entry_t *entry,
+                        void *baton,
+                        apr_pool_t *scratch_pool)
+{
+  struct rev_size_baton_t *b = baton;
+
+  if (entry->item.revision == b->revision)
+    b->rev_size += entry->size;
+  return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__revision_size(apr_off_t *rev_size,
+                         svn_fs_t *fs,
+                         svn_revnum_t revision,
+                         apr_pool_t *scratch_pool)
+{
+  /* Get the size of the revision (excluding rev-props) */
+  if (svn_fs_fs__use_log_addressing(fs))
+    {
+      /* This works for a packed or a non-packed revision.
+         We could provide an optimized case for a non-packed revision
+         using svn_fs_fs__p2l_get_max_offset(). */
+      struct rev_size_baton_t b = { 0, 0 };
+
+      b.revision = revision;
+      SVN_ERR(svn_fs_fs__dump_index(fs, revision,
+                                    rev_size_index_entry_cb, &b,
+                                    NULL, NULL, scratch_pool));
+      *rev_size = b.rev_size;
+    }
+  else
+    {
+      svn_fs_fs__revision_file_t *rev_file;
+      svn_revnum_t min_unpacked_rev;
+
+      SVN_ERR(svn_fs_fs__open_pack_or_rev_file(&rev_file, fs, revision,
+                                               scratch_pool, scratch_pool));
+      SVN_ERR(svn_fs_fs__min_unpacked_rev(&min_unpacked_rev, fs,
+                                          scratch_pool));
+      if (revision < min_unpacked_rev)
+        {
+          int shard_size = svn_fs_fs__shard_size(fs);
+          apr_off_t start_offset, end_offset;
+
+          SVN_ERR(svn_fs_fs__get_packed_offset(&start_offset, fs, revision,
+                                               scratch_pool));
+          if (((revision + 1) % shard_size) == 0)
+            {
+              svn_filesize_t file_size;
+
+              SVN_ERR(svn_io_file_size_get(&file_size, rev_file->file, scratch_pool));
+              end_offset = (apr_off_t)file_size;
+            }
+          else
+            {
+              SVN_ERR(svn_fs_fs__get_packed_offset(&end_offset, fs,
+                                                   revision + 1, scratch_pool));
+            }
+          *rev_size = (end_offset - start_offset);
+        }
+      else
+        {
+          svn_filesize_t file_size;
+
+          SVN_ERR(svn_io_file_size_get(&file_size, rev_file->file, scratch_pool));
+          *rev_size = (apr_off_t)file_size;
+        }
+
+      SVN_ERR(svn_fs_fs__close_revision_file(rev_file));
+    }
+
+  /* Add the size of the rev-props */
+  {
+    apr_off_t size;
+
+    SVN_ERR(svn_fs_fs__get_revision_props_size(&size, fs, revision, scratch_pool));
+    *rev_size += size;
+  }
+
+  return SVN_NO_ERROR;
+}
diff --git a/subversion/svnadmin/svnadmin.c b/subversion/svnadmin/svnadmin.c
index 0301046..85a6279 100644
--- a/subversion/svnadmin/svnadmin.c
+++ b/subversion/svnadmin/svnadmin.c
@@ -42,6 +42,7 @@
 #include "svn_time.h"
 #include "svn_user.h"
 #include "svn_xml.h"
+#include "svn_fs.h"
 
 #include "private/svn_cmdline_private.h"
 #include "private/svn_opt_private.h"
@@ -49,6 +50,7 @@
 #include "private/svn_subr_private.h"
 #include "private/svn_cmdline_private.h"
 #include "private/svn_fspath.h"
+#include "private/svn_fs_fs_private.h"
 
 #include "svn_private_config.h"
 
@@ -114,6 +116,7 @@
   subcommand_lstxns,
   subcommand_pack,
   subcommand_recover,
+  subcommand_rev_size,
   subcommand_rmlocks,
   subcommand_rmtxns,
   subcommand_setlog,
@@ -518,6 +521,17 @@
    )},
    {svnadmin__wait} },
 
+  {"rev-size", subcommand_rev_size, {0}, {N_(
+    "usage: svnadmin rev-size REPOS_PATH -r REVISION\n"
+    "\n"), N_(
+    "Print the total size in bytes of the representation on disk of\n"
+    "revision REVISION.\n"
+    "\n"), N_(
+    "The size includes revision properties and excludes FSFS indexes.\n"
+   )},
+   {'r', 'q', 'M'},
+   { {'q', "print only the size and a newline"} } },
+
   {"rmlocks", subcommand_rmlocks, {0}, {N_(
     "usage: svnadmin rmlocks REPOS_PATH LOCKED_PATH...\n"
     "\n"), N_(
@@ -2847,6 +2861,68 @@
 }
 
 
+/* Set *REV_SIZE to the total size in bytes of the representation on disk
+ * of revision REVISION in FS.
+ *
+ * This is implemented only for FSFS repositories, and otherwise returns
+ * an SVN_ERR_UNSUPPORTED_FEATURE error.
+ *
+ * The size includes revision properties and excludes FSFS indexes.
+ */
+static svn_error_t *
+revision_size(apr_off_t *rev_size,
+              svn_fs_t *fs,
+              svn_revnum_t revision,
+              apr_pool_t *scratch_pool)
+{
+  svn_error_t *err;
+  svn_fs_fs__ioctl_revision_size_input_t input = {0};
+  svn_fs_fs__ioctl_revision_size_output_t *output;
+
+  input.revision = revision;
+  err = svn_fs_ioctl(fs, SVN_FS_FS__IOCTL_REVISION_SIZE,
+                     &input, (void **)&output,
+                     check_cancel, NULL, scratch_pool, scratch_pool);
+  if (err && err->apr_err == SVN_ERR_FS_UNRECOGNIZED_IOCTL_CODE)
+    {
+      return svn_error_quick_wrapf(err,
+                                   _("Revision size query is not implemented "
+                                     "for the filesytem type found in '%s'"),
+                                   svn_fs_path(fs, scratch_pool));
+    }
+  SVN_ERR(err);
+
+  *rev_size = output->rev_size;
+  return SVN_NO_ERROR;
+}
+
+/* This implements `svn_opt_subcommand_t'. */
+svn_error_t *
+subcommand_rev_size(apr_getopt_t *os, void *baton, apr_pool_t *pool)
+{
+  struct svnadmin_opt_state *opt_state = baton;
+  svn_revnum_t revision;
+  apr_off_t rev_size;
+  svn_repos_t *repos;
+
+  if (opt_state->start_revision.kind != svn_opt_revision_number
+      || opt_state->end_revision.kind != svn_opt_revision_unspecified)
+    return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
+                            _("Invalid revision specifier"));
+  revision = opt_state->start_revision.value.number;
+
+  SVN_ERR(open_repos(&repos, opt_state->repository_path, opt_state, pool));
+  SVN_ERR(revision_size(&rev_size, svn_repos_fs(repos), revision, pool));
+
+  if (opt_state->quiet)
+    SVN_ERR(svn_cmdline_printf(pool, "%"APR_OFF_T_FMT"\n", rev_size));
+  else
+    SVN_ERR(svn_cmdline_printf(pool, _("%12"APR_OFF_T_FMT" bytes in revision %ld\n"),
+                               rev_size, revision));
+
+  return SVN_NO_ERROR;
+}
+
 
 /** Main. **/
 
diff --git a/tools/client-side/bash_completion b/tools/client-side/bash_completion
index d2095ca..44d15bb 100644
--- a/tools/client-side/bash_completion
+++ b/tools/client-side/bash_completion
@@ -1139,7 +1139,7 @@
 	# Possible expansions, without pure-prefix abbreviations such as "h".
 	cmds='crashtest create delrevprop deltify dump dump-revprops freeze \
 	      help hotcopy info list-dblogs list-unused-dblogs \
-	      load load-revprops lock lslocks lstxns pack recover rmlocks \
+	      load load-revprops lock lslocks lstxns pack recover rev-size rmlocks \
 	      rmtxns setlog setrevprop setuuid unlock upgrade verify --version'
 
 	if [[ $COMP_CWORD -eq 1 ]] ; then
@@ -1211,6 +1211,9 @@
 	recover)
 		cmdOpts="--wait"
 		;;
+	rev-size)
+		cmdOpts="-r --revision -M --memory-cache-size -q --quiet"
+		;;
 	rmlocks)
 		cmdOpts="-q --quiet"
 		;;