diff --git a/clients/drcachesim/CMakeLists.txt b/clients/drcachesim/CMakeLists.txt
index 6dfa9b3dd1160b84e5cd1fcfc4ed571a9e28ccde..f082e7641b1b50f035fedad76c2cca671639031a 100644
--- a/clients/drcachesim/CMakeLists.txt
+++ b/clients/drcachesim/CMakeLists.txt
@@ -77,6 +77,12 @@ add_library(simulator STATIC
   simulator/tlb_simulator.cpp
   )
 
+add_library(raw2trace STATIC
+  tracer/raw2trace.cpp
+  tracer/raw2trace_directory.cpp
+  )
+configure_DynamoRIO_standalone(raw2trace)
+
 add_executable(drcachesim
   launcher.cpp
   analyzer.cpp
@@ -87,15 +93,13 @@ add_executable(drcachesim
   ${zlib_reader}
   reader/ipc_reader.cpp
   simulator/analyzer_interface.cpp
-  # We embed the raw2trace conversion for convenience:
-  tracer/raw2trace.cpp
   tracer/instru.cpp
   tracer/instru_online.cpp
   )
 # In order to embed raw2trace we need to be standalone:
 configure_DynamoRIO_standalone(drcachesim)
 # Link in our tools:
-target_link_libraries(drcachesim simulator reuse_distance histogram reuse_time)
+target_link_libraries(drcachesim simulator reuse_distance histogram reuse_time raw2trace)
 # To avoid dup symbol errors between drinjectlib and the drdecode brought in
 # by drfrontendlib we have to explicitly list drdecode up front:
 target_link_libraries(drcachesim drdecode drinjectlib drconfiglib drfrontendlib)
@@ -164,10 +168,10 @@ install_client_header(tracer/drmemtrace.h)
 
 add_executable(drraw2trace
   tracer/raw2trace_launcher.cpp
-  tracer/raw2trace.cpp
   tracer/instru.cpp
   tracer/instru_online.cpp
   )
+target_link_libraries(drraw2trace raw2trace)
 # To avoid dup symbol errors on some VS builds we list drdecode before DR:
 target_link_libraries(drraw2trace drdecode)
 configure_DynamoRIO_standalone(drraw2trace)
@@ -332,8 +336,23 @@ if (BUILD_TESTS)
     add_win32_flags(tool.drcacheoff.burst_threads)
     target_link_libraries(tool.drcacheoff.burst_threads ${libpthread})
 
-    # FIXME i#2099: the weak symbol is not supported on Windows.
     if (UNIX)
+      if (NOT APPLE)
+        # uses ptrace and looks for linux-specific syscalls
+        add_executable(tool.drcacheoff.raw2trace_io tests/raw2trace_io.cpp
+          tracer/instru.cpp
+          tracer/instru_online.cpp)
+        configure_DynamoRIO_standalone(tool.drcacheoff.raw2trace_io)
+        add_win32_flags(tool.drcacheoff.raw2trace_io)
+        target_link_libraries(tool.drcacheoff.raw2trace_io raw2trace)
+        use_DynamoRIO_extension(tool.drcacheoff.raw2trace_io droption)
+        target_link_libraries(tool.drcacheoff.raw2trace_io drdecode)
+        use_DynamoRIO_extension(tool.drcacheoff.raw2trace_io drcovlib_static)
+        # Because we're leveraging instru_online code we have to link with drutil:
+        use_DynamoRIO_extension(tool.drcacheoff.raw2trace_io drutil_static)
+      endif ()
+
+      # FIXME i#2099: the weak symbol is not supported on Windows.
       add_executable(tool.drcacheoff.burst_client tests/burst_static.cpp)
       append_property_list(TARGET tool.drcacheoff.burst_client
         COMPILE_DEFINITIONS "TEST_APP_DR_CLIENT_MAIN")
diff --git a/clients/drcachesim/analyzer_multi.cpp b/clients/drcachesim/analyzer_multi.cpp
index 3a3c03e1ddb0b23d7a65f4e8f06a9db2262bb65b..c5033245884f4d1df20206a7e4248831fbf89a17 100644
--- a/clients/drcachesim/analyzer_multi.cpp
+++ b/clients/drcachesim/analyzer_multi.cpp
@@ -40,6 +40,7 @@
 # include "reader/compressed_file_reader.h"
 #endif
 #include "reader/ipc_reader.h"
+#include "tracer/raw2trace_directory.h"
 #include "tracer/raw2trace.h"
 
 analyzer_multi_t::analyzer_multi_t()
@@ -65,8 +66,11 @@ analyzer_multi_t::analyzer_multi_t()
             trace_iter = existing;
         else {
             delete existing;
-            raw2trace_t raw2trace(op_indir.get_value(), tracefile);
-            raw2trace.do_conversion();
+            raw2trace_directory_t dir(op_indir.get_value(), tracefile);
+            raw2trace_t raw2trace(dir.modfile_bytes, dir.thread_files, &dir.out_file);
+            std::string error = raw2trace.do_conversion();
+            if (!error.empty())
+                ERRMSG("raw2trace failed: %s", error.c_str());
             trace_iter = new file_reader_t(tracefile.c_str());
         }
         // We don't support a compressed file here (is_complete() is too hard
diff --git a/clients/drcachesim/common/utils.h b/clients/drcachesim/common/utils.h
index 0af5d00e9f22ae961fa32a130be1b6a37b3adf8e..6a32841872dd2a1d9dc3251ef3c7dc1dd04ef527 100644
--- a/clients/drcachesim/common/utils.h
+++ b/clients/drcachesim/common/utils.h
@@ -48,6 +48,7 @@
 #define BUFFER_SIZE_ELEMENTS(buf)   (BUFFER_SIZE_BYTES(buf) / sizeof(buf[0]))
 #define BUFFER_LAST_ELEMENT(buf)    buf[BUFFER_SIZE_ELEMENTS(buf) - 1]
 #define NULL_TERMINATE_BUFFER(buf)  BUFFER_LAST_ELEMENT(buf) = 0
+#define TESTANY(mask, var) (((mask) & (var)) != 0)
 
 #define BOOLS_MATCH(b1, b2) (!!(b1) == !!(b2))
 
diff --git a/clients/drcachesim/tests/raw2trace-simple.templatex b/clients/drcachesim/tests/raw2trace-simple.templatex
new file mode 100644
index 0000000000000000000000000000000000000000..f8de8bd0aff8c2d30e6ddcc0d37f115f215ca824
--- /dev/null
+++ b/clients/drcachesim/tests/raw2trace-simple.templatex
@@ -0,0 +1,5 @@
+\[drmemtrace\]: Reading module file from memory
+open|close
+\[drmemtrace\]: Successfully read [0-9]+ modules
+\[drmemtrace\]: Successfully converted [0-9]+ thread files
+Processed
diff --git a/clients/drcachesim/tests/raw2trace_io.cpp b/clients/drcachesim/tests/raw2trace_io.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bb8b92e63ce126510702e275adaaf86f41a3bc55
--- /dev/null
+++ b/clients/drcachesim/tests/raw2trace_io.cpp
@@ -0,0 +1,166 @@
+/* **********************************************************
+ * Copyright (c) 2017 Google, Inc.  All rights reserved.
+ * **********************************************************/
+
+/*
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ *
+ * * Neither the name of Google, Inc. nor the names of its contributors may be
+ *   used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+
+/* This application processes a set of existing raw files with raw2trace_t.
+ * It uses ptrace to make sure raw2trace only interacts with the filesystem when expected
+ * -- it doesn't open/close any files outside of mapping modules (which should be local).
+ *
+ * XXX: We use ptrace as opposed to running under memtrace with replaced file operations
+ * because raw2trace uses drmodtrack, which doesn't isolate under static memtrace.
+ */
+
+#include "droption.h"
+#include "tracer/raw2trace.h"
+#include "tracer/raw2trace_directory.h"
+#include <cassert>
+#include <errno.h>
+#include <fcntl.h>
+#include <iostream>
+#include <string>
+#include <sys/ptrace.h>
+#include <sys/user.h>
+#include <sys/wait.h>
+#include <syscall.h>
+#include <unistd.h>
+
+static droption_t<std::string> op_indir(DROPTION_SCOPE_FRONTEND, "indir", "",
+                                        "[Required] Directory with trace input files",
+                                        "Specifies a directory with raw files.");
+
+static droption_t<std::string> op_out(DROPTION_SCOPE_FRONTEND, "out", "",
+                                      "[Required] Path to output file",
+                                      "Specifies the path to the output file.");
+#if defined(X64)
+# define SYS_NUM(r) (r).orig_rax
+#elif defined(X86)
+# define SYS_NUM(r) (r).orig_eax
+#else
+# error "Test only supports x86"
+#endif
+
+#ifndef LINUX
+# error "Test only supports Linux"
+#endif
+
+int
+main(int argc, const char *argv[])
+{
+    std::string parse_err;
+    if (!droption_parser_t::parse_argv(DROPTION_SCOPE_FRONTEND, argc, (const char **)argv,
+                                       &parse_err, NULL) ||
+        op_indir.get_value().empty() || op_out.get_value().empty()) {
+        std::cerr << "Usage error: " << parse_err << "\nUsage:\n"
+                  << droption_parser_t::usage_short(DROPTION_SCOPE_ALL);
+        return 1;
+    }
+
+    /* Open input/output files outside of traced region. And explicitly don't destroy dir,
+     * so they never get closed.
+     */
+    raw2trace_directory_t *dir =
+        new raw2trace_directory_t(op_indir.get_value(), op_out.get_value());
+
+    pid_t pid = fork();
+    if (pid == -1) {
+        std::cerr << "Fork failed\n";
+        return 1;
+    }
+    if (pid != 0) {
+        /* parent */
+        long res;
+        int status;
+        while (true) {
+            int wait_res = waitpid(pid, &status, __WALL);
+            if (wait_res < 0) {
+                if (errno == EINTR)
+                    continue;
+                perror("Failed waiting");
+                return 1;
+            }
+            if (WIFEXITED(status))
+                break;
+            user_regs_struct regs;
+            res = ptrace(PTRACE_GETREGS, pid, NULL, &regs);
+            if (res < 0) {
+                perror("ptrace failed");
+                return 1;
+            }
+            /* We don't distinguish syscall entry/exit because orig_rax is set
+             * on both.
+             */
+            if (SYS_NUM(regs) == __NR_open || SYS_NUM(regs) == __NR_openat ||
+                SYS_NUM(regs) == __NR_creat) {
+                std::cerr << "open\n";
+            } else if (SYS_NUM(regs) == __NR_close) {
+                std::cerr << "close\n";
+            }
+            res = ptrace(PTRACE_SYSCALL, pid, NULL, 0);
+            if (res < 0) {
+                perror("ptrace failed");
+                return 1;
+            }
+        }
+        if (WIFEXITED(status)) {
+            return 0;
+        }
+        res = ptrace(PTRACE_DETACH, pid, NULL, NULL);
+        if (res < 0) {
+            perror("ptrace failed");
+            return 1;
+        }
+        std::cerr << "Detached\n";
+        return 0;
+    } else {
+        /* child */
+        long res = ptrace(PTRACE_TRACEME, 0, NULL, NULL);
+        if (res < 0) {
+            perror("ptrace me failed");
+            return 1;
+        }
+        /* Force a wait until parent attaches, so we don't race on the fork. */
+        raise(SIGSTOP);
+
+        /* Sycalls below will be ptraced. We don't expect any open/close calls outside of
+         * raw2trace::read_and_map_modules().
+         */
+        raw2trace_t raw2trace(dir->modfile_bytes, dir->thread_files, &dir->out_file,
+                              GLOBAL_DCONTEXT, 1);
+        std::string error = raw2trace.do_conversion();
+        if (!error.empty()) {
+            std::cerr << "raw2trace failed " << error << "\n";
+            return 1;
+        }
+        std::cerr << "Processed\n";
+        return 0;
+    }
+    return 0;
+}
diff --git a/clients/drcachesim/tracer/raw2trace.cpp b/clients/drcachesim/tracer/raw2trace.cpp
index 52cf258160f54a66628eebfdd33e482b707746a9..4ff3c1588349343f77d9f07b30a86476ca4d22f8 100644
--- a/clients/drcachesim/tracer/raw2trace.cpp
+++ b/clients/drcachesim/tracer/raw2trace.cpp
@@ -34,46 +34,23 @@
  * by the cache simulator and other analysis tools.
  */
 
-#define UNICODE
-
 #include "dr_api.h"
-#include "droption.h"
 #include "drcovlib.h"
-#include "dr_frontend.h"
 #include "raw2trace.h"
 #include "instru.h"
 #include "../common/memref.h"
 #include "../common/trace_entry.h"
+#include <cstring>
 #include <fstream>
+#include <sstream>
 #include <vector>
 
-#ifdef UNIX
-# include <dirent.h> /* opendir, readdir */
-# include <unistd.h> /* getcwd */
-#else
-# include <windows.h>
-# include <direct.h> /* _getcwd */
-# pragma comment(lib, "User32.lib")
-#endif
-
-// XXX: currently the error handling and diagnostics are *not* modular:
-// this class assumes an option op_verbose, and for errors it just
-// prints directly and exits the process.
-// The original plan was to have drcachesim launch this as a separate
-// executable.
-extern droption_t<unsigned int> op_verbose;
-
 // XXX: DR should export this
 #define INVALID_THREAD_ID 0
 
-#define FATAL_ERROR(msg, ...) do { \
-    fprintf(stderr, "ERROR: " msg "\n", ##__VA_ARGS__);    \
-    fflush(stderr); \
-    exit(1); \
-} while (0)
-
-#define CHECK(val, msg, ...) do { \
-    if (!(val)) FATAL_ERROR(msg, ##__VA_ARGS__); \
+// Assumes we return an error string by convention.
+#define CHECK(val, msg) do { \
+    if (!(val)) return msg; \
 } while (0)
 
 #define WARN(msg, ...) do {                                  \
@@ -82,14 +59,14 @@ extern droption_t<unsigned int> op_verbose;
 } while (0)
 
 #define VPRINT(level, ...) do { \
-    if (op_verbose.get_value() >= (level)) { \
+    if (this->verbosity >= (level)) { \
         fprintf(stderr, "[drmemtrace]: "); \
         fprintf(stderr, __VA_ARGS__); \
     } \
 } while (0)
 
 #define DO_VERBOSE(level, x) do { \
-    if (op_verbose.get_value() >= (level)) { \
+    if (this->verbosity >= (level)) { \
         x \
     } \
 } while (0)
@@ -98,24 +75,19 @@ extern droption_t<unsigned int> op_verbose;
  * Module list
  */
 
-void
-raw2trace_t::read_and_map_modules(void)
+std::string
+raw2trace_t::read_and_map_modules(const char *module_map)
 {
     // Read and load all of the modules.
-    std::string modfilename = indir + std::string(DIRSEP) +
-        DRMEMTRACE_MODULE_LIST_FILENAME;
     uint num_mods;
-    VPRINT(1, "Reading module file %s\n", modfilename.c_str());
-    file_t modfile = dr_open_file(modfilename.c_str(), DR_FILE_READ);
-    if (modfile == INVALID_FILE)
-        FATAL_ERROR("Failed to open module file %s", modfilename.c_str());
-    if (drmodtrack_offline_read(modfile, NULL, NULL, &modhandle, &num_mods) !=
+    VPRINT(1, "Reading module file from memory\n");
+    if (drmodtrack_offline_read(INVALID_FILE, module_map, NULL, &modhandle, &num_mods) !=
         DRCOVLIB_SUCCESS)
-        FATAL_ERROR("Failed to parse module file %s", modfilename.c_str());
+        return "Failed to parse module file";
     for (uint i = 0; i < num_mods; i++) {
         drmodtrack_info_t info = {sizeof(info),};
         if (drmodtrack_offline_lookup(modhandle, i, &info) != DRCOVLIB_SUCCESS)
-            FATAL_ERROR("Failed to query module file");
+            return "Failed to query module file";
         if (strcmp(info.path, "<unknown>") == 0 ||
             // i#2062: VDSO is hard to decode so for now we treat is as non-module.
             // FIXME: currently we're dropping the ifetch data: we need the tracer
@@ -146,7 +118,7 @@ raw2trace_t::read_and_map_modules(void)
                 if (strstr(info.path, "dynamorio") != NULL)
                     modvec.push_back(module_t(info.path, info.start, NULL, 0));
                 else
-                    FATAL_ERROR("Failed to map module %s", info.path);
+                    return "Failed to map module " + std::string(info.path);
             } else {
                 VPRINT(1, "Mapped module %d @" PFX " = %s\n", (int)modvec.size(),
                        (ptr_uint_t)base_pc, info.path);
@@ -155,13 +127,14 @@ raw2trace_t::read_and_map_modules(void)
         }
     }
     VPRINT(1, "Successfully read %d modules\n", num_mods);
+    return "";
 }
 
-void
+std::string
 raw2trace_t::unmap_modules(void)
 {
     if (drmodtrack_offline_exit(modhandle) != DRCOVLIB_SUCCESS)
-        FATAL_ERROR("Failed to clean up module table data");
+        return "Failed to clean up module table data";
     for (std::vector<module_t>::iterator mvi = modvec.begin();
          mvi != modvec.end(); ++mvi) {
         if (mvi->map_base != NULL && mvi->map_size != 0) {
@@ -170,90 +143,9 @@ raw2trace_t::unmap_modules(void)
                 WARN("Failed to unmap module %s", mvi->path);
         }
     }
+    return "";
 }
 
-/***************************************************************************
- * Directory iterator
- */
-
-// We open each thread log file in a vector so we can read from them simultaneously.
-void
-raw2trace_t::open_thread_log_file(const char *basename)
-{
-    char path[MAXIMUM_PATH];
-    CHECK(basename[0] != '/',
-          "dir iterator entry %s should not be an absolute path\n", basename);
-    // Skip the module list log.
-    if (strcmp(basename, DRMEMTRACE_MODULE_LIST_FILENAME) == 0)
-        return;
-    // Skip any non-.raw in case someone put some other file in there.
-    if (strstr(basename, OUTFILE_SUFFIX) == NULL)
-        return;
-    if (dr_snprintf(path, BUFFER_SIZE_ELEMENTS(path), "%s%s%s",
-                    indir.c_str(), DIRSEP, basename) <= 0) {
-        FATAL_ERROR("Failed to get full path of file %s", basename);
-    }
-    NULL_TERMINATE_BUFFER(path);
-    thread_files.push_back(new std::ifstream(path, std::ifstream::binary));
-    if (!(*thread_files.back()))
-        FATAL_ERROR("Failed to open thread log file %s", path);
-    // Check version header.
-    offline_entry_t ver_entry;
-    if (!thread_files.back()->read((char*)&ver_entry, sizeof(ver_entry)))
-        FATAL_ERROR("Unable to read thread log file %s", path);
-    if (ver_entry.extended.type != OFFLINE_TYPE_EXTENDED ||
-        ver_entry.extended.ext != OFFLINE_EXT_TYPE_HEADER)
-        FATAL_ERROR("Thread log file %s is corrupted: missing version entry", path);
-    if (ver_entry.extended.value != OFFLINE_FILE_VERSION) {
-        FATAL_ERROR("Version mismatch: expect %d vs %d in file %s",
-                    OFFLINE_FILE_VERSION, (int)ver_entry.extended.value, path);
-    }
-    VPRINT(1, "Opened thread log file %s\n", path);
-}
-
-#ifdef UNIX
-void
-raw2trace_t::open_thread_files()
-{
-    struct dirent *ent;
-    DIR *dir = opendir(indir.c_str());
-    VPRINT(1, "Iterating dir %s\n", indir.c_str());
-    if (dir == NULL)
-        FATAL_ERROR("Failed to list directory %s", indir.c_str());
-    while ((ent = readdir(dir)) != NULL)
-        open_thread_log_file(ent->d_name);
-    closedir (dir);
-}
-#else
-void
-raw2trace_t::open_thread_files()
-{
-    HANDLE find = INVALID_HANDLE_VALUE;
-    WIN32_FIND_DATAW data;
-    char path[MAXIMUM_PATH];
-    TCHAR wpath[MAXIMUM_PATH];
-    VPRINT(1, "Iterating dir %s\n", indir.c_str());
-    // Append \*
-    dr_snprintf(path, BUFFER_SIZE_ELEMENTS(path), "%s\\*", indir.c_str());
-    NULL_TERMINATE_BUFFER(path);
-    if (drfront_char_to_tchar(path, wpath, BUFFER_SIZE_ELEMENTS(wpath)) !=
-        DRFRONT_SUCCESS)
-        FATAL_ERROR("Failed to convert from utf-8 to utf-16");
-    find = FindFirstFileW(wpath, &data);
-    if (find == INVALID_HANDLE_VALUE)
-        FATAL_ERROR("Failed to list directory %s\n", indir.c_str());
-    do {
-        if (!TESTANY(data.dwFileAttributes, FILE_ATTRIBUTE_DIRECTORY)) {
-            if (drfront_tchar_to_char(data.cFileName, path, BUFFER_SIZE_ELEMENTS(path)) !=
-                DRFRONT_SUCCESS)
-                FATAL_ERROR("Failed to convert from utf-16 to utf-8");
-            open_thread_log_file(path);
-        }
-    } while (FindNextFile(find, &data) != 0);
-    FindClose(find);
-}
-#endif
-
 /***************************************************************************
  * Disassembly to fill in instr and memref entries
  */
@@ -271,14 +163,14 @@ instr_is_rep_string(instr_t *instr)
 #endif
 }
 
-trace_entry_t *
-raw2trace_t::append_memref(trace_entry_t *buf_in, uint tidx, instr_t *instr,
+std::string
+raw2trace_t::append_memref(INOUT trace_entry_t **buf_in, uint tidx, instr_t *instr,
                            opnd_t ref, bool write)
 {
-    trace_entry_t *buf = buf_in;
+    trace_entry_t *buf = *buf_in;
     offline_entry_t in_entry;
     if (!thread_files[tidx]->read((char*)&in_entry, sizeof(in_entry)))
-        FATAL_ERROR("Trace ends mid-block");
+        return "Trace ends mid-block";
     if (in_entry.addr.type != OFFLINE_TYPE_MEMREF &&
         in_entry.addr.type != OFFLINE_TYPE_MEMREF_HIGH) {
         // This happens when there are predicated memrefs in the bb.
@@ -290,7 +182,7 @@ raw2trace_t::append_memref(trace_entry_t *buf_in, uint tidx, instr_t *instr,
         // Put back the entry.
         thread_files[tidx]->seekg(-(std::streamoff)sizeof(in_entry),
                                   thread_files[tidx]->cur);
-        return buf;
+        return "";
     }
     if (instr_is_prefetch(instr)) {
         buf->type = instru_t::instr_to_prefetch_type(instr);
@@ -308,12 +200,12 @@ raw2trace_t::append_memref(trace_entry_t *buf_in, uint tidx, instr_t *instr,
     // We take the full value, to handle low or high.
     buf->addr = (addr_t) in_entry.combined_value;
     VPRINT(4, "Appended memref to " PFX "\n", (ptr_uint_t)buf->addr);
-    ++buf;
-    return buf;
+    *buf_in = ++buf;
+    return "";
 }
 
-bool
-raw2trace_t::append_bb_entries(uint tidx, offline_entry_t *in_entry)
+std::string
+raw2trace_t::append_bb_entries(uint tidx, offline_entry_t *in_entry, OUT bool *handled)
 {
     uint instr_count = in_entry->pc.instr_count;
     instr_t instr;
@@ -326,7 +218,8 @@ raw2trace_t::append_bb_entries(uint tidx, offline_entry_t *in_entry)
         // Once that support is in we can remove the bool return value and handle
         // the memrefs up here.
         VPRINT(3, "Skipping ifetch for %u instrs not in a module\n", instr_count);
-        return false;
+        *handled = false;
+        return "";
     } else {
         VPRINT(3, "Appending %u instrs in bb " PFX " in mod %u +" PIFX " = %s\n",
                instr_count, (ptr_uint_t)start_pc, (uint)in_entry->pc.modidx,
@@ -387,30 +280,35 @@ raw2trace_t::append_bb_entries(uint tidx, offline_entry_t *in_entry)
             (instr_reads_memory(&instr) || instr_writes_memory(&instr))) {
             for (int i = 0; i < instr_num_srcs(&instr); i++) {
                 if (opnd_is_memory_reference(instr_get_src(&instr, i))) {
-                    buf = append_memref(buf, tidx, &instr, instr_get_src(&instr, i),
-                                        false);
+                    std::string error = append_memref(&buf, tidx, &instr,
+                                                      instr_get_src(&instr, i), false);
+                    if (!error.empty())
+                        return error;
                 }
             }
             for (int i = 0; i < instr_num_dsts(&instr); i++) {
                 if (opnd_is_memory_reference(instr_get_dst(&instr, i))) {
-                    buf = append_memref(buf, tidx, &instr, instr_get_dst(&instr, i),
-                                        true);
+                    std::string error = append_memref(&buf, tidx, &instr,
+                                                      instr_get_dst(&instr, i), true);
+                    if (!error.empty())
+                        return error;
                 }
             }
         }
         CHECK((size_t)(buf - buf_start) < MAX_COMBINED_ENTRIES, "Too many entries");
-        if (!out_file.write((char*)buf_start, (buf - buf_start)*sizeof(trace_entry_t)))
-            FATAL_ERROR("Failed to write to output file");
+        if (!out_file->write((char*)buf_start, (buf - buf_start)*sizeof(trace_entry_t)))
+            return "Failed to write to output file";
     }
     instr_free(dcontext, &instr);
-    return true;
+    *handled = true;
+    return "";
 }
 
 /***************************************************************************
  * Top-level
  */
 
-void
+std::string
 raw2trace_t::merge_and_process_thread_files()
 {
     // The current thread we're processing is tidx.  If it's set to thread_files.size()
@@ -440,9 +338,9 @@ raw2trace_t::merge_and_process_thread_files()
                 if (times[i] == 0 && !thread_files[i]->eof()) {
                     offline_entry_t entry;
                     if (!thread_files[i]->read((char*)&entry, sizeof(entry)))
-                        FATAL_ERROR("Failed to read from input file");
+                        return "Failed to read from input file";
                     if (entry.timestamp.type != OFFLINE_TYPE_TIMESTAMP)
-                        FATAL_ERROR("Missing timestamp entry");
+                        return "Missing timestamp entry";
                     times[i] = entry.timestamp.usec;
                     VPRINT(3, "Thread %u timestamp is @0x" ZHEX64_FORMAT_STRING
                            "\n", (uint)tids[i], times[i]);
@@ -464,8 +362,8 @@ raw2trace_t::merge_and_process_thread_files()
             if (size > 0) {
                 // We have to write this now before we append any bb entries.
                 CHECK((uint)size < MAX_COMBINED_ENTRIES, "Too many entries");
-                if (!out_file.write((char*)buf_base, size))
-                    FATAL_ERROR("Failed to write to output file");
+                if (!out_file->write((char*)buf_base, size))
+                    return "Failed to write to output file";
                 buf = buf_base;
             }
             size = 0;
@@ -474,13 +372,16 @@ raw2trace_t::merge_and_process_thread_files()
                (uint)tids[tidx], (int)thread_files[tidx]->tellg());
         if (!thread_files[tidx]->read((char*)&in_entry, sizeof(in_entry))) {
             if (thread_files[tidx]->eof()) {
-                // Rather than a FATAL_ERROR we try to continue to provide partial
+                // Rather than a fatal error we try to continue to provide partial
                 // results in case the disk was full or there was some other issue.
                 WARN("Input file for thread %d is truncated", (uint)tids[tidx]);
                 in_entry.extended.type = OFFLINE_TYPE_EXTENDED;
                 in_entry.extended.ext = OFFLINE_EXT_TYPE_FOOTER;
-            } else
-                FATAL_ERROR("Failed to read from file for thread %d", (uint)tids[tidx]);
+            } else {
+                std::stringstream ss;
+                ss << "Failed to read from file for thread " << (uint)tids[tidx];
+                return ss.str();
+            }
         }
         if (in_entry.extended.type == OFFLINE_TYPE_EXTENDED) {
             if (in_entry.extended.ext == OFFLINE_EXT_TYPE_FOOTER) {
@@ -488,15 +389,18 @@ raw2trace_t::merge_and_process_thread_files()
                 offline_entry_t entry;
                 if (thread_files[tidx]->read((char*)&entry, sizeof(entry)) ||
                     !thread_files[tidx]->eof())
-                    FATAL_ERROR("Footer is not the final entry");
+                    return "Footer is not the final entry";
                 CHECK(tids[tidx] != INVALID_THREAD_ID, "Missing thread id");
                 VPRINT(2, "Thread %d exit\n", (uint)tids[tidx]);
                 size += instru.append_thread_exit(buf, tids[tidx]);
                 buf += size;
                 --thread_count;
                 tidx = (uint)thread_files.size(); // Request thread scan.
-            } else
-                FATAL_ERROR("Invalid extension type %d", (int)in_entry.extended.ext);
+            } else {
+                std::stringstream ss;
+                ss << "Invalid extension type " << (int)in_entry.extended.ext;
+                return ss.str();
+            }
         } else if (in_entry.timestamp.type == OFFLINE_TYPE_TIMESTAMP) {
             VPRINT(2, "Thread %u timestamp 0x" ZHEX64_FORMAT_STRING "\n",
                    (uint)tids[tidx], in_entry.timestamp.usec);
@@ -517,10 +421,12 @@ raw2trace_t::merge_and_process_thread_files()
                 buf += size;
             } else {
                 // We should see an instr entry first
-                CHECK(false, "memref entry found outside of bb");
+                return "memref entry found outside of bb";
             }
         } else if (in_entry.pc.type == OFFLINE_TYPE_PC) {
-            last_bb_handled = append_bb_entries(tidx, &in_entry);
+          std::string result = append_bb_entries(tidx, &in_entry, &last_bb_handled);
+          if (!result.empty())
+              return result;
         } else if (in_entry.tid.type == OFFLINE_TYPE_THREAD) {
             VPRINT(2, "Thread %u entry\n", (uint)in_entry.tid.tid);
             if (tids[tidx] == INVALID_THREAD_ID)
@@ -535,68 +441,91 @@ raw2trace_t::merge_and_process_thread_files()
             offline_entry_t entry;
             if (!thread_files[tidx]->read((char*)&entry, sizeof(entry)) ||
                 entry.addr.type != OFFLINE_TYPE_IFLUSH)
-                FATAL_ERROR("Flush missing 2nd entry");
+                return "Flush missing 2nd entry";
             VPRINT(2, "Flush " PFX"-" PFX"\n", (ptr_uint_t)in_entry.addr.addr,
                    (ptr_uint_t)entry.addr.addr);
             size += instru.append_iflush(buf, in_entry.addr.addr,
                                          (size_t)(entry.addr.addr - in_entry.addr.addr));
             buf += size;
-        } else
-            FATAL_ERROR("Unknown trace type %d", (int)in_entry.timestamp.type);
+        } else {
+            std::stringstream ss;
+            ss << "Unknown trace type " << (int)in_entry.timestamp.type;
+            return ss.str();
+        }
         if (size > 0) {
             CHECK((uint)size < MAX_COMBINED_ENTRIES, "Too many entries");
-            if (!out_file.write((char*)buf_base, size))
-                FATAL_ERROR("Failed to write to output file");
+            if (!out_file->write((char*)buf_base, size))
+                return "Failed to write to output file";
         }
     } while (thread_count > 0);
+    return "";
 }
 
-void
+std::string
+raw2trace_t::check_thread_file(std::istream *f)
+{
+    // Check version header.
+    offline_entry_t ver_entry;
+    if (!f->read((char*)&ver_entry, sizeof(ver_entry))) {
+        return "Unable to read thread log file";
+    }
+    if (ver_entry.extended.type != OFFLINE_TYPE_EXTENDED ||
+        ver_entry.extended.ext != OFFLINE_EXT_TYPE_HEADER) {
+        return "Thread log file is corrupted: missing version entry";
+    }
+    if (ver_entry.extended.value != OFFLINE_FILE_VERSION) {
+        std::stringstream ss;
+        ss << "Version mismatch: expect " << OFFLINE_FILE_VERSION << " vs "
+           << (int)ver_entry.extended.value;
+        return ss.str();
+    }
+    return "";
+}
+
+std::string
 raw2trace_t::do_conversion()
 {
+    std::string error = read_and_map_modules(modmap);
+    if (!error.empty())
+        return error;
     trace_entry_t entry;
     entry.type = TRACE_TYPE_HEADER;
     entry.size = 0;
     entry.addr = TRACE_ENTRY_VERSION;
-    if (!out_file.write((char*)&entry, sizeof(entry)))
-        FATAL_ERROR("Failed to write header to output file %s", outname.c_str());
+    if (!out_file->write((char*)&entry, sizeof(entry)))
+        return "Failed to write header to output file";
 
-    read_and_map_modules();
-    open_thread_files();
     merge_and_process_thread_files();
 
     entry.type = TRACE_TYPE_FOOTER;
     entry.size = 0;
     entry.addr = 0;
-    if (!out_file.write((char*)&entry, sizeof(entry)))
-        FATAL_ERROR("Failed to write footer to output file %s", outname.c_str());
+    if (!out_file->write((char*)&entry, sizeof(entry)))
+        return "Failed to write footer to output file";
+    VPRINT(1, "Successfully converted %zu thread files\n", thread_files.size());
+    return "";
 }
 
-raw2trace_t::raw2trace_t(std::string indir_in, std::string outname_in)
-    : indir(indir_in), outname(outname_in), prev_instr_was_rep_string(false),
-      instrs_are_separate(false)
+raw2trace_t::raw2trace_t(const char *module_map_in,
+                         const std::vector<std::istream*> &thread_files_in,
+                         std::ostream *out_file_in,
+                         void *dcontext_in,
+                         unsigned int verbosity_in)
+    : modmap(module_map_in), thread_files(thread_files_in), out_file(out_file_in),
+      dcontext(dcontext_in), prev_instr_was_rep_string(false), instrs_are_separate(false),
+      verbosity(verbosity_in)
 {
-    // Support passing both base dir and raw/ subdir.
-    if (indir.find(OUTFILE_SUBDIR) == std::string::npos)
-        indir += std::string(DIRSEP) + OUTFILE_SUBDIR;
-    out_file.open(outname.c_str(), std::ofstream::binary);
-    if (!out_file)
-        FATAL_ERROR("Failed to open output file %s", outname.c_str());
-    VPRINT(1, "Writing to %s\n", outname.c_str());
-
-    dcontext = dr_standalone_init();
+    if (dcontext == NULL) {
+        dcontext = dr_standalone_init();
 #ifdef ARM
-    // We keep the mode at ARM and rely on LSB=1 offsets in the modoffs fields
-    // to trigger Thumb decoding.
-    dr_set_isa_mode(dcontext, DR_ISA_ARM_A32, NULL);
+        // We keep the mode at ARM and rely on LSB=1 offsets in the modoffs fields
+        // to trigger Thumb decoding.
+        dr_set_isa_mode(dcontext, DR_ISA_ARM_A32, NULL);
 #endif
+    }
 }
 
 raw2trace_t::~raw2trace_t()
 {
-    out_file.close();
-    for (std::vector<std::ifstream*>::iterator fi = thread_files.begin();
-         fi != thread_files.end(); ++fi)
-        (*fi)->close();
     unmap_modules();
 }
diff --git a/clients/drcachesim/tracer/raw2trace.h b/clients/drcachesim/tracer/raw2trace.h
index 4af3b3fa108cf6351b776067c2e43fe73b9e8cf4..d736a3eaf9762afdb433806b7f939259729d69a7 100644
--- a/clients/drcachesim/tracer/raw2trace.h
+++ b/clients/drcachesim/tracer/raw2trace.h
@@ -38,7 +38,7 @@
 
 #include "dr_api.h"
 #include "drmemtrace.h"
-#include "../common/trace_entry.h"
+#include "trace_entry.h"
 #include <fstream>
 #include <vector>
 
@@ -58,32 +58,37 @@ struct module_t {
 
 class raw2trace_t {
 public:
-    raw2trace_t(std::string indir, std::string outname);
+    // module_map, thread_files and out_file are all owned and opened/closed by the
+    // caller.
+    raw2trace_t(const char *module_map, const std::vector<std::istream*> &thread_files,
+                std::ostream *out_file, void *dcontext = NULL,
+                unsigned int verbosity = 0);
     ~raw2trace_t();
-    void do_conversion();
+    // Returns non-empty error message on failure.
+    std::string do_conversion();
+    static std::string check_thread_file(std::istream *f);
 
 private:
-    void read_and_map_modules(void);
-    void unmap_modules(void);
-    void open_thread_log_file(const char *basename);
-    void open_thread_files();
-    void merge_and_process_thread_files();
-    bool append_bb_entries(uint tidx, offline_entry_t *in_entry);
-    trace_entry_t *append_memref(trace_entry_t *buf_in, uint tidx, instr_t *instr,
-                                 opnd_t ref, bool write);
+    std::string read_and_map_modules(const char *module_map);
+    std::string unmap_modules(void);
+    std::string merge_and_process_thread_files();
+    std::string append_bb_entries(uint tidx, offline_entry_t *in_entry,
+                                  OUT bool *handled);
+    std::string append_memref(INOUT trace_entry_t **buf_in, uint tidx, instr_t *instr,
+                              opnd_t ref, bool write);
 
-    std::string indir;
-    std::string outname;
-    std::ofstream out_file;
     static const uint MAX_COMBINED_ENTRIES = 64;
+    const char *modmap;
     void *modhandle;
     std::vector<module_t> modvec;
-    std::vector<std::ifstream*> thread_files;
+    std::vector<std::istream*> thread_files;
+    std::ostream *out_file;
     void *dcontext;
     bool prev_instr_was_rep_string;
     // This indicates that each memref has its own PC entry and that each
     // icache entry does not need to be considered a memref PC entry as well.
     bool instrs_are_separate;
+    unsigned int verbosity;
 };
 
 #endif /* _RAW2TRACE_H_ */
diff --git a/clients/drcachesim/tracer/raw2trace_directory.cpp b/clients/drcachesim/tracer/raw2trace_directory.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ee6c1bc8f2cdb4b12564f8778d713cc755b9e6d0
--- /dev/null
+++ b/clients/drcachesim/tracer/raw2trace_directory.cpp
@@ -0,0 +1,184 @@
+/* **********************************************************
+ * Copyright (c) 2017 Google, Inc.  All rights reserved.
+ * **********************************************************/
+
+/*
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ *
+ * * Neither the name of Google, Inc. nor the names of its contributors may be
+ *   used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL VMWARE, INC. OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+
+/* raw2trace helper to iterate directories and open files.
+ * Separate from raw2trace_t, so that raw2trace doesn't depend on dr_frontend.
+ */
+
+#include <cstring>
+#include <iostream>
+#include <vector>
+
+#ifdef UNIX
+# include <dirent.h> /* opendir, readdir */
+# include <unistd.h> /* getcwd */
+#else
+# define UNICODE
+# define _UNICODE
+# define WIN32_LEAN_AND_MEAN
+# include <windows.h>
+# include <direct.h> /* _getcwd */
+# pragma comment(lib, "User32.lib")
+#endif
+
+#include "dr_api.h"
+#include "dr_frontend.h"
+#include "raw2trace.h"
+#include "raw2trace_directory.h"
+#include "utils.h"
+
+#define FATAL_ERROR(msg, ...) do { \
+    fprintf(stderr, "ERROR: " msg "\n", ##__VA_ARGS__);    \
+    fflush(stderr); \
+    exit(1); \
+} while (0)
+
+#define CHECK(val, msg, ...) do { \
+    if (!(val)) FATAL_ERROR(msg, ##__VA_ARGS__); \
+} while (0)
+
+#define VPRINT(level, ...) do { \
+    if (this->verbosity >= (level)) { \
+        fprintf(stderr, "[drmemtrace]: "); \
+        fprintf(stderr, __VA_ARGS__); \
+    } \
+} while (0)
+
+#ifdef UNIX
+void
+raw2trace_directory_t::open_thread_files()
+{
+    struct dirent *ent;
+    DIR *dir = opendir(indir.c_str());
+    VPRINT(1, "Iterating dir %s\n", indir.c_str());
+    if (dir == NULL)
+        FATAL_ERROR("Failed to list directory %s", indir.c_str());
+    while ((ent = readdir(dir)) != NULL)
+        open_thread_log_file(ent->d_name);
+    closedir (dir);
+}
+#else
+void
+raw2trace_directory_t::open_thread_files()
+{
+    HANDLE find = INVALID_HANDLE_VALUE;
+    WIN32_FIND_DATAW data;
+    char path[MAXIMUM_PATH];
+    TCHAR wpath[MAXIMUM_PATH];
+    VPRINT(1, "Iterating dir %s\n", indir.c_str());
+    // Append \*
+    dr_snprintf(path, BUFFER_SIZE_ELEMENTS(path), "%s\\*", indir.c_str());
+    NULL_TERMINATE_BUFFER(path);
+    if (drfront_char_to_tchar(path, wpath, BUFFER_SIZE_ELEMENTS(wpath)) !=
+        DRFRONT_SUCCESS)
+        FATAL_ERROR("Failed to convert from utf-8 to utf-16");
+    find = FindFirstFileW(wpath, &data);
+    if (find == INVALID_HANDLE_VALUE)
+        FATAL_ERROR("Failed to list directory %s\n", indir.c_str());
+    do {
+        if (!TESTANY(data.dwFileAttributes, FILE_ATTRIBUTE_DIRECTORY)) {
+            if (drfront_tchar_to_char(data.cFileName, path, BUFFER_SIZE_ELEMENTS(path)) !=
+                DRFRONT_SUCCESS)
+                FATAL_ERROR("Failed to convert from utf-16 to utf-8");
+            open_thread_log_file(path);
+        }
+    } while (FindNextFile(find, &data) != 0);
+    FindClose(find);
+}
+#endif
+
+void
+raw2trace_directory_t::open_thread_log_file(const char *basename)
+{
+    char path[MAXIMUM_PATH];
+    CHECK(basename[0] != '/',
+          "dir iterator entry %s should not be an absolute path\n", basename);
+    // Skip the module list log.
+    if (strcmp(basename, DRMEMTRACE_MODULE_LIST_FILENAME) == 0)
+        return;
+    // Skip any non-.raw in case someone put some other file in there.
+    if (strstr(basename, OUTFILE_SUFFIX) == NULL)
+        return;
+    if (dr_snprintf(path, BUFFER_SIZE_ELEMENTS(path), "%s%s%s",
+                    indir.c_str(), DIRSEP, basename) <= 0) {
+        FATAL_ERROR("Failed to get full path of file %s", basename);
+    }
+    NULL_TERMINATE_BUFFER(path);
+    thread_files.push_back(new std::ifstream(path, std::ifstream::binary));
+    if (!(*thread_files.back()))
+        FATAL_ERROR("Failed to open thread log file %s", path);
+    std::string error = raw2trace_t::check_thread_file(thread_files.back());
+    if (!error.empty()) {
+        FATAL_ERROR("Failed sanity checks for thread log file %s: %s", path,
+                    error.c_str());
+    }
+    VPRINT(1, "Opened thread log file %s\n", path);
+}
+
+raw2trace_directory_t::raw2trace_directory_t(const std::string &indir_in,
+                                             const std::string &outname_in,
+                                             unsigned int verbosity_in)
+    : indir(indir_in), outname(outname_in), verbosity(verbosity_in)
+{
+    // Support passing both base dir and raw/ subdir.
+    if (indir.find(OUTFILE_SUBDIR) == std::string::npos)
+        indir += std::string(DIRSEP) + OUTFILE_SUBDIR;
+    std::string modfilename = indir + std::string(DIRSEP) +
+        DRMEMTRACE_MODULE_LIST_FILENAME;
+    modfile = dr_open_file(modfilename.c_str(), DR_FILE_READ);
+    if (modfile == INVALID_FILE)
+        FATAL_ERROR("Failed to open module file %s", modfilename.c_str());
+    uint64 modfile_size;
+    if (!dr_file_size(modfile, &modfile_size))
+        FATAL_ERROR("Failed to get module file size: %s", modfilename.c_str());
+    size_t modfile_size_ = (size_t)modfile_size;
+    modfile_bytes = new char[modfile_size_];
+    if (dr_read_file(modfile, modfile_bytes, modfile_size_) < (ssize_t)modfile_size_)
+        FATAL_ERROR("Didn't read whole module file %s", modfilename.c_str());
+
+    out_file.open(outname.c_str(), std::ofstream::binary);
+    if (!out_file)
+        FATAL_ERROR("Failed to open output file %s", outname.c_str());
+    VPRINT(1, "Writing to %s\n", outname.c_str());
+
+    open_thread_files();
+}
+
+raw2trace_directory_t::~raw2trace_directory_t()
+{
+    delete[] modfile_bytes;
+    dr_close_file(modfile);
+    for (std::vector<std::istream*>::iterator fi = thread_files.begin();
+         fi != thread_files.end(); ++fi) {
+        delete *fi;
+    }
+}
diff --git a/clients/drcachesim/tracer/raw2trace_directory.h b/clients/drcachesim/tracer/raw2trace_directory.h
new file mode 100644
index 0000000000000000000000000000000000000000..afbe75a65385f049f13b31810e564800d9c72b85
--- /dev/null
+++ b/clients/drcachesim/tracer/raw2trace_directory.h
@@ -0,0 +1,61 @@
+/* **********************************************************
+ * Copyright (c) 2017 Google, Inc.  All rights reserved.
+ * **********************************************************/
+
+/*
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ *
+ * * Neither the name of Google, Inc. nor the names of its contributors may be
+ *   used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL VMWARE, INC. OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+
+#ifndef _RAW2TRACE_HELPER_H_
+#define _RAW2TRACE_HELPER_H_ 1
+
+#endif  /* _RAW2TRACE_HELPER_H_ */
+
+#include <fstream>
+#include <string>
+#include <vector>
+
+#include "dr_api.h"
+
+class raw2trace_directory_t {
+public:
+    raw2trace_directory_t(const std::string &indir, const std::string &outname,
+                          unsigned int verbosity = 0);
+    ~raw2trace_directory_t();
+
+    char *modfile_bytes;
+    std::vector<std::istream*> thread_files;
+    std::ofstream out_file;
+
+private:
+    void open_thread_files();
+    void open_thread_log_file(const char *basename);
+    file_t modfile;
+    std::string indir;
+    std::string outname;
+    unsigned int verbosity;
+};
diff --git a/clients/drcachesim/tracer/raw2trace_launcher.cpp b/clients/drcachesim/tracer/raw2trace_launcher.cpp
index 54eab671cf256b33adecc72a4e867eb1ee1126be..49000f96086521a16fa180c14497376b225ef723 100644
--- a/clients/drcachesim/tracer/raw2trace_launcher.cpp
+++ b/clients/drcachesim/tracer/raw2trace_launcher.cpp
@@ -39,16 +39,10 @@
 # include <windows.h>
 #endif
 
-#include "dr_api.h"
 #include "droption.h"
 #include "dr_frontend.h"
 #include "raw2trace.h"
-
-#define FATAL_ERROR(msg, ...) do { \
-    fprintf(stderr, "ERROR: " msg "\n", ##__VA_ARGS__);    \
-    fflush(stderr); \
-    exit(1); \
-} while (0)
+#include "raw2trace_directory.h"
 
 static droption_t<std::string> op_indir
 (DROPTION_SCOPE_FRONTEND, "indir", "", "[Required] Directory with trace input files",
@@ -58,11 +52,16 @@ static droption_t<std::string> op_out
 (DROPTION_SCOPE_FRONTEND, "out", "", "[Required] Path to output file",
  "Specifies the path to the output file.");
 
-// Non-static for use by raw2trace.cpp
-droption_t<unsigned int> op_verbose
+static droption_t<unsigned int> op_verbose
 (DROPTION_SCOPE_FRONTEND, "verbose", 0, "Verbosity level for diagnostic output",
  "Verbosity level for diagnostic output.");
 
+#define FATAL_ERROR(msg, ...) do { \
+    fprintf(stderr, "ERROR: " msg "\n", ##__VA_ARGS__);    \
+    fflush(stderr); \
+    exit(1); \
+} while (0)
+
 int
 _tmain(int argc, const TCHAR *targv[])
 {
@@ -80,7 +79,14 @@ _tmain(int argc, const TCHAR *targv[])
         FATAL_ERROR("Usage error: %s\nUsage:\n%s", parse_err.c_str(),
                     droption_parser_t::usage_short(DROPTION_SCOPE_ALL).c_str());
     }
-    raw2trace_t raw2trace(op_indir.get_value(), op_out.get_value());
-    raw2trace.do_conversion();
+
+    raw2trace_directory_t dir(op_indir.get_value(), op_out.get_value(),
+                              op_verbose.get_value());
+    raw2trace_t raw2trace(dir.modfile_bytes, dir.thread_files, &dir.out_file, NULL,
+                          op_verbose.get_value());
+    std::string error = raw2trace.do_conversion();
+    if (!error.empty())
+        FATAL_ERROR("Conversion failed: %s", error.c_str());
+
     return 0;
 }
diff --git a/suite/tests/CMakeLists.txt b/suite/tests/CMakeLists.txt
index 10de686d2bc9df578ebe231328ecd61193289445..72929231adff3a37f8511ba221ec82a910dfd756 100644
--- a/suite/tests/CMakeLists.txt
+++ b/suite/tests/CMakeLists.txt
@@ -2563,6 +2563,29 @@ if (CLIENT_INTERFACE)
           torunonly_drcacheoff(burst_threads tool.drcacheoff.burst_threads "" "")
           set(tool.drcacheoff.burst_threads_nodr ON)
 
+          if (NOT APPPLE)
+            # Test that raw2trace doesn't do more IO than it should.
+            get_target_path_for_execution(raw2trace_io_path tool.drcacheoff.raw2trace_io)
+            prefix_cmd_if_necessary(raw2trace_io_path ON ${raw2trace_io_path})
+            macro (torunonly_raw2trace testname exetgt extra_ops app_args)
+              torunonly_ci(tool.raw2trace.${testname} ${exetgt} drcachesim
+                "raw2trace-${testname}.c" # for templatex basename
+                "-offline ${extra_ops}" "" "${app_args}")
+              set(tool.raw2trace.${testname}_toolname "drcachesim")
+              set(tool.raw2trace.${testname}_basedir
+                "${PROJECT_SOURCE_DIR}/clients/drcachesim/tests")
+              set(tool.raw2trace.${testname}_rawtemp ON) # no preprocessor
+              set(tool.raw2trace.${testname}_runcmp
+                "${CMAKE_CURRENT_SOURCE_DIR}/runmulti.cmake")
+              set(tool.raw2trace.${testname}_precmd
+                "foreach@${CMAKE_COMMAND}@-E@remove_directory@drmemtrace.${exetgt}.*.dir")
+              set(tool.raw2trace.${testname}_postcmd
+              "${raw2trace_io_path}@-indir@drmemtrace.${exetgt}.*.dir@-out@drraw2trace.${exetgt}.out")
+            endmacro()
+            # actual trace processing not important -- set a very small limit for speed
+            torunonly_raw2trace(simple ${ci_shared_app} "-max_trace_size 8K" "")
+          endif()
+
           # FIXME i#2099: the weak symbol is not supported not work on Windows
           torunonly_drcacheoff(burst_client tool.drcacheoff.burst_client "" "")
           set(tool.drcacheoff.burst_client_nodr ON)