fsfs-access-map.c   [plain text]


/* fsfs-access-map.c -- convert strace output into FSFS access bitmap
 *
 * ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); you may not use this file except in compliance
 *    with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing,
 *    software distributed under the License is distributed on an
 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *    KIND, either express or implied.  See the License for the
 *    specific language governing permissions and limitations
 *    under the License.
 * ====================================================================
 */

#include "svn_pools.h"
#include "svn_string.h"
#include "svn_io.h"

#include "private/svn_string_private.h"

/* The information we gather for each file.  There will be one instance
 * per file name - even if the file got deleted and re-created.
 */
typedef struct file_stats_t
{
  /* file name as found in the open() call */
  const char *name;

  /* file size as determined during the tool run.  Will be 0 for
   * files that no longer exist.  However, there may still be entries
   * in the read_map. */
  apr_int64_t size;

  /* for rev files (packed or non-packed), this will be the first revision
   * that file. -1 for non-rev files. */
  apr_int64_t rev_num;

  /* number of times this file got opened */
  apr_int64_t open_count;

  /* number of lseek counts */
  apr_int64_t seek_count;

  /* number of lseek calls to clusters not previously read */
  apr_int64_t uncached_seek_count;

  /* number of lseek counts not followed by a read */
  apr_int64_t unnecessary_seeks;

  /* number of read() calls */
  apr_int64_t read_count;

  /* number of read() calls that returned 0 bytes */
  apr_int64_t empty_reads;

  /* total number of bytes returned by those reads */
  apr_int64_t read_size;

  /* number of clusters read */
  apr_int64_t clusters_read;

  /* number of different clusters read
   * (i.e. number of non-zero entries in read_map). */
  apr_int64_t unique_clusters_read;

  /* cluster -> read count mapping (1 word per cluster, saturated at 64k) */
  apr_array_header_t *read_map;

} file_stats_t;

/* Represents an open file handle.  It refers to a file and concatenates
 * consecutive reads such that we don't artificially hit the same cluster
 * multiple times.  Instances of this type will be reused to limit the
 * allocation load on the lookup map.
 */
typedef struct handle_info_t
{
  /* the open file */
  file_stats_t *file;

  /* file offset at which the current series of reads started (default: 0) */
  apr_int64_t last_read_start;

  /* bytes read so far in the current series of reads started (default: 0) */
  apr_int64_t last_read_size;

  /* number of read() calls in this series */
  apr_int64_t read_count;
} handle_info_t;

/* useful typedef */
typedef unsigned char byte;
typedef unsigned short word;

/* an RGB color */
typedef byte color_t[3];

/* global const char * file name -> *file_info_t map */
static apr_hash_t *files = NULL;

/* global int handle -> *handle_info_t map.  Entries don't get removed
 * by close().  Instead, we simply recycle (and re-initilize) existing
 * instances. */
static apr_hash_t *handles = NULL;

/* assume cluster size.  64 and 128kB are typical values for RAIDs. */
static apr_int64_t cluster_size = 64 * 1024;

/* Call this after a sequence of reads has been ended by either close()
 * or lseek() for this HANDLE_INFO.  This will update the read_map and
 * unique_clusters_read members of the underlying file_info_t structure.
 */
static void
store_read_info(handle_info_t *handle_info)
{
  if (handle_info->last_read_size)
    {
      apr_size_t i;
      apr_size_t first_cluster
         = (apr_size_t)(handle_info->last_read_start / cluster_size);
      apr_size_t last_cluster
         = (apr_size_t)((  handle_info->last_read_start
                         + handle_info->last_read_size
                         - 1) / cluster_size);

      /* auto-expand access map in case the file later shrunk or got deleted */
      while (handle_info->file->read_map->nelts <= last_cluster)
        APR_ARRAY_PUSH(handle_info->file->read_map, word) = 0;

      /* accumulate the accesses per cluster. Saturate and count first
       * (i.e. disjoint) accesses clusters */
      handle_info->file->clusters_read += last_cluster - first_cluster + 1;
      for (i = first_cluster; i <= last_cluster; ++i)
        {
          word *count = &APR_ARRAY_IDX(handle_info->file->read_map, i, word);
          if (*count == 0)
            handle_info->file->unique_clusters_read++;
          if (*count < 0xffff)
            ++*count;
        }
    }
  else if (handle_info->read_count == 0)
    {
      /* two consecutive seeks */
      handle_info->file->unnecessary_seeks++;
    }
}

/* Handle a open() call.  Ensures that a file_info_t for the given NAME
 * exists.  Auto-create and initialize a handle_info_t for it linked to
 * HANDLE.
 */
static void
open_file(const char *name, int handle)
{
  file_stats_t *file = apr_hash_get(files, name, APR_HASH_KEY_STRING);
  handle_info_t *handle_info = apr_hash_get(handles, &handle, sizeof(handle));

  /* auto-create file info */
  if (!file)
    {
      apr_pool_t *pool = apr_hash_pool_get(files);
      apr_pool_t *subpool = svn_pool_create(pool);

      apr_file_t *apr_file = NULL;
      apr_finfo_t finfo = { 0 };
      int cluster_count = 0;

      /* determine file size (if file still exists) */
      apr_file_open(&apr_file, name,
                    APR_READ | APR_BUFFERED, APR_OS_DEFAULT, subpool);
      if (apr_file)
        apr_file_info_get(&finfo, APR_FINFO_SIZE, apr_file);
      svn_pool_destroy(subpool);

      file = apr_pcalloc(pool, sizeof(*file));
      file->name = apr_pstrdup(pool, name);
      file->size = finfo.size;

      /* pre-allocate cluster map accordingly
       * (will be auto-expanded later if necessary) */
      cluster_count = (int)(1 + (file->size - 1) / cluster_size);
      file->read_map = apr_array_make(pool, file->size
                                          ? cluster_count
                                          : 1, sizeof(word));

      while (file->read_map->nelts < cluster_count)
        APR_ARRAY_PUSH(file->read_map, byte) = 0;

      /* determine first revision of rev / packed rev files */
      if (strstr(name, "/db/revs/") != NULL && strstr(name, "manifest") == NULL)
        if (strstr(name, ".pack/pack") != NULL)
          file->rev_num = SVN_STR_TO_REV(strstr(name, "/db/revs/") + 9);
        else
          file->rev_num = SVN_STR_TO_REV(strrchr(name, '/') + 1);
      else
        file->rev_num = -1;

      /* filter out log/phys index files */
      if (file->rev_num >= 0)
        {
          const char *suffix = name + strlen(name) - 4;
          if (strcmp(suffix, ".l2p") == 0 || strcmp(suffix, ".p2l") == 0)
            file->rev_num = -1;
        }

      apr_hash_set(files, file->name, APR_HASH_KEY_STRING, file);
    }

  file->open_count++;

  /* auto-create handle instance */
  if (!handle_info)
    {
      apr_pool_t *pool = apr_hash_pool_get(handles);
      int *key = apr_palloc(pool, sizeof(*key));
      *key = handle;

      handle_info = apr_pcalloc(pool, sizeof(*handle_info));
      apr_hash_set(handles, key, sizeof(*key), handle_info);
    }

  /* link handle to file */
  handle_info->file = file;
  handle_info->last_read_start = 0;
  handle_info->last_read_size = 0;
}

/* COUNT bytes have been read from file with the given HANDLE.
 */
static void
read_file(int handle, apr_int64_t count)
{
  handle_info_t *handle_info = apr_hash_get(handles, &handle, sizeof(handle));
  if (handle_info)
    {
      /* known file handle -> expand current read sequence */

      handle_info->read_count++;
      handle_info->last_read_size += count;
      handle_info->file->read_count++;
      handle_info->file->read_size += count;

      if (count == 0)
        handle_info->file->empty_reads++;
    }
}

/* Seek to offset LOCATION in file given by HANDLE.
 */
static void
seek_file(int handle, apr_int64_t location)
{
  handle_info_t *handle_info = apr_hash_get(handles, &handle, sizeof(handle));
  if (handle_info)
    {
      /* known file handle -> end current read sequence and start a new one */

      apr_size_t cluster = (apr_size_t)(location / cluster_size);

      store_read_info(handle_info);

      handle_info->last_read_size = 0;
      handle_info->last_read_start = location;
      handle_info->read_count = 0;
      handle_info->file->seek_count++;

      /* if we seek to a location that had not been read from before,
       * there will probably be a real I/O seek on the following read.
       */
      if (   handle_info->file->read_map->nelts <= cluster
          || APR_ARRAY_IDX(handle_info->file->read_map, cluster, word) == 0)
        handle_info->file->uncached_seek_count++;
    }
}

/* The given file HANDLE has been closed.
 */
static void
close_file(int handle)
{
  /* for known file handles, end current read sequence */

  handle_info_t *handle_info = apr_hash_get(handles, &handle, sizeof(handle));
  if (handle_info)
    store_read_info(handle_info);
}

/* Parse / process non-empty the LINE from an strace output.
 */
static void
parse_line(svn_stringbuf_t *line)
{
  /* determine function name, first parameter and return value */
  char *func_end = strchr(line->data, '(');
  char *return_value = strrchr(line->data, ' ');
  char *first_param_end;
  apr_int64_t func_return = 0;
  char *func_start = strchr(line->data, ' ');

  if (func_end == NULL || return_value == NULL)
    return;

  if (func_start == NULL || func_start > func_end)
    func_start = line->data;
  else
    while(*func_start == ' ')
      func_start++;

  first_param_end = strchr(func_end, ',');
  if (first_param_end == NULL)
    first_param_end = strchr(func_end, ')');

  if (first_param_end == NULL)
    return;

  *func_end++ = 0;
  *first_param_end = 0;
  ++return_value;

  /* (try to) convert the return value into an integer.
   * If that fails, continue anyway as defaulting to 0 will be safe for us. */
  svn_error_clear(svn_cstring_atoi64(&func_return, return_value));

  /* process those operations that we care about */
  if (strcmp(func_start, "open") == 0)
    {
      /* remove double quotes from file name parameter */
      *func_end++ = 0;
      *--first_param_end = 0;

      open_file(func_end, (int)func_return);
    }
  else if (strcmp(func_start, "read") == 0)
    read_file(atoi(func_end), func_return);
  else if (strcmp(func_start, "lseek") == 0)
    seek_file(atoi(func_end), func_return);
  else if (strcmp(func_start, "close") == 0)
    close_file(atoi(func_end));
}

/* Process the strace output stored in FILE.
 */
static void
parse_file(apr_file_t *file)
{
  apr_pool_t *pool = svn_pool_create(NULL);
  apr_pool_t *iterpool = svn_pool_create(pool);

  /* limit lines to 4k (usually, we need less than 200 bytes) */
  svn_stringbuf_t *line = svn_stringbuf_create_ensure(4096, pool);

  do
    {
      svn_error_t *err = NULL;

      line->len = line->blocksize-1;
      err = svn_io_read_length_line(file, line->data, &line->len, iterpool);
      svn_error_clear(err);
      if (err)
        break;

      parse_line(line);
      svn_pool_clear(iterpool);
    }
  while (line->len > 0);
}

/* qsort() callback.  Sort files by revision number.
 */
static int
compare_files(file_stats_t **lhs, file_stats_t **rhs)
{
  return (*lhs)->rev_num < (*rhs)->rev_num;
}

/* Return all rev (and packed rev) files sorted by revision number.
 * Allocate the result in POOL.
 */
static apr_array_header_t *
get_rev_files(apr_pool_t *pool)
{
  apr_hash_index_t *hi;
  apr_array_header_t *result = apr_array_make(pool,
                                              apr_hash_count(files),
                                              sizeof(file_stats_t *));

  /* select all files that have a rev number */
  for (hi = apr_hash_first(pool, files); hi; hi = apr_hash_next(hi))
    {
      const char *name = NULL;
      apr_ssize_t len = 0;
      file_stats_t *file = NULL;

      apr_hash_this(hi, (const void **)&name, &len, (void**)&file);
      if (file->rev_num >= 0)
        APR_ARRAY_PUSH(result, file_stats_t *) = file;
    }

  /* sort them */
  qsort(result->elts, result->nelts, result->elt_size,
        (int (*)(const void *, const void *))compare_files);

  /* return the result */
  return result;
}

/* store VALUE to DEST in little-endian format.  Assume that the target
 * buffer is filled with 0.
 */
static void
write_number(byte *dest, int value)
{
  while (value)
    {
      *dest = (byte)(value % 256);
      value /= 256;
      ++dest;
    }
}

/* Return a linearly interpolated y value for X with X0 <= X <= X1 and
 * the corresponding Y0 and Y1 values.
 */
static int
interpolate(int y0, int x0, int y1, int x1, int x)
{
  return y0 + ((y1 - y0) * (x - x0)) / (x1 - x0);
}

/* Return the BMP-encoded 24 bit COLOR for the given value.
 */
static void
select_color(byte color[3], word value)
{
  enum { COLOR_COUNT = 10 };

  /* value -> color table. Missing values get interpolated.
   * { count, B - G - R } */
  word table[COLOR_COUNT][4] =
    {
      {     0, 255, 255, 255 },   /* unread -> white */
      {     1,  64, 128,   0 },   /* read once -> turquoise  */
      {     2,   0, 128,   0 },   /* twice  -> green */
      {     8,   0, 192, 192 },   /*    8x  -> yellow */
      {    64,   0,   0, 192 },   /*   64x  -> red */
      {   256,  64,  32, 230 },   /*  256x  -> bright red */
      {   512, 192,   0, 128 },   /*  512x  -> purple */
      {  1024,  96,  32,  96 },   /* 1024x  -> UV purple */
      {  4096,  32,  16,  32 },   /* 4096x  -> EUV purple */
      { 65535,   0,   0,   0 }    /*   max  -> black */
    };

  /* find upper limit entry for value */
  int i;
  for (i = 0; i < COLOR_COUNT; ++i)
    if (table[i][0] >= value)
      break;

  /* exact match? */
  if (table[i][0] == value)
    {
      color[0] = (byte)table[i][1];
      color[1] = (byte)table[i][2];
      color[2] = (byte)table[i][3];
    }
  else
    {
      /* interpolate */
      color[0] = (byte)interpolate(table[i-1][1], table[i-1][0],
                                   table[i][1], table[i][0],
                                   value);
      color[1] = (byte)interpolate(table[i-1][2], table[i-1][0],
                                   table[i][2], table[i][0],
                                   value);
      color[2] = (byte)interpolate(table[i-1][3], table[i-1][0],
                                   table[i][3], table[i][0],
                                   value);
    }
}

/* Writes a BMP image header to FILE for a 24-bit color picture of the
 * given XSIZE and YSIZE dimension.
 */
static void
write_bitmap_header(apr_file_t *file, int xsize, int ysize)
{
  /* BMP file header (some values need to filled in later)*/
  byte header[54] =
    {
      'B', 'M',        /* magic */
      0, 0, 0, 0,      /* file size (to be written later) */
      0, 0, 0, 0,      /* reserved, unused */
      54, 0, 0, 0,     /* pixel map starts at offset 54dec */

      40, 0, 0, 0,     /* DIB header has 40 bytes */
      0, 0, 0, 0,      /* x size in pixel */
      0, 0, 0, 0,      /* y size in pixel */
      1, 0,            /* 1 color plane */
      24, 0,           /* 24 bits / pixel */
      0, 0, 0, 0,      /* no pixel compression used */
      0, 0, 0, 0,      /* size of pixel array (to be written later) */
      0xe8, 3, 0, 0,   /* 1 pixel / mm */
      0xe8, 3, 0, 0,   /* 1 pixel / mm */
      0, 0, 0, 0,      /* no colors in palette */
      0, 0, 0, 0       /* no colors to import */
    };

  apr_size_t written;

  /* rows in BMP files must be aligned to 4 bytes */
  int row_size = APR_ALIGN(xsize * 3, 4);

  /* write numbers to header */
  write_number(header + 2, ysize * row_size + 54);
  write_number(header + 18, xsize);
  write_number(header + 22, ysize);
  write_number(header + 38, ysize * row_size);

  /* write header to file */
  written = sizeof(header);
  apr_file_write(file, header, &written);
}

/* To COLOR, add the fractional value of SOURCE from fractional indexes
 * SOURCE_START to SOURCE_END and apply the SCALING_FACTOR.
 */
static void
add_sample(color_t color,
           color_t *source,
           double source_start,
           double source_end,
           double scaling_factor)
{
  double factor = (source_end - source_start) / scaling_factor;

  apr_size_t i;
  for (i = 0; i < sizeof(color_t) / sizeof(*color); ++i)
    color[i] += (source_end - source_start < 0.5) && source_start > 1.0
              ? factor * source[(apr_size_t)source_start - 1][i]
              : factor * source[(apr_size_t)source_start][i];
}

/* Scale the IN_LEN RGB values from IN to OUT_LEN RGB values in OUT.
 */
static void
scale_line(color_t* out,
           int out_len,
           color_t *in,
           int in_len)
{
  double scaling_factor = (double)(in_len) / (double)(out_len);

  apr_size_t i;
  memset(out, 0, out_len * sizeof(color_t));
  for (i = 0; i < out_len; ++i)
    {
      color_t color = { 0 };

      double source_start = i * scaling_factor;
      double source_end = (i + 1) * scaling_factor;

      if ((apr_size_t)source_start == (apr_size_t)source_end)
        {
          add_sample(color, in, source_start, source_end, scaling_factor);
        }
      else
        {
          apr_size_t k;
          apr_size_t first_sample_end = (apr_size_t)source_start + 1;
          apr_size_t last_sample_start = (apr_size_t)source_end;

          add_sample(color, in, source_start, first_sample_end, scaling_factor);
          for (k = first_sample_end; k < last_sample_start; ++k)
            add_sample(color, in, k, k + 1, scaling_factor);

          add_sample(color, in, last_sample_start, source_end, scaling_factor);
        }

      memcpy(out[i], color, sizeof(color));
    }
}

/* Write the cluster read map for all files in INFO as BMP image to FILE.
 * If MAX_X is not 0, scale all lines to MAX_X pixels.  Use POOL for
 * allocations.
 */
static void
write_bitmap(apr_array_header_t *info,
             int max_x,
             apr_file_t *file,
             apr_pool_t *pool)
{
  int ysize = info->nelts;
  int xsize = 0;
  int x, y;
  apr_size_t row_size;
  apr_size_t written;
  color_t *line, *scaled_line;
  svn_boolean_t do_scale = max_x > 0;

  /* xsize = max cluster number */
  for (y = 0; y < ysize; ++y)
    if (xsize < APR_ARRAY_IDX(info, y, file_stats_t *)->read_map->nelts)
      xsize = APR_ARRAY_IDX(info, y, file_stats_t *)->read_map->nelts;

  /* limit picture dimensions (16k pixels in each direction) */
  if (xsize >= 0x4000)
    xsize = 0x3fff;
  if (ysize >= 0x4000)
    ysize = 0x3fff;
  if (max_x == 0)
    max_x = xsize;

  /* rows in BMP files must be aligned to 4 bytes */
  row_size = APR_ALIGN(max_x * sizeof(color_t), 4);

  /**/
  line = apr_pcalloc(pool, xsize * sizeof(color_t));
  scaled_line = apr_pcalloc(pool, row_size);

  /* write header to file */
  write_bitmap_header(file, max_x, ysize);

  /* write all rows */
  for (y = 0; y < ysize; ++y)
    {
      file_stats_t *file_info = APR_ARRAY_IDX(info, y, file_stats_t *);
      int block_count = file_info->read_map->nelts;
      for (x = 0; x < xsize; ++x)
        {
          color_t color = { 128, 128, 128 };
          if (x < block_count)
            {
              word count = APR_ARRAY_IDX(file_info->read_map, x, word);
              select_color(color, count);
            }

          memcpy(line[x], color, sizeof(color));
        }

      scale_line(scaled_line, max_x, line, block_count ? block_count : 1);

      written = row_size;
      apr_file_write(file, do_scale ? scaled_line : line, &written);
    }
}

/* write a color bar with (roughly) logarithmic scale as BMP image to FILE.
 */
static void
write_scale(apr_file_t *file)
{
  int x;
  word value = 0, inc = 1;

  /* write header to file */
  write_bitmap_header(file, 64, 1);

  for (x = 0; x < 64; ++x)
    {
      apr_size_t written;
      byte color[3] = { 128, 128, 128 };

      select_color(color, value);
      if (value + (int)inc < 0x10000)
        {
          value += inc;
          if (value >= 8 * inc)
            inc *= 2;
        }

      written = sizeof(color);
      apr_file_write(file, color, &written);
    }
}

/* Write a summary of the I/O ops to stdout.
 * Use POOL for temporaries.
 */
static void
print_stats(apr_pool_t *pool)
{
  apr_int64_t open_count = 0;
  apr_int64_t seek_count = 0;
  apr_int64_t read_count = 0;
  apr_int64_t read_size = 0;
  apr_int64_t clusters_read = 0;
  apr_int64_t unique_clusters_read = 0;
  apr_int64_t uncached_seek_count = 0;
  apr_int64_t unnecessary_seek_count = 0;
  apr_int64_t empty_read_count = 0;

  apr_hash_index_t *hi;
  for (hi = apr_hash_first(pool, files); hi; hi = apr_hash_next(hi))
    {
      const char *name = NULL;
      apr_ssize_t len = 0;
      file_stats_t *file = NULL;

      apr_hash_this(hi, (const void **)&name, &len, (void**)&file);

      open_count += file->open_count;
      seek_count += file->seek_count;
      read_count += file->read_count;
      read_size += file->read_size;
      clusters_read += file->clusters_read;
      unique_clusters_read += file->unique_clusters_read;
      uncached_seek_count += file->uncached_seek_count;
      unnecessary_seek_count += file->unnecessary_seeks;
      empty_read_count += file->empty_reads;
    }

  printf("%20s files\n", svn__i64toa_sep(apr_hash_count(files), ',', pool));
  printf("%20s files opened\n", svn__i64toa_sep(open_count, ',', pool));
  printf("%20s seeks\n", svn__i64toa_sep(seek_count, ',', pool));
  printf("%20s unnecessary seeks\n", svn__i64toa_sep(unnecessary_seek_count, ',', pool));
  printf("%20s uncached seeks\n", svn__i64toa_sep(uncached_seek_count, ',', pool));
  printf("%20s reads\n", svn__i64toa_sep(read_count, ',', pool));
  printf("%20s empty reads\n", svn__i64toa_sep(empty_read_count, ',', pool));
  printf("%20s unique clusters read\n", svn__i64toa_sep(unique_clusters_read, ',', pool));
  printf("%20s clusters read\n", svn__i64toa_sep(clusters_read, ',', pool));
  printf("%20s bytes read\n", svn__i64toa_sep(read_size, ',', pool));
}

/* Some help output. */
static void
print_usage(void)
{
  printf("fsfs-access-map <file>\n\n");
  printf("Reads strace of some FSFS-based tool from <file>, prints some stats\n");
  printf("and writes a cluster access map to 'access.bmp' the current folder.\n");
  printf("Each pixel corresponds to one 64kB cluster and every line to a rev\n");
  printf("or packed rev file in the repository.  Turquoise and green indicate\n");
  printf("1 and 2 hits, yellow to read-ish colors for up to 20, shares of\n");
  printf("for up to 100 and black for > 200 hits.\n\n");
  printf("A typical strace invocation looks like this:\n");
  printf("strace -e trace=open,close,read,lseek -o strace.txt svn log ...\n");
}

/* linear control flow */
int main(int argc, const char *argv[])
{
  apr_pool_t *pool = NULL;
  apr_file_t *file = NULL;

  apr_initialize();
  atexit(apr_terminate);

  pool = svn_pool_create(NULL);
  files = apr_hash_make(pool);
  handles = apr_hash_make(pool);

  if (argc == 2)
    apr_file_open(&file, argv[1], APR_READ | APR_BUFFERED, APR_OS_DEFAULT,
                  pool);
  if (file == NULL)
    {
      print_usage();
      return 0;
    }
  parse_file(file);
  apr_file_close(file);

  print_stats(pool);

  apr_file_open(&file, "access.bmp",
                APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BUFFERED,
                APR_OS_DEFAULT, pool);
  write_bitmap(get_rev_files(pool), 0, file, pool);
  apr_file_close(file);

  apr_file_open(&file, "access_scaled.bmp",
                APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BUFFERED,
                APR_OS_DEFAULT, pool);
  write_bitmap(get_rev_files(pool), 1024, file, pool);
  apr_file_close(file);

  apr_file_open(&file, "scale.bmp",
                APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BUFFERED,
                APR_OS_DEFAULT, pool);
  write_scale(file);
  apr_file_close(file);

  return 0;
}