diff --git a/zircon/system/ulib/trace-reader/BUILD.gn b/zircon/system/ulib/trace-reader/BUILD.gn
index 1a1bdbe336c8073230a078026a7691921404c166..c4447a570eb8767286f292ad40a72a3c64b32b34 100644
--- a/zircon/system/ulib/trace-reader/BUILD.gn
+++ b/zircon/system/ulib/trace-reader/BUILD.gn
@@ -5,12 +5,14 @@
 library("trace-reader") {
   sdk = "source"
   sdk_headers = [
+    "trace-reader/file_reader.h",
     "trace-reader/reader.h",
     "trace-reader/reader_internal.h",
     "trace-reader/records.h",
   ]
   host = true
   sources = [
+    "file_reader.cpp",
     "reader.cpp",
     "reader_internal.cpp",
     "records.cpp",
@@ -30,6 +32,7 @@ library("trace-reader") {
 
 test("trace-reader-test") {
   sources = [
+    "file_reader_tests.cpp",
     "reader_tests.cpp",
     "records_tests.cpp",
   ]
diff --git a/zircon/system/ulib/trace-reader/file_reader.cpp b/zircon/system/ulib/trace-reader/file_reader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..664e07eef09237d8a60b89d23695d955b9388b72
--- /dev/null
+++ b/zircon/system/ulib/trace-reader/file_reader.cpp
@@ -0,0 +1,60 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <trace-reader/file_reader.h>
+
+namespace trace {
+
+// static
+bool FileReader::Create(const char* file_path,
+                        RecordConsumer record_consumer,
+                        ErrorHandler error_handler,
+                        std::unique_ptr<FileReader>* out_reader) {
+    ZX_DEBUG_ASSERT(out_reader != nullptr);
+    FILE* f = fopen(file_path, "rb");
+    if (f == nullptr) {
+        return false;
+    }
+
+    out_reader->reset(new FileReader(f, std::move(record_consumer),
+                                     std::move(error_handler)));
+    return true;
+}
+
+FileReader::FileReader(FILE* file, RecordConsumer record_consumer,
+                       ErrorHandler error_handler)
+    : TraceReader(std::move(record_consumer),
+                  std::move(error_handler)),
+      file_(file) {
+}
+
+void FileReader::ReadFile() {
+    for (;;) {
+        size_t to_read = buffer_.size() - buffer_end_;
+        size_t actual = fread(buffer_.data() + buffer_end_, 1u, to_read, file_);
+
+        if (actual == 0) {
+            break;
+        }
+
+        buffer_end_ += actual;
+        size_t bytes_available = buffer_end_;
+
+        size_t bytes_consumed;
+        trace::Chunk chunk(reinterpret_cast<const uint64_t*>(buffer_.data()),
+                           trace::BytesToWords(bytes_available));
+        if (!ReadRecords(chunk)) {
+            ReportError("Trace stream is corrupted");
+            break;
+        }
+        bytes_consumed =
+            bytes_available - trace::WordsToBytes(chunk.remaining_words());
+
+        bytes_available -= bytes_consumed;
+        memmove(buffer_.data(), buffer_.data() + bytes_consumed, bytes_available);
+        buffer_end_ = bytes_available;
+    }
+}
+
+} // namespace trace
diff --git a/zircon/system/ulib/trace-reader/file_reader_tests.cpp b/zircon/system/ulib/trace-reader/file_reader_tests.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ad0f87310eeb487ae0f6ca58e4b3d09541ffd042
--- /dev/null
+++ b/zircon/system/ulib/trace-reader/file_reader_tests.cpp
@@ -0,0 +1,67 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <trace-reader/file_reader.h>
+
+#include <memory>
+#include <stdint.h>
+#include <stdio.h>
+#include <utility>
+
+#include <fbl/algorithm.h>
+#include <fbl/vector.h>
+#include <trace-engine/fields.h>
+#include <trace-engine/types.h>
+#include <zxtest/zxtest.h>
+
+#include "reader_tests.h"
+
+namespace trace {
+namespace {
+
+const char kTestInputFile[] = "/tmp/trace-reader-test.fxt";
+
+TEST(TraceFileReader, Records) {
+    FILE* f = fopen(kTestInputFile, "wb");
+    ASSERT_NOT_NULL(f);
+
+    constexpr zx_koid_t kProcessKoid = 42;
+    constexpr zx_koid_t kThreadKoid = 43;
+    constexpr trace_thread_index_t kThreadIndex = 99;
+
+    uint64_t thread_record[3]{};
+    ThreadRecordFields::Type::Set(thread_record[0],
+                                  static_cast<uint64_t>(RecordType::kThread));
+    ThreadRecordFields::RecordSize::Set(thread_record[0], fbl::count_of(thread_record));
+    ThreadRecordFields::ThreadIndex::Set(thread_record[0], kThreadIndex);
+    thread_record[1] = kProcessKoid;
+    thread_record[2] = kThreadKoid;
+
+    ASSERT_EQ(fwrite(&thread_record[0], sizeof(thread_record[0]),
+                     fbl::count_of(thread_record), f),
+              fbl::count_of(thread_record));
+    ASSERT_EQ(fclose(f), 0);
+
+    std::unique_ptr<trace::FileReader> reader;
+    fbl::Vector<trace::Record> records;
+    fbl::String error;
+    ASSERT_TRUE(trace::FileReader::Create(
+        kTestInputFile, test::MakeRecordConsumer(&records),
+        test::MakeErrorHandler(&error), &reader));
+
+    reader->ReadFile();
+    EXPECT_TRUE(error.empty());
+    ASSERT_EQ(records.size(), 1u);
+    const trace::Record& rec = records[0];
+    EXPECT_EQ(rec.type(), RecordType::kThread);
+    const trace::Record::Thread& thread = rec.GetThread();
+    EXPECT_EQ(thread.index, kThreadIndex);
+    EXPECT_EQ(thread.process_thread.process_koid(), kProcessKoid);
+    EXPECT_EQ(thread.process_thread.thread_koid(), kThreadKoid);
+}
+
+// NOTE: Most of the reader is covered by the libtrace tests.
+
+} // namespace
+} // namespace trace
diff --git a/zircon/system/ulib/trace-reader/include/trace-reader/file_reader.h b/zircon/system/ulib/trace-reader/include/trace-reader/file_reader.h
new file mode 100644
index 0000000000000000000000000000000000000000..a696366634eab1c4b28f569195583334696ff815
--- /dev/null
+++ b/zircon/system/ulib/trace-reader/include/trace-reader/file_reader.h
@@ -0,0 +1,51 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef TRACE_READER_FILE_READER_H_
+#define TRACE_READER_FILE_READER_H_
+
+#include <array>
+#include <memory>
+#include <stdio.h>
+
+#include <trace-engine/fields.h>
+#include <trace-reader/reader.h>
+
+namespace trace {
+
+// Read records from a file, in fxt file format.
+
+class FileReader : public TraceReader {
+public:
+    static bool Create(const char* file_path,
+                       RecordConsumer record_consumer,
+                       ErrorHandler error_handler,
+                       std::unique_ptr<FileReader>* out_reader);
+
+    void ReadFile();
+
+private:
+    // Note: Buffer needs to be big enough to store records of maximum size.
+    static constexpr size_t kReadBufferSize =
+        trace::RecordFields::kMaxRecordSizeBytes * 4;
+
+    explicit FileReader(FILE* file, RecordConsumer record_consumer,
+                        ErrorHandler error_handler);
+
+    FILE* const file_;
+    RecordConsumer const record_consumer_;
+    ErrorHandler const error_handler_;
+
+    // We don't use a vector here to avoid the housekeeping necessary to keep
+    // checkers happy (e.g., asan). We use this buffer in an atypical way.
+    std::array<uint8_t, kReadBufferSize> buffer_;
+    // The amount of space in use in |buffer_|.
+    size_t buffer_end_ = 0u;
+
+    DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(FileReader);
+};
+
+} // namespace trace
+
+#endif  // TRACE_READER_FILE_READER_H_
diff --git a/zircon/system/ulib/trace-reader/include/trace-reader/reader.h b/zircon/system/ulib/trace-reader/include/trace-reader/reader.h
index 9143689304b1f769afdb7d50a9f2dd1c2b8ded7d..b33c3b06fcf241db8337aa744764ba1c9672c86d 100644
--- a/zircon/system/ulib/trace-reader/include/trace-reader/reader.h
+++ b/zircon/system/ulib/trace-reader/include/trace-reader/reader.h
@@ -70,6 +70,9 @@ public:
 
     const ErrorHandler& error_handler() const { return error_handler_; }
 
+protected:
+    void ReportError(fbl::String error) const;
+
 private:
     bool ReadMetadataRecord(Chunk& record,
                             RecordHeader header);
@@ -102,8 +105,6 @@ private:
                          trace_encoded_thread_ref_t thread_ref,
                          ProcessThread* out_process_thread) const;
 
-    void ReportError(fbl::String error) const;
-
     RecordConsumer const record_consumer_;
     ErrorHandler const error_handler_;
 
diff --git a/zircon/system/ulib/trace-reader/reader_tests.cpp b/zircon/system/ulib/trace-reader/reader_tests.cpp
index f9af88b3728498e401c4297f015e0f5b2ffc4fd1..fa9bd8663f5728d9701b5d6d2bb982de5c00f297 100644
--- a/zircon/system/ulib/trace-reader/reader_tests.cpp
+++ b/zircon/system/ulib/trace-reader/reader_tests.cpp
@@ -12,27 +12,11 @@
 
 #include <utility>
 
+#include "reader_tests.h"
+
 namespace trace {
 namespace {
 
-template <typename T>
-uint64_t ToWord(const T& value) {
-    return *reinterpret_cast<const uint64_t*>(&value);
-}
-
-trace::TraceReader::RecordConsumer MakeRecordConsumer(
-    fbl::Vector<trace::Record>* out_records) {
-    return [out_records](trace::Record record) {
-        out_records->push_back(std::move(record));
-    };
-}
-
-trace::TraceReader::ErrorHandler MakeErrorHandler(fbl::String* out_error) {
-    return [out_error](fbl::String error) {
-        *out_error = std::move(error);
-    };
-}
-
 TEST(TraceReader, EmptyChunk) {
     uint64_t value;
     int64_t int64_value;
@@ -70,11 +54,11 @@ TEST(TraceReader, NonEmptyChunk) {
         0,
         UINT64_MAX,
         // int64 values
-        ToWord(INT64_MIN),
-        ToWord(INT64_MAX),
+        test::ToWord(INT64_MIN),
+        test::ToWord(INT64_MAX),
         // double values
-        ToWord(1.5),
-        ToWord(-3.14),
+        test::ToWord(1.5),
+        test::ToWord(-3.14),
         // string values (will be filled in)
         0,
         0,
@@ -145,7 +129,8 @@ TEST(TraceReader, NonEmptyChunk) {
 TEST(TraceReader, InitialState) {
     fbl::Vector<trace::Record> records;
     fbl::String error;
-    trace::TraceReader reader(MakeRecordConsumer(&records), MakeErrorHandler(&error));
+    trace::TraceReader reader(
+        test::MakeRecordConsumer(&records), test::MakeErrorHandler(&error));
 
     EXPECT_EQ(0, reader.current_provider_id());
     EXPECT_TRUE(reader.current_provider_name() == "");
@@ -157,7 +142,8 @@ TEST(TraceReader, InitialState) {
 TEST(TraceReader, EmptyBuffer) {
     fbl::Vector<trace::Record> records;
     fbl::String error;
-    trace::TraceReader reader(MakeRecordConsumer(&records), MakeErrorHandler(&error));
+    trace::TraceReader reader(
+        test::MakeRecordConsumer(&records), test::MakeErrorHandler(&error));
 
     trace::Chunk empty;
     EXPECT_TRUE(reader.ReadRecords(empty));
diff --git a/zircon/system/ulib/trace-reader/reader_tests.h b/zircon/system/ulib/trace-reader/reader_tests.h
new file mode 100644
index 0000000000000000000000000000000000000000..b21ce8a9e4ac71e41cd42972b4dd7de00f75c0bc
--- /dev/null
+++ b/zircon/system/ulib/trace-reader/reader_tests.h
@@ -0,0 +1,38 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <trace-reader/reader.h>
+
+#include <stdint.h>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <zxtest/zxtest.h>
+
+namespace trace {
+namespace test {
+
+template <typename T>
+uint64_t ToWord(const T& value) {
+    return *reinterpret_cast<const uint64_t*>(&value);
+}
+
+static inline trace::TraceReader::RecordConsumer MakeRecordConsumer(
+    fbl::Vector<trace::Record>* out_records) {
+    return [out_records](trace::Record record) {
+        out_records->push_back(std::move(record));
+    };
+}
+
+static inline trace::TraceReader::ErrorHandler MakeErrorHandler(fbl::String* out_error) {
+    return [out_error](fbl::String error) {
+        *out_error = std::move(error);
+    };
+}
+
+} // namespace test
+} // namespace trace