Replace IPC with Mojo Interface for MHTML Serialization

This change replaces the current usage of legacy IPC within the
context of MHTML Generation with Mojo to both update the interface as
well as prepare for the upcoming streaming of serialized MHTML data
back to the browser.

Changes made:
    1.) A new Mojo interface is introduced in both the Browser and
        Renderer processes, which uses a message pipe in lieu of IPC.
    2.) The MHTMLGenerationManager will now create self-owning Jobs
        that delete themselves. It no longer needs to keep track of
        created Jobs via job_id_. Many of the manager's functions are
        moved into the Job implementation.
    3.) In absence of the std::set, an std::vector will be used within
        the generation delegate instead, performing DCHECKs to ensure
        it is sorted and unique.
    4.) The file handle passed to the Renderer is no longer duplicated
        via IPC::GetPlatformFileForTransit, but uses a similar
        function File::Duplicate.

Bug: 915966
Change-Id: Id402c8540ee829b25a5a5839961cbbd5aad696cb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1403518
Commit-Queue: Mark Lieu <[email protected]>
Reviewed-by: Daniel Cheng <[email protected]>
Reviewed-by: Charlie Reis <[email protected]>
Reviewed-by: Carlos Knippschild <[email protected]>
Reviewed-by: Ken Buchanan <[email protected]>
Reviewed-by: Min Qin <[email protected]>
Reviewed-by: Dan H <[email protected]>
Reviewed-by: Ɓukasz Anforowicz <[email protected]>
Cr-Commit-Position: refs/heads/master@{#639297}
diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index 3642892..1771e0a 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -128,7 +128,7 @@
   OBSOLETE_WC_CONTENT_WITH_CERT_ERRORS_BAD_SECURITY_INFO = 101,
   RFMF_RENDERER_FAKED_ITS_OWN_DEATH = 102,
   DWNLD_INVALID_SAVABLE_RESOURCE_LINKS_RESPONSE = 103,
-  DWNLD_INVALID_SERIALIZE_AS_MHTML_RESPONSE = 104,
+  OBSOLETE_DWNLD_INVALID_SERIALIZE_AS_MHTML_RESPONSE = 104,
   BDH_DEVICE_NOT_ALLOWED_FOR_ORIGIN = 105,
   OBSOLETE_ACI_WRONG_STORAGE_PARTITION = 106,
   OBSOLETE_RDHI_WRONG_STORAGE_PARTITION = 107,
diff --git a/content/browser/download/mhtml_generation_browsertest.cc b/content/browser/download/mhtml_generation_browsertest.cc
index c0b1e4cf..c62e936 100644
--- a/content/browser/download/mhtml_generation_browsertest.cc
+++ b/content/browser/download/mhtml_generation_browsertest.cc
@@ -18,7 +18,7 @@
 #include "base/threading/thread_restrictions.h"
 #include "components/download/public/common/download_task_runner.h"
 #include "content/browser/renderer_host/render_process_host_impl.h"
-#include "content/common/frame_messages.h"
+#include "content/common/mhtml_file_writer.mojom.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/mhtml_extra_parts.h"
@@ -36,6 +36,7 @@
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
 #include "third_party/blink/public/mojom/frame/find_in_page.mojom.h"
 
 using testing::ContainsRegex;
@@ -97,9 +98,111 @@
 
 }  // namespace
 
+// This Mock injects our overwritten interface, running the callback
+// SerializeAsMHTMLResponse and immediately disconnecting the message pipe.
+class MockMhtmlFileWriter : public mojom::MhtmlFileWriter {
+ public:
+  explicit MockMhtmlFileWriter() : binding_(this) {}
+
+  ~MockMhtmlFileWriter() override {}
+
+  void BindRequest(mojo::ScopedInterfaceEndpointHandle handle) {
+    binding_.Bind(mojom::MhtmlFileWriterAssociatedRequest(std::move(handle)));
+  }
+
+  void WriteDataToDestinationFile(base::File& destination_file) {
+    const char kTestData[] =
+        "Sample Text to write on generated MHTML "
+        "file to verify it has been written to.";
+    base::ScopedAllowBlockingForTesting allow_blocking;
+    destination_file.WriteAtCurrentPos(kTestData, strlen(kTestData));
+    destination_file.Close();
+  }
+
+  void SerializeAsMHTML(mojom::SerializeAsMHTMLParamsPtr params,
+                        SerializeAsMHTMLCallback callback) override {
+    // Upon using the overridden mock interface implementation, this will be
+    // handled by the product code as illustrated below.  (1), (2), (3) depict
+    // points in time when product code runs on UI thread and download sequence.
+    // For the repro, the message pipe disconnection needs to happen between (1)
+    // and (3).
+    //
+    //   Test instance     UI thread         download sequence
+    //     ---------       ---------           -----------
+    //        |                |                     |
+    //    WE ARE HERE          |                     |
+    //        |                |                     |
+    //        |                |                     |
+    //        +--------------->+                     |
+    //        |                |                     |
+    //        |                |                     |
+    //        |                |                     |
+    //        |                |                     |
+    //        |                |                     |
+    //        |                |                     |
+    // (1)    |      MHTMLGenerationManager::Job     |
+    //        |      ::SerializeAsMHTMLResponse      |
+    //        |                +-------------------->+
+    //        |                |                     |
+    //        |                |                     |
+    //        |                |                     |
+    // (2)    |                |          MHTMLGenerationManager::Job
+    //        |                |          ::CloseFileOnFileThread
+    //        |                |                     |
+    //        |                |                     |
+    //        |           test needs to              |
+    //        |       disconnect message pipe        |
+    //        |      HERE - between (1) and (3)      |
+    //        |                |                     |
+    //        |                |                     |
+    //        |                +<--------------------+
+    //        |                |                     |
+    // (3)    |      MHTMLGenerationManager          |
+    //        |      Job::OnFinished                 |
+    //        |                |                     |
+    //
+    // We hope that the error handler is invoked between (1) and (3) by doing
+    // the following:
+    // - From here, run the callback response to the UI thread. This queues
+    //   the response message onto the bound message pipe.
+    // - After running the callback response, immediately unbind the message
+    //   pipe in order to queue a message onto the bound message pipe to notify
+    //   the Browser the connection was closed and invoke the error handler.
+    // - Upon resuming operation, the FIFO ordering property of associated
+    //   interfaces guarantees the execution of (1) before the error handler.
+    //   (1) posts (2) to the download sequence and terminates. The client end
+    //   then accepts the error notification and invokes the connection error
+    //   handler, guaranteeing its execution before (3).
+
+    // Write a valid MHTML file to destination_file, since we are not
+    // actively running a serialization pipeline in the mock implementation.
+    WriteDataToDestinationFile(params->destination_file);
+
+    std::vector<std::string> dummy_digests;
+    base::TimeDelta dummy_time_delta = base::TimeDelta::Max();
+    std::move(callback).Run(mojom::MhtmlSaveStatus::SUCCESS, dummy_digests,
+                            dummy_time_delta);
+
+    // Close the message pipe connection to invoke the connection error
+    // callback. The connection error handler from here will finalize
+    // the Job and attempt to call MHTMLGenerationManager::Job::CloseFile
+    // a second time. If this situation is handled correctly, the
+    // browser file should be invalidated and idempotent.
+    binding_.Unbind();
+  }
+
+ private:
+  mojo::AssociatedBinding<mojom::MhtmlFileWriter> binding_;
+
+  DISALLOW_COPY_AND_ASSIGN(MockMhtmlFileWriter);
+};
+
 class MHTMLGenerationTest : public ContentBrowserTest {
  public:
-  MHTMLGenerationTest() : has_mhtml_callback_run_(false), file_size_(0) {}
+  MHTMLGenerationTest()
+      : has_mhtml_callback_run_(false),
+        file_size_(0),
+        well_formedness_check_(true) {}
 
  protected:
   void SetUpOnMainThread() override {
@@ -131,8 +234,9 @@
     ASSERT_TRUE(has_mhtml_callback_run())
         << "Unexpected error generating MHTML file";
 
-    // Skip well formedness check if there was an generation error.
-    if (file_size() == -1)
+    // Skip well formedness check if explicitly disabled or there was a
+    // generation error.
+    if (!well_formedness_check_ || file_size() == -1)
       return;
 
     // Loads the generated file to check if it is well formed.
@@ -218,6 +322,13 @@
     }
   }
 
+  // In the case that we are using a pre-generated .mhtml file, we do
+  // not have any control over the final mhtml_boundary_marker write
+  // operation. This results in the post-generation verification tests
+  // reporting a malformed multipart archive, unintentionally failing the
+  // test.
+  void DisableWellformednessCheck() { well_formedness_check_ = false; }
+
   bool has_mhtml_callback_run() const { return has_mhtml_callback_run_; }
   int64_t file_size() const { return file_size_; }
   base::HistogramTester* histogram_tester() { return histogram_tester_.get(); }
@@ -233,6 +344,7 @@
 
   bool has_mhtml_callback_run_;
   int64_t file_size_;
+  bool well_formedness_check_;
   std::unique_ptr<base::HistogramTester> histogram_tester_;
 };
 
@@ -264,120 +376,28 @@
       static_cast<int>(MhtmlSaveStatus::SUCCESS), 1);
 }
 
-class GenerateMHTMLAndExitRendererMessageFilter : public BrowserMessageFilter {
- public:
-  GenerateMHTMLAndExitRendererMessageFilter(
-      RenderProcessHostImpl* render_process_host)
-      : BrowserMessageFilter(FrameMsgStart),
-        render_process_host_(render_process_host) {}
-
- protected:
-  ~GenerateMHTMLAndExitRendererMessageFilter() override {}
-
- private:
-  bool OnMessageReceived(const IPC::Message& message) override {
-    if (message.type() == FrameHostMsg_SerializeAsMHTMLResponse::ID) {
-      // After |return false| below, this IPC message will be handled by the
-      // product code as illustrated below.  (1), (2), (3) depict points in time
-      // when product code runs on UI thread and download sequence.  (X), (Y),
-      // (Z) depict when we want test-injected tasks to run - for the repro, (Z)
-      // has to happen between (1) and (3).  (Y?) and (Z?) depict when test
-      // tasks can theoretically happen and ruin the repro.
-      //
-      //     IO thread       UI thread         download sequence
-      //     ---------       ---------           -----------
-      //        |                |                     |
-      //    WE ARE HERE          |                     |
-      //        |                |                     |
-      // after |return false|    |                     |
-      //        +--------------->+                     |
-      //        |                |                     |
-      //        |               (X)                    |
-      //        |                |                     |
-      //        |                |                    (Y?)
-      //        |               (Z?)                   |
-      //        |                |                     |
-      // (1)    |      MHTMLGenerationManager          |
-      //        |      ::OnSerializeAsMHTMLResponse    |
-      //        |                +-------------------->+
-      //        |                |                     |
-      //        |                |                    (Y)
-      //        |                |                     |
-      // (2)    |                |          MHTMLGenerationManager::Job
-      //        |                |          ::CloseFileOnFileThread
-      //        |                |                     |
-      //        |               (Z)                    |
-      //        |         test needs to inject         |
-      //        |        fast renderer shutdown        |
-      //        |      HERE - between (1) and (3)      |
-      //        |                |                     |
-      //        |                |                     |
-      //        |                +<--------------------+
-      //        |                |                     |
-      // (3)    |      MHTMLGenerationManager          |
-      //        |      ::OnFileClosed                  |
-      //        |                |                     |
-      //
-      // We hope that (Z) happens between (1) and (3) by doing the following:
-      // - From here post TaskX to UI thread.  (X) is guaranteed to happen
-      //   before timepoint (1) (because posting of (1) happens after
-      //   |return false| / before we post TaskX below).
-      // - From (X) post TaskY to download sequence.  Because this posting is
-      //   done before (1), we can guarantee that (Y) will happen before (2).
-      // - From (Y) post TaskZ to UI thread.  Because this posting is done
-      //   before (2), we can guarantee that (Z) will happen before (3).
-      // - We cannot really guarantee that (Y) and (Z) happen *after* (1) - i.e.
-      //   execution at (Y?) and (Z?) instead is possible.  In practice,
-      //   bouncing off of UI and download sequence does mean (Z) happens
-      //   after (1).
-      base::PostTaskWithTraits(
-          FROM_HERE, {BrowserThread::UI},
-          base::BindOnce(&GenerateMHTMLAndExitRendererMessageFilter::TaskX,
-                         base::Unretained(this)));
-    }
-
-    return false;
-  }
-
-  void TaskX() {
-    download::GetDownloadTaskRunner()->PostTask(
-        FROM_HERE,
-        base::BindOnce(&GenerateMHTMLAndExitRendererMessageFilter::TaskY,
-                       base::Unretained(this)));
-  }
-
-  void TaskY() {
-    base::PostTaskWithTraits(
-        FROM_HERE, {BrowserThread::UI},
-        base::BindOnce(&GenerateMHTMLAndExitRendererMessageFilter::TaskZ,
-                       base::Unretained(this)));
-  }
-
-  void TaskZ() {
-    render_process_host_->FastShutdownIfPossible();
-  }
-
-  RenderProcessHostImpl* render_process_host_;
-
-  DISALLOW_COPY_AND_ASSIGN(GenerateMHTMLAndExitRendererMessageFilter);
-};
-
 // Regression test for the crash/race from https://crbug.com/612098.
-IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest, GenerateMHTMLAndExitRenderer) {
+IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest, GenerateMHTMLAndCloseConnection) {
+  MockMhtmlFileWriter mock_writer;
+
   NavigateToURL(shell(), embedded_test_server()->GetURL("/simple_page.html"));
-
-  RenderProcessHostImpl* render_process_host =
-      static_cast<RenderProcessHostImpl*>(
-          shell()->web_contents()->GetMainFrame()->GetProcess());
-  scoped_refptr<BrowserMessageFilter> filter =
-      new GenerateMHTMLAndExitRendererMessageFilter(render_process_host);
-  render_process_host->AddFilter(filter.get());
-
   base::FilePath path(temp_dir_.GetPath());
   path = path.Append(FILE_PATH_LITERAL("test.mht"));
+
+  blink::AssociatedInterfaceProvider* remote_interfaces =
+      shell()->web_contents()->GetMainFrame()->GetRemoteAssociatedInterfaces();
+  remote_interfaces->OverrideBinderForTesting(
+      mojom::MhtmlFileWriter::Name_,
+      base::BindRepeating(&MockMhtmlFileWriter::BindRequest,
+                          base::Unretained(&mock_writer)));
+
+  DisableWellformednessCheck();
   GenerateMHTMLForCurrentPage(MHTMLGenerationParams(path));
 
-  EXPECT_GT(ReadFileSizeFromDisk(path), 100);  // Verify the actual file size.
+  // Verify the file has some contents written to it.
+  EXPECT_GT(ReadFileSizeFromDisk(path), 100);
+  // Verify the reported file size matches the file written to disk.
+  EXPECT_EQ(ReadFileSizeFromDisk(path), file_size());
 }
 
 // TODO(crbug.com/672313): Flaky on Windows.
diff --git a/content/browser/download/mhtml_generation_manager.cc b/content/browser/download/mhtml_generation_manager.cc
index 61d87ed..d87df05 100644
--- a/content/browser/download/mhtml_generation_manager.cc
+++ b/content/browser/download/mhtml_generation_manager.cc
@@ -4,7 +4,6 @@
 
 #include "content/browser/download/mhtml_generation_manager.h"
 
-#include <map>
 #include <utility>
 
 #include "base/bind.h"
@@ -26,88 +25,61 @@
 #include "content/browser/download/mhtml_extra_parts_impl.h"
 #include "content/browser/frame_host/frame_tree_node.h"
 #include "content/browser/frame_host/render_frame_host_impl.h"
-#include "content/common/frame_messages.h"
+#include "content/common/mhtml_file_writer.mojom.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/mhtml_extra_parts.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_process_host.h"
-#include "content/public/browser/render_process_host_observer.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/mhtml_generation_params.h"
+#include "mojo/core/embedder/embedder.h"
 #include "net/base/mime_util.h"
+#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
 
 namespace {
 const char kContentLocation[] = "Content-Location: ";
 const char kContentType[] = "Content-Type: ";
 int kInvalidFileSize = -1;
+struct CloseFileResult {
+  CloseFileResult(content::MhtmlSaveStatus status, int64_t size)
+      : save_status(status), file_size(size) {}
+  content::MhtmlSaveStatus save_status;
+  int64_t file_size;
+};
 }  // namespace
 
 namespace content {
 
 // The class and all of its members live on the UI thread.  Only static methods
 // are executed on other threads.
-class MHTMLGenerationManager::Job : public RenderProcessHostObserver {
+// Job instances are created in MHTMLGenerationManager::Job::StartNewJob(),
+// proceeding with the MHTML saving process unmanaged. Every instance is
+// self-owned and responsible for deleting itself upon invoking OnFinished.
+// With self-ownership lifetime concerns, we make the following precautions:
+// - SerializeAsMHTMLResponse() always proceeds with finalizing upon detecting
+//   Job completion/cancellation.
+// - Jobs are prematurely finalized and deleted upon detecting a connection
+//   error with the message pipe during serialization.
+// - Any pending callbacks after deletion are invalidated using weak pointers.
+class MHTMLGenerationManager::Job {
  public:
-  Job(int job_id,
-      WebContents* web_contents,
-      const MHTMLGenerationParams& params,
-      GenerateMHTMLCallback callback);
-  ~Job() override;
-
-  int id() const { return job_id_; }
-  void set_browser_file(base::File file) { browser_file_ = std::move(file); }
-  base::TimeTicks creation_time() const { return creation_time_; }
-
-  GenerateMHTMLCallback callback() { return std::move(callback_); }
-
-  // Indicates whether we expect a message from the |sender| at this time.
-  // We expect only one message per frame - therefore calling this method
-  // will always clear |frame_tree_node_id_of_busy_frame_|.
-  bool IsMessageFromFrameExpected(RenderFrameHostImpl* sender);
-
-  // Handler for FrameHostMsg_SerializeAsMHTMLResponse (a notification from the
-  // renderer that the MHTML generation for previous frame has finished).
-  // Returns MhtmlSaveStatus::SUCCESS or a specific error status.
-  MhtmlSaveStatus OnSerializeAsMHTMLResponse(
-      const std::set<std::string>& digests_of_uris_of_serialized_resources);
-
-  // Sends IPC to the renderer, asking for MHTML generation of the next frame.
-  // Returns MhtmlSaveStatus::SUCCESS or a specific error status.
-  MhtmlSaveStatus SendToNextRenderFrame();
-
-  // Indicates if more calls to SendToNextRenderFrame are needed.
-  bool IsDone() const {
-    bool waiting_for_response_from_renderer =
-        frame_tree_node_id_of_busy_frame_ !=
-        FrameTreeNode::kFrameTreeNodeInvalidId;
-    bool no_more_requests_to_send = pending_frame_tree_node_ids_.empty();
-    return !waiting_for_response_from_renderer && no_more_requests_to_send;
-  }
-
-  // Write the MHTML footer and close the file on the file thread and respond
-  // back on the UI thread with the updated status and file size (which will be
-  // negative in case of errors).
-  void CloseFile(
-      base::OnceCallback<void(const std::tuple<MhtmlSaveStatus, int64_t>&)>
-          callback,
-      MhtmlSaveStatus save_status);
-
-  // RenderProcessHostObserver:
-  void RenderProcessExited(RenderProcessHost* host,
-                           const ChildProcessTerminationInfo& info) override;
-  void RenderProcessHostDestroyed(RenderProcessHost* host) override;
-
-  void MarkAsFinished();
-
-  void ReportRendererMainThreadTime(base::TimeDelta renderer_main_thread_time);
+  // Creates and registers a new job.
+  static void StartNewJob(WebContents* web_contents,
+                          const MHTMLGenerationParams& params,
+                          GenerateMHTMLCallback callback);
 
  private:
+  Job(WebContents* web_contents,
+      const MHTMLGenerationParams& params,
+      GenerateMHTMLCallback callback);
+  ~Job();
+
   // Writes the MHTML footer to the file and closes it.
   //
   // Note: The same |boundary| marker must be used for all "boundaries" -- in
   // the header, parts and footer -- that belong to the same MHTML document (see
   // also rfc1341, section 7.2.1, "boundary" description).
-  static std::tuple<MhtmlSaveStatus, int64_t> FinalizeAndCloseFileOnFileThread(
+  static CloseFileResult FinalizeAndCloseFileOnFileThread(
       MhtmlSaveStatus save_status,
       const std::string& boundary,
       base::File file,
@@ -125,14 +97,62 @@
   // Writes the footer into the MHTML file.  Returns false for faiulre.
   static bool WriteFooter(const std::string& boundary, base::File& file);
 
+  // Called on the UI thread when the file that should hold the MHTML data has
+  // been created.
+  void OnFileAvailable(base::File browser_file);
+
+  // Called on the UI thread after the file got finalized and we have its size,
+  // or an error occurred while creating a new file.
+  void OnFinished(const CloseFileResult& close_file_result);
+
+  // Called when the message pipe to the renderer is disconnected.
+  void OnConnectionError();
+
+  // Handler for the Mojo interface callback (a notification from the
+  // renderer that the MHTML generation for previous frame has finished).
+  void SerializeAsMHTMLResponse(
+      mojom::MhtmlSaveStatus mojo_save_status,
+      const std::vector<std::string>& digests_of_uris_of_serialized_resources,
+      base::TimeDelta renderer_main_thread_time);
+
+  // Records newly serialized resource digests into
+  // |digests_of_already_serialized_uris_|, and continues sending serialization
+  // requests to the next frame if there are more frames to be serialized.
+  // Returns MhtmlSaveStatus::SUCCESS or a specific error status.
+  MhtmlSaveStatus RecordDigestsAndContinue(
+      const std::vector<std::string>& digests_of_uris_of_serialized_resources);
+
+  // Packs up the current status of the MHTML file saving into a Mojo
+  // struct to send to the renderer process.
+  mojom::SerializeAsMHTMLParamsPtr CreateMojoParams();
+
+  // Sends Mojo interface call to the renderer, asking for MHTML
+  // generation of the next frame. Returns MhtmlSaveStatus::SUCCESS or a
+  // specific error status.
+  MhtmlSaveStatus SendToNextRenderFrame();
+
+  // Indicates if more calls to SendToNextRenderFrame are needed.
+  // This check is necessary to prevent a race condition between the
+  // Renderer and Browser where the Job is deleted before the response
+  // is received.
+  bool IsDone() const;
+
+  // Called on the UI thread when a job has been finished.
+  void Finalize(MhtmlSaveStatus save_status);
+
+  // Write the MHTML footer and close the file on the file thread and respond
+  // back on the UI thread with the updated status and file size (which will be
+  // negative in case of errors).
+  void CloseFile(MhtmlSaveStatus save_status);
+
+  void MarkAsFinished();
+
+  void ReportRendererMainThreadTime(base::TimeDelta renderer_main_thread_time);
+
   // Close the MHTML file if it looks good, setting the size param.  Returns
   // false for failure.
   static bool CloseFileIfValid(base::File& file, int64_t* file_size);
 
-  // Id used to map renderer responses to jobs.
-  // See also MHTMLGenerationManager::id_to_job_ map.
-  const int job_id_;
-
   // Time tracking for performance metrics reporting.
   const base::TimeTicks creation_time_;
   base::TimeTicks wait_on_renderer_start_time_;
@@ -147,9 +167,9 @@
   // The IDs of frames that still need to be processed.
   base::queue<int> pending_frame_tree_node_ids_;
 
-  // Identifies a frame to which we've sent FrameMsg_SerializeAsMHTML but for
-  // which we didn't yet process FrameHostMsg_SerializeAsMHTMLResponse via
-  // OnSerializeAsMHTMLResponse.
+  // Identifies a frame to which we've sent through
+  // MhtmlFileWriter::SerializeAsMHTML but for which we didn't yet process
+  // the response via SerializeAsMHTMLResponse.
   int frame_tree_node_id_of_busy_frame_;
 
   // The handle to the file the MHTML is saved to for the browser process.
@@ -166,34 +186,39 @@
   GenerateMHTMLCallback callback_;
 
   // Whether the job is finished (set to true only for the short duration of
-  // time between MHTMLGenerationManager::JobFinished is called and the job is
-  // destroyed by MHTMLGenerationManager::OnFileClosed).
+  // time between MHTMLGenerationManager::Job::Finalize is called and the job is
+  // destroyed by MHTMLGenerationManager::Job::OnFinished).
   bool is_finished_;
 
   // Any extra data parts that should be emitted into the output MHTML.
   std::vector<MHTMLExtraDataPart> extra_data_parts_;
 
-  // RAII helper for registering this Job as a RenderProcessHost observer.
-  ScopedObserver<RenderProcessHost, MHTMLGenerationManager::Job>
-      observed_renderer_process_host_;
+  // MHTML File Writer pointer to keep the variable alive.
+  mojom::MhtmlFileWriterAssociatedPtr writer_;
+
+  base::WeakPtrFactory<Job> weak_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(Job);
 };
 
-MHTMLGenerationManager::Job::Job(int job_id,
-                                 WebContents* web_contents,
+MHTMLGenerationManager::Job::Job(WebContents* web_contents,
                                  const MHTMLGenerationParams& params,
                                  GenerateMHTMLCallback callback)
-    : job_id_(job_id),
-      creation_time_(base::TimeTicks::Now()),
+    : creation_time_(base::TimeTicks::Now()),
       params_(params),
       frame_tree_node_id_of_busy_frame_(FrameTreeNode::kFrameTreeNodeInvalidId),
       mhtml_boundary_marker_(net::GenerateMimeMultipartBoundary()),
       salt_(base::GenerateGUID()),
       callback_(std::move(callback)),
       is_finished_(false),
-      observed_renderer_process_host_(this) {
+      weak_factory_(this) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+  TRACE_EVENT_NESTABLE_ASYNC_BEGIN2(
+      "page-serialization", "SavingMhtmlJob", this, "url",
+      web_contents->GetLastCommittedURL().possibly_invalid_spec(), "file",
+      params.file_path.AsUTF8Unsafe());
+
   web_contents->ForEachFrame(base::BindRepeating(
       &MHTMLGenerationManager::Job::AddFrame,
       base::Unretained(this)));  // Safe because ForEachFrame() is synchronous.
@@ -208,23 +233,42 @@
       MHTMLExtraParts::FromWebContents(web_contents));
   if (extra_parts)
     extra_data_parts_ = extra_parts->parts();
+
+  base::PostTaskAndReplyWithResult(
+      download::GetDownloadTaskRunner().get(), FROM_HERE,
+      base::BindOnce(&CreateFile, params.file_path),
+      base::BindOnce(&Job::OnFileAvailable, weak_factory_.GetWeakPtr()));
 }
 
 MHTMLGenerationManager::Job::~Job() {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
 }
 
+mojom::SerializeAsMHTMLParamsPtr
+MHTMLGenerationManager::Job::CreateMojoParams() {
+  mojom::SerializeAsMHTMLParamsPtr mojo_params =
+      mojom::SerializeAsMHTMLParams::New();
+  mojo_params->mhtml_boundary_marker = mhtml_boundary_marker_;
+  mojo_params->mhtml_binary_encoding = params_.use_binary_encoding;
+  mojo_params->mhtml_popup_overlay_removal = params_.remove_popup_overlay;
+  mojo_params->mhtml_problem_detection = params_.use_page_problem_detectors;
+
+  // File::Duplicate() creates a reference to this file for use in the Renderer.
+  mojo_params->destination_file = browser_file_.Duplicate();
+
+  // Tell the renderer to skip (= deduplicate) already covered MHTML parts.
+  mojo_params->salt = salt_;
+  mojo_params->digests_of_uris_to_skip.assign(
+      digests_of_already_serialized_uris_.begin(),
+      digests_of_already_serialized_uris_.end());
+
+  return mojo_params;
+}
+
 MhtmlSaveStatus MHTMLGenerationManager::Job::SendToNextRenderFrame() {
   DCHECK(browser_file_.IsValid());
   DCHECK(!pending_frame_tree_node_ids_.empty());
 
-  FrameMsg_SerializeAsMHTML_Params ipc_params;
-  ipc_params.job_id = job_id_;
-  ipc_params.mhtml_boundary_marker = mhtml_boundary_marker_;
-  ipc_params.mhtml_binary_encoding = params_.use_binary_encoding;
-  ipc_params.mhtml_popup_overlay_removal = params_.remove_popup_overlay;
-  ipc_params.mhtml_problem_detection = params_.use_page_problem_detectors;
-
   int frame_tree_node_id = pending_frame_tree_node_ids_.front();
   pending_frame_tree_node_ids_.pop();
 
@@ -233,22 +277,22 @@
     return MhtmlSaveStatus::FRAME_NO_LONGER_EXISTS;
   RenderFrameHost* rfh = ftn->current_frame_host();
 
-  // Get notified if the target of the IPC message dies between responding.
-  observed_renderer_process_host_.RemoveAll();
-  observed_renderer_process_host_.Add(rfh->GetProcess());
+  // Bind Mojo interface to the RenderFrame
+  rfh->GetRemoteAssociatedInterfaces()->GetInterface(&writer_);
+  auto callback = base::BindOnce(&Job::SerializeAsMHTMLResponse,
+                                 weak_factory_.GetWeakPtr());
 
-  // Tell the renderer to skip (= deduplicate) already covered MHTML parts.
-  ipc_params.salt = salt_;
-  ipc_params.digests_of_uris_to_skip = digests_of_already_serialized_uris_;
+  // Safe, as |writer_| is owned by this Job instance.
+  auto error_callback =
+      base::BindOnce(&Job::OnConnectionError, base::Unretained(this));
+  writer_.set_connection_error_handler(std::move(error_callback));
 
-  ipc_params.destination_file = IPC::GetPlatformFileForTransit(
-      browser_file_.GetPlatformFile(), false);  // |close_source_handle|.
-
-  // Send the IPC asking the renderer to serialize the frame.
+  // Send a Mojo request asking to serialize the frame.
   DCHECK_EQ(FrameTreeNode::kFrameTreeNodeInvalidId,
             frame_tree_node_id_of_busy_frame_);
   frame_tree_node_id_of_busy_frame_ = frame_tree_node_id;
-  rfh->Send(new FrameMsg_SerializeAsMHTML(rfh->GetRoutingID(), ipc_params));
+  writer_->SerializeAsMHTML(CreateMojoParams(), std::move(callback));
+
   TRACE_EVENT_NESTABLE_ASYNC_BEGIN1("page-serialization", "WaitingOnRenderer",
                                     this, "frame tree node id",
                                     frame_tree_node_id);
@@ -257,23 +301,57 @@
   return MhtmlSaveStatus::SUCCESS;
 }
 
-void MHTMLGenerationManager::Job::RenderProcessExited(
-    RenderProcessHost* host,
-    const ChildProcessTerminationInfo& info) {
+void MHTMLGenerationManager::Job::OnConnectionError() {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
-  MHTMLGenerationManager::GetInstance()->RenderProcessExited(this);
+  DLOG(ERROR) << "Message pipe to renderer closed while expecting response";
+  Finalize(MhtmlSaveStatus::RENDER_PROCESS_EXITED);
+}
+
+void MHTMLGenerationManager::Job::OnFileAvailable(base::File browser_file) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+  if (!browser_file.IsValid()) {
+    DLOG(ERROR) << "Failed to create file";
+    Finalize(MhtmlSaveStatus::FILE_CREATION_ERROR);
+    return;
+  }
+
+  browser_file_ = std::move(browser_file);
+
+  MhtmlSaveStatus save_status = SendToNextRenderFrame();
+  if (save_status != MhtmlSaveStatus::SUCCESS)
+    Finalize(save_status);
+}
+
+void MHTMLGenerationManager::Job::OnFinished(
+    const CloseFileResult& close_file_result) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  MhtmlSaveStatus save_status = close_file_result.save_status;
+  int64_t file_size = close_file_result.file_size;
+
+  TRACE_EVENT_NESTABLE_ASYNC_END2(
+      "page-serialization", "SavingMhtmlJob", this, "job save status",
+      GetMhtmlSaveStatusLabel(save_status), "file size", file_size);
+  UMA_HISTOGRAM_TIMES("PageSerialization.MhtmlGeneration.FullPageSavingTime",
+                      base::TimeTicks::Now() - creation_time_);
+  UMA_HISTOGRAM_ENUMERATION("PageSerialization.MhtmlGeneration.FinalSaveStatus",
+                            static_cast<int>(save_status),
+                            static_cast<int>(MhtmlSaveStatus::LAST));
+  std::move(callback_).Run(save_status == MhtmlSaveStatus::SUCCESS ? file_size
+                                                                   : -1);
+  delete this;  // This is the last time the Job is referenced.
 }
 
 void MHTMLGenerationManager::Job::MarkAsFinished() {
-  DCHECK(!is_finished_);
-  if (is_finished_)
+  // MarkAsFinished() may be called twice only in the case which
+  // writer_.reset() does not correctly stop OnConnectionError
+  // notifications for the case described in https://crbug.com/612098.
+  if (is_finished_) {
+    NOTREACHED();
     return;
-
+  }
   is_finished_ = true;
-
-  // Stopping RenderProcessExited notifications is needed to avoid calling
-  // JobFinished twice.  See also https://crbug.com/612098.
-  observed_renderer_process_host_.RemoveAll();
+  writer_.reset();
 
   TRACE_EVENT_NESTABLE_ASYNC_INSTANT0("page-serialization", "JobFinished",
                                       this);
@@ -321,16 +399,7 @@
   pending_frame_tree_node_ids_.push(frame_tree_node_id);
 }
 
-void MHTMLGenerationManager::Job::RenderProcessHostDestroyed(
-    RenderProcessHost* host) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-  observed_renderer_process_host_.Remove(host);
-}
-
-void MHTMLGenerationManager::Job::CloseFile(
-    base::OnceCallback<void(const std::tuple<MhtmlSaveStatus, int64_t>&)>
-        callback,
-    MhtmlSaveStatus save_status) {
+void MHTMLGenerationManager::Job::CloseFile(MhtmlSaveStatus save_status) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   DCHECK(!mhtml_boundary_marker_.empty());
 
@@ -338,7 +407,7 @@
     // Only update the status if that won't hide an earlier error.
     if (save_status == MhtmlSaveStatus::SUCCESS)
       save_status = MhtmlSaveStatus::FILE_WRITTING_ERROR;
-    std::move(callback).Run(std::make_tuple(save_status, -1));
+    OnFinished(CloseFileResult(save_status, -1));
     return;
   }
 
@@ -351,24 +420,44 @@
           (save_status == MhtmlSaveStatus::SUCCESS ? mhtml_boundary_marker_
                                                    : std::string()),
           std::move(browser_file_), std::move(extra_data_parts_)),
-      std::move(callback));
+      base::BindOnce(&Job::OnFinished, weak_factory_.GetWeakPtr()));
 }
 
-bool MHTMLGenerationManager::Job::IsMessageFromFrameExpected(
-    RenderFrameHostImpl* sender) {
-  int sender_id = sender->frame_tree_node()->frame_tree_node_id();
-  if (sender_id != frame_tree_node_id_of_busy_frame_)
-    return false;
+void MHTMLGenerationManager::Job::SerializeAsMHTMLResponse(
+    mojom::MhtmlSaveStatus mojo_save_status,
+    const std::vector<std::string>& digests_of_uris_of_serialized_resources,
+    base::TimeDelta renderer_main_thread_time) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
 
-  // We only expect one message per frame - let's make sure subsequent messages
-  // from the same |sender| will be rejected.
+  TRACE_EVENT_NESTABLE_ASYNC_END0("page-serialization", "WaitingOnRenderer",
+                                  this);
+  ReportRendererMainThreadTime(renderer_main_thread_time);
+
   frame_tree_node_id_of_busy_frame_ = FrameTreeNode::kFrameTreeNodeInvalidId;
 
-  return true;
+  // TODO(crbug.com/915966): Remove this statement once enums are dedupped.
+  MhtmlSaveStatus save_status = static_cast<MhtmlSaveStatus>(mojo_save_status);
+
+  // If the renderer succeeded, update the status.
+  if (save_status == MhtmlSaveStatus::SUCCESS) {
+    save_status =
+        RecordDigestsAndContinue(digests_of_uris_of_serialized_resources);
+  }
+
+  // If there was a failure (either from the renderer or from the job) then
+  // terminate the job and return.
+  if (save_status != MhtmlSaveStatus::SUCCESS) {
+    Finalize(save_status);
+    return;
+  }
+
+  // Otherwise report completion if the job is done.
+  if (IsDone())
+    Finalize(MhtmlSaveStatus::SUCCESS);
 }
 
-MhtmlSaveStatus MHTMLGenerationManager::Job::OnSerializeAsMHTMLResponse(
-    const std::set<std::string>& digests_of_uris_of_serialized_resources) {
+MhtmlSaveStatus MHTMLGenerationManager::Job::RecordDigestsAndContinue(
+    const std::vector<std::string>& digests_of_uris_of_serialized_resources) {
   DCHECK(!wait_on_renderer_start_time_.is_null());
   base::TimeDelta renderer_wait_time =
       base::TimeTicks::Now() - wait_on_renderer_start_time_;
@@ -382,7 +471,10 @@
   // Renderer should be deduping resources with the same uris.
   DCHECK_EQ(0u, base::STLSetIntersection<std::set<std::string>>(
                     digests_of_already_serialized_uris_,
-                    digests_of_uris_of_serialized_resources).size());
+                    std::set<std::string>(
+                        digests_of_uris_of_serialized_resources.begin(),
+                        digests_of_uris_of_serialized_resources.end()))
+                    .size());
   digests_of_already_serialized_uris_.insert(
       digests_of_uris_of_serialized_resources.begin(),
       digests_of_uris_of_serialized_resources.end());
@@ -394,9 +486,33 @@
   return SendToNextRenderFrame();
 }
 
+bool MHTMLGenerationManager::Job::IsDone() const {
+  bool waiting_for_response_from_renderer =
+      frame_tree_node_id_of_busy_frame_ !=
+      FrameTreeNode::kFrameTreeNodeInvalidId;
+  bool no_more_requests_to_send = pending_frame_tree_node_ids_.empty();
+  return !waiting_for_response_from_renderer && no_more_requests_to_send;
+}
+
+void MHTMLGenerationManager::Job::Finalize(MhtmlSaveStatus save_status) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  MarkAsFinished();
+  CloseFile(save_status);
+}
+
 // static
-std::tuple<MhtmlSaveStatus, int64_t>
-MHTMLGenerationManager::Job::FinalizeAndCloseFileOnFileThread(
+void MHTMLGenerationManager::Job::StartNewJob(
+    WebContents* web_contents,
+    const MHTMLGenerationParams& params,
+    GenerateMHTMLCallback callback) {
+  // Creates a new Job.
+  // The constructor starts the serialization process and it will delete
+  // itself upon finishing.
+  new Job(web_contents, params, std::move(callback));
+}
+
+// static
+CloseFileResult MHTMLGenerationManager::Job::FinalizeAndCloseFileOnFileThread(
     MhtmlSaveStatus save_status,
     const std::string& boundary,
     base::File file,
@@ -429,7 +545,7 @@
     save_status = MhtmlSaveStatus::FILE_CLOSING_ERROR;
   }
 
-  return std::make_tuple(save_status, file_size);
+  return CloseFileResult(save_status, file_size);
 }
 
 // static
@@ -493,66 +609,16 @@
   return base::Singleton<MHTMLGenerationManager>::get();
 }
 
-MHTMLGenerationManager::MHTMLGenerationManager() : next_job_id_(0) {}
+MHTMLGenerationManager::MHTMLGenerationManager() {}
 
-MHTMLGenerationManager::~MHTMLGenerationManager() {
-}
+MHTMLGenerationManager::~MHTMLGenerationManager() {}
 
 void MHTMLGenerationManager::SaveMHTML(WebContents* web_contents,
                                        const MHTMLGenerationParams& params,
                                        GenerateMHTMLCallback callback) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
 
-  Job* job = NewJob(web_contents, params, std::move(callback));
-  TRACE_EVENT_NESTABLE_ASYNC_BEGIN2(
-      "page-serialization", "SavingMhtmlJob", job, "url",
-      web_contents->GetLastCommittedURL().possibly_invalid_spec(),
-      "file", params.file_path.AsUTF8Unsafe());
-
-  base::PostTaskAndReplyWithResult(
-      download::GetDownloadTaskRunner().get(), FROM_HERE,
-      base::Bind(&MHTMLGenerationManager::CreateFile, params.file_path),
-      base::Bind(&MHTMLGenerationManager::OnFileAvailable,
-                 base::Unretained(this),  // Safe b/c |this| is a singleton.
-                 job->id()));
-}
-
-void MHTMLGenerationManager::OnSerializeAsMHTMLResponse(
-    RenderFrameHostImpl* sender,
-    int job_id,
-    MhtmlSaveStatus save_status,
-    const std::set<std::string>& digests_of_uris_of_serialized_resources,
-    base::TimeDelta renderer_main_thread_time) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
-  Job* job = FindJob(job_id);
-  if (!job || !job->IsMessageFromFrameExpected(sender)) {
-    NOTREACHED();
-    ReceivedBadMessage(sender->GetProcess(),
-                       bad_message::DWNLD_INVALID_SERIALIZE_AS_MHTML_RESPONSE);
-    return;
-  }
-
-  TRACE_EVENT_NESTABLE_ASYNC_END0("page-serialization", "WaitingOnRenderer",
-                                  job);
-  job->ReportRendererMainThreadTime(renderer_main_thread_time);
-
-  // If the renderer succeeded notify the Job and update the status.
-  if (save_status == MhtmlSaveStatus::SUCCESS) {
-    save_status = job->OnSerializeAsMHTMLResponse(
-        digests_of_uris_of_serialized_resources);
-  }
-
-  // If there was a failure (either from the renderer or from the job) then
-  // terminate the job and return.
-  if (save_status != MhtmlSaveStatus::SUCCESS) {
-    JobFinished(job, save_status);
-    return;
-  }
-
-  // Otherwise report completion if the job is done.
-  if (job->IsDone())
-    JobFinished(job, MhtmlSaveStatus::SUCCESS);
+  Job::StartNewJob(web_contents, params, std::move(callback));
 }
 
 // static
@@ -569,93 +635,10 @@
 
   base::File browser_file(file_path, file_flags);
   if (!browser_file.IsValid()) {
-    LOG(ERROR) << "Failed to create file to save MHTML at: "
-               << file_path.value();
+    DLOG(ERROR) << "Failed to create file to save MHTML at: "
+                << file_path.value();
   }
   return browser_file;
 }
 
-void MHTMLGenerationManager::OnFileAvailable(int job_id,
-                                             base::File browser_file) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
-  Job* job = FindJob(job_id);
-  DCHECK(job);
-
-  if (!browser_file.IsValid()) {
-    LOG(ERROR) << "Failed to create file";
-    JobFinished(job, MhtmlSaveStatus::FILE_CREATION_ERROR);
-    return;
-  }
-
-  job->set_browser_file(std::move(browser_file));
-
-  MhtmlSaveStatus save_status = job->SendToNextRenderFrame();
-  if (save_status != MhtmlSaveStatus::SUCCESS) {
-    JobFinished(job, save_status);
-  }
-}
-
-void MHTMLGenerationManager::JobFinished(Job* job,
-                                         MhtmlSaveStatus save_status) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-  DCHECK(job);
-  job->MarkAsFinished();
-  job->CloseFile(
-      base::BindOnce(&MHTMLGenerationManager::OnFileClosed,
-                     base::Unretained(this),  // Safe b/c |this| is a singleton.
-                     job->id()),
-      save_status);
-}
-
-void MHTMLGenerationManager::OnFileClosed(
-    int job_id,
-    const std::tuple<MhtmlSaveStatus, int64_t>& save_status_size) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-  MhtmlSaveStatus save_status = std::get<0>(save_status_size);
-  int64_t file_size = std::get<1>(save_status_size);
-
-  Job* job = FindJob(job_id);
-  DCHECK(job);
-  TRACE_EVENT_NESTABLE_ASYNC_END2(
-      "page-serialization", "SavingMhtmlJob", job, "job save status",
-      GetMhtmlSaveStatusLabel(save_status), "file size", file_size);
-  UMA_HISTOGRAM_TIMES("PageSerialization.MhtmlGeneration.FullPageSavingTime",
-                      base::TimeTicks::Now() - job->creation_time());
-  UMA_HISTOGRAM_ENUMERATION("PageSerialization.MhtmlGeneration.FinalSaveStatus",
-                            static_cast<int>(save_status),
-                            static_cast<int>(MhtmlSaveStatus::LAST));
-  std::move(job->callback())
-      .Run(save_status == MhtmlSaveStatus::SUCCESS ? file_size : -1);
-  id_to_job_.erase(job_id);
-}
-
-MHTMLGenerationManager::Job* MHTMLGenerationManager::NewJob(
-    WebContents* web_contents,
-    const MHTMLGenerationParams& params,
-    GenerateMHTMLCallback callback) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
-  Job* job = new Job(++next_job_id_, web_contents, params, std::move(callback));
-  id_to_job_[job->id()] = base::WrapUnique(job);
-  return job;
-}
-
-MHTMLGenerationManager::Job* MHTMLGenerationManager::FindJob(int job_id) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
-  auto iter = id_to_job_.find(job_id);
-  if (iter == id_to_job_.end()) {
-    NOTREACHED();
-    return nullptr;
-  }
-  return iter->second.get();
-}
-
-void MHTMLGenerationManager::RenderProcessExited(Job* job) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-  DCHECK(job);
-  JobFinished(job, MhtmlSaveStatus::RENDER_PROCESS_EXITED);
-}
-
 }  // namespace content
diff --git a/content/browser/download/mhtml_generation_manager.h b/content/browser/download/mhtml_generation_manager.h
index 57a1c7e..72c259bf 100644
--- a/content/browser/download/mhtml_generation_manager.h
+++ b/content/browser/download/mhtml_generation_manager.h
@@ -6,7 +6,6 @@
 
 #include <stdint.h>
 
-#include <map>
 #include <memory>
 #include <set>
 #include <string>
@@ -17,7 +16,6 @@
 #include "base/process/process.h"
 #include "content/common/download/mhtml_save_status.h"
 #include "content/public/common/mhtml_generation_params.h"
-#include "ipc/ipc_platform_file.h"
 
 namespace base {
 class FilePath;
@@ -25,15 +23,14 @@
 
 namespace content {
 
-class RenderFrameHostImpl;
 class WebContents;
 
 // The class and all of its members live on the UI thread.  Only static methods
 // are executed on other threads.
 //
 // MHTMLGenerationManager is a singleton.  Each call to SaveMHTML method creates
-// a new instance of MHTMLGenerationManager::Job that tracks generation of a
-// single MHTML file.
+// a new instance of MHTMLGenerationManager::Job that continues with the MHTML
+// serialization process on its own, eventually deleting itself.
 class MHTMLGenerationManager {
  public:
   static MHTMLGenerationManager* GetInstance();
@@ -49,14 +46,8 @@
                  const MHTMLGenerationParams& params,
                  GenerateMHTMLCallback callback);
 
-  // Handler for FrameHostMsg_SerializeAsMHTMLResponse (a notification from the
-  // renderer that the MHTML generation finished for a single frame).
-  void OnSerializeAsMHTMLResponse(
-      RenderFrameHostImpl* sender,
-      int job_id,
-      MhtmlSaveStatus save_status,
-      const std::set<std::string>& digests_of_uris_of_serialized_resources,
-      base::TimeDelta renderer_main_thread_time);
+  // Called on the file thread to create a new file for MHTML serialization.
+  static base::File CreateFile(const base::FilePath& file_path);
 
  private:
   friend struct base::DefaultSingletonTraits<MHTMLGenerationManager>;
@@ -65,36 +56,6 @@
   MHTMLGenerationManager();
   virtual ~MHTMLGenerationManager();
 
-  // Called on the file thread to create |file|.
-  static base::File CreateFile(const base::FilePath& file_path);
-
-  // Called on the UI thread when the file that should hold the MHTML data has
-  // been created.
-  void OnFileAvailable(int job_id, base::File browser_file);
-
-  // Called on the UI thread when a job has been finished.
-  void JobFinished(Job* job, MhtmlSaveStatus save_status);
-
-  // Called on the UI thread after the file got finalized and we have its size.
-  void OnFileClosed(
-      int job_id,
-      const std::tuple<MhtmlSaveStatus, int64_t>& save_status_size);
-
-  // Creates and registers a new job.
-  Job* NewJob(WebContents* web_contents,
-              const MHTMLGenerationParams& params,
-              GenerateMHTMLCallback callback);
-
-  // Finds job by id.  Returns nullptr if no job with a given id was found.
-  Job* FindJob(int job_id);
-
-  // Called when the render process connected to a job exits.
-  void RenderProcessExited(Job* job);
-
-  std::map<int, std::unique_ptr<Job>> id_to_job_;
-
-  int next_job_id_;
-
   DISALLOW_COPY_AND_ASSIGN(MHTMLGenerationManager);
 };
 
diff --git a/content/browser/frame_host/render_frame_host_impl.cc b/content/browser/frame_host/render_frame_host_impl.cc
index b12276d..e3582271 100644
--- a/content/browser/frame_host/render_frame_host_impl.cc
+++ b/content/browser/frame_host/render_frame_host_impl.cc
@@ -1426,8 +1426,6 @@
     IPC_MESSAGE_HANDLER(FrameHostMsg_DidStopLoading, OnDidStopLoading)
     IPC_MESSAGE_HANDLER(FrameHostMsg_DidChangeLoadProgress,
                         OnDidChangeLoadProgress)
-    IPC_MESSAGE_HANDLER(FrameHostMsg_SerializeAsMHTMLResponse,
-                        OnSerializeAsMHTMLResponse)
     IPC_MESSAGE_HANDLER(FrameHostMsg_SelectionChanged, OnSelectionChanged)
     IPC_MESSAGE_HANDLER(FrameHostMsg_FocusedNodeChanged, OnFocusedNodeChanged)
     IPC_MESSAGE_HANDLER(FrameHostMsg_UpdateUserActivationState,
@@ -3452,16 +3450,6 @@
   frame_tree_node_->DidChangeLoadProgress(load_progress);
 }
 
-void RenderFrameHostImpl::OnSerializeAsMHTMLResponse(
-    int job_id,
-    MhtmlSaveStatus save_status,
-    const std::set<std::string>& digests_of_uris_of_serialized_resources,
-    base::TimeDelta renderer_main_thread_time) {
-  MHTMLGenerationManager::GetInstance()->OnSerializeAsMHTMLResponse(
-      this, job_id, save_status, digests_of_uris_of_serialized_resources,
-      renderer_main_thread_time);
-}
-
 void RenderFrameHostImpl::OnSelectionChanged(const base::string16& text,
                                              uint32_t offset,
                                              const gfx::Range& range) {
diff --git a/content/browser/frame_host/render_frame_host_impl.h b/content/browser/frame_host/render_frame_host_impl.h
index 8a1b64b9..44faa84 100644
--- a/content/browser/frame_host/render_frame_host_impl.h
+++ b/content/browser/frame_host/render_frame_host_impl.h
@@ -40,7 +40,6 @@
 #include "content/common/buildflags.h"
 #include "content/common/content_export.h"
 #include "content/common/content_security_policy/csp_context.h"
-#include "content/common/download/mhtml_save_status.h"
 #include "content/common/frame.mojom.h"
 #include "content/common/frame_message_enums.h"
 #include "content/common/frame_replication_state.h"
@@ -1094,11 +1093,6 @@
       blink::WebSuddenTerminationDisablerType disabler_type);
   void OnDidStopLoading();
   void OnDidChangeLoadProgress(double load_progress);
-  void OnSerializeAsMHTMLResponse(
-      int job_id,
-      MhtmlSaveStatus save_status,
-      const std::set<std::string>& digests_of_uris_of_serialized_resources,
-      base::TimeDelta renderer_main_thread_time);
   void OnSelectionChanged(const base::string16& text,
                           uint32_t offset,
                           const gfx::Range& range);
diff --git a/content/common/BUILD.gn b/content/common/BUILD.gn
index a1ac31a..482dd3e 100644
--- a/content/common/BUILD.gn
+++ b/content/common/BUILD.gn
@@ -484,6 +484,7 @@
     "media/peer_connection_tracker.mojom",
     "media/renderer_audio_input_stream_factory.mojom",
     "media/renderer_audio_output_stream_factory.mojom",
+    "mhtml_file_writer.mojom",
     "native_types.mojom",
     "navigation_client.mojom",
     "navigation_params.mojom",
diff --git a/content/common/frame_messages.h b/content/common/frame_messages.h
index f1b940d..831e7c4 100644
--- a/content/common/frame_messages.h
+++ b/content/common/frame_messages.h
@@ -26,7 +26,6 @@
 #include "content/common/content_param_traits.h"
 #include "content/common/content_security_policy/csp_context.h"
 #include "content/common/content_security_policy_header.h"
-#include "content/common/download/mhtml_save_status.h"
 #include "content/common/frame_message_enums.h"
 #include "content/common/frame_message_structs.h"
 #include "content/common/frame_owner_properties.h"
@@ -613,40 +612,6 @@
   IPC_STRUCT_TRAITS_MEMBER(routing_id)
 IPC_STRUCT_TRAITS_END()
 
-IPC_STRUCT_BEGIN(FrameMsg_SerializeAsMHTML_Params)
-  // Job id - used to match responses to requests.
-  IPC_STRUCT_MEMBER(int, job_id)
-
-  // Destination file handle.
-  IPC_STRUCT_MEMBER(IPC::PlatformFileForTransit, destination_file)
-
-  // MHTML boundary marker / MIME multipart boundary maker.  The same
-  // |mhtml_boundary_marker| should be used for serialization of each frame.
-  IPC_STRUCT_MEMBER(std::string, mhtml_boundary_marker)
-
-  // Whether to use binary encoding while serializing.  Binary encoding is not
-  // supported outside of Chrome, so this should not be used if the MHTML is
-  // intended for sharing.
-  IPC_STRUCT_MEMBER(bool, mhtml_binary_encoding)
-
-  // Whether to remove popup overlay while serializing.
-  IPC_STRUCT_MEMBER(bool, mhtml_popup_overlay_removal)
-
-  // Whether to detect problems while serializing.
-  IPC_STRUCT_MEMBER(bool, mhtml_problem_detection)
-
-  // |digests_of_uris_to_skip| contains digests of uris of MHTML parts that
-  // should be skipped.  This helps deduplicate mhtml parts across frames.
-  // SECURITY NOTE: Sha256 digests (rather than uris) are used to prevent
-  // disclosing uris to other renderer processes;  the digests should be
-  // generated using SHA256HashString function from crypto/sha2.h and hashing
-  // |salt + url.spec()|.
-  IPC_STRUCT_MEMBER(std::set<std::string>, digests_of_uris_to_skip)
-
-  // Salt used for |digests_of_uris_to_skip|.
-  IPC_STRUCT_MEMBER(std::string, salt)
-IPC_STRUCT_END()
-
 // This message is used to send hittesting data from the renderer in order
 // to perform hittesting on the browser process.
 IPC_STRUCT_BEGIN(FrameHostMsg_HittestData_Params)
@@ -1013,13 +978,6 @@
                     FrameMsg_GetSerializedHtmlWithLocalLinks_UrlMap,
                     FrameMsg_GetSerializedHtmlWithLocalLinks_FrameRoutingIdMap)
 
-// Serialize target frame and its resources into MHTML and write it into the
-// provided destination file handle.  Note that when serializing multiple
-// frames, one needs to serialize the *main* frame first (the main frame
-// needs to go first according to RFC2557 + the main frame will trigger
-// generation of the MHTML header).
-IPC_MESSAGE_ROUTED1(FrameMsg_SerializeAsMHTML, FrameMsg_SerializeAsMHTML_Params)
-
 IPC_MESSAGE_ROUTED1(FrameMsg_SetFrameOwnerProperties,
                     content::FrameOwnerProperties /* frame_owner_properties */)
 
@@ -1614,14 +1572,6 @@
                     std::string /* data buffer */,
                     bool /* end of data? */)
 
-// Response to FrameMsg_SerializeAsMHTML.
-IPC_MESSAGE_ROUTED4(
-    FrameHostMsg_SerializeAsMHTMLResponse,
-    int /* job_id (used to match responses to requests) */,
-    content::MhtmlSaveStatus /* final success/failure status */,
-    std::set<std::string> /* digests of uris of serialized resources */,
-    base::TimeDelta /* how much time of the main render thread was used */)
-
 // Sent when the renderer updates hint for importance of a tab.
 IPC_MESSAGE_ROUTED1(FrameHostMsg_UpdatePageImportanceSignals,
                     content::PageImportanceSignals)
diff --git a/content/common/mhtml_file_writer.mojom b/content/common/mhtml_file_writer.mojom
new file mode 100644
index 0000000..a245732
--- /dev/null
+++ b/content/common/mhtml_file_writer.mojom
@@ -0,0 +1,67 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module content.mojom;
+
+import "mojo/public/mojom/base/file.mojom";
+import "mojo/public/mojom/base/time.mojom";
+
+// TODO(crbug/915966): This is currently mapped from mhtml_save_status.h.
+// We should consolidate these sources later to prevent duplicate enums.
+enum MhtmlSaveStatus {
+  SUCCESS = 0,
+  FILE_CLOSING_ERROR,
+  FILE_CREATION_ERROR,
+  FILE_WRITTING_ERROR,
+  FRAME_NO_LONGER_EXISTS,
+  FRAME_SERIALIZATION_FORBIDDEN,
+  RENDER_PROCESS_EXITED,
+};
+
+struct SerializeAsMHTMLParams {
+  // Destination file handle.
+  mojo_base.mojom.File destination_file;
+
+  // MHTML boundary marker / MIME multipart boundary maker. The same
+  // |mhtml_boundary_marker| should be used for serialization of each frame.
+  string mhtml_boundary_marker;
+
+  // Whether to use binary encoding while serializing.  Binary encoding is not
+  // supported outside of Chrome, so this should not be used if the MHTML is
+  // intended for sharing.
+  bool mhtml_binary_encoding;
+
+  // Whether to remove popup overlay while serializing.
+  bool mhtml_popup_overlay_removal;
+
+  // Whether to detect problems while serializing.
+  bool mhtml_problem_detection;
+
+  // To avoid duplicating MHTML parts across frames, |digests_of_uris_to_skip|
+  // contains digests of parts that have already been serialized and should
+  // be skipped.
+  // SECURITY NOTE: Sha256 digests (rather than uris) are used to prevent
+  // disclosing uris to other renderer processes;  the digests should be
+  // generated using SHA256HashString function from crypto/sha2.h and hashing
+  // |salt + url.spec()|.
+  // This array MUST be sorted and contain no duplicates.
+  array<string> digests_of_uris_to_skip;
+
+  // Salt used for |digests_of_uris_to_skip|.
+  string salt;
+};
+
+// Serialize target frame and its resources into MHTML and write it into the
+// provided destination file handle.  Note that when serializing multiple
+// frames, one needs to serialize the *main* frame first (the main frame
+// needs to go first according to RFC2557 + the main frame will trigger
+// generation of the MHTML header).
+interface MhtmlFileWriter {
+  // |renderer_main_thread_time| is the amount of time spent in the main
+  // thread serializing the frame.
+  SerializeAsMHTML(SerializeAsMHTMLParams params)
+    =>  (MhtmlSaveStatus status,
+        array<string> digests_of_uris_to_skip,
+        mojo_base.mojom.TimeDelta renderer_main_thread_time);
+};
\ No newline at end of file
diff --git a/content/public/app/content_renderer_manifest.cc b/content/public/app/content_renderer_manifest.cc
index 22d3f7d..dccb95d 100644
--- a/content/public/app/content_renderer_manifest.cc
+++ b/content/public/app/content_renderer_manifest.cc
@@ -34,6 +34,7 @@
                   "content.mojom.ChildHistogramFetcher",
                   "content.mojom.ChildHistogramFetcherFactory",
                   "content.mojom.FrameFactory",
+                  "content.mojom.MhtmlFileWriter",
                   "content.mojom.RenderWidgetWindowTreeClientFactory",
                   "content.mojom.ResourceUsageReporter",
                   "IPC.mojom.ChannelBootstrap",
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index b82743e..ea7e926 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -630,7 +630,7 @@
 
 // Implementation of WebFrameSerializer::MHTMLPartsGenerationDelegate that
 // 1. Bases shouldSkipResource and getContentID responses on contents of
-//    FrameMsg_SerializeAsMHTML_Params.
+//    SerializeAsMHTMLParams.
 // 2. Stores digests of urls of serialized resources (i.e. urls reported via
 //    shouldSkipResource) into |serialized_resources_uri_digests| passed
 //    to the constructor.
@@ -638,11 +638,18 @@
     : public WebFrameSerializer::MHTMLPartsGenerationDelegate {
  public:
   MHTMLPartsGenerationDelegate(
-      const FrameMsg_SerializeAsMHTML_Params& params,
-      std::set<std::string>* serialized_resources_uri_digests)
+      const mojom::SerializeAsMHTMLParams& params,
+      std::unordered_set<std::string>* serialized_resources_uri_digests)
       : params_(params),
         serialized_resources_uri_digests_(serialized_resources_uri_digests) {
     DCHECK(serialized_resources_uri_digests_);
+    // Digests must be sorted for binary search.
+    DCHECK(std::is_sorted(params_.digests_of_uris_to_skip.begin(),
+                          params_.digests_of_uris_to_skip.end()));
+    // URLs are not duplicated.
+    DCHECK(std::adjacent_find(params_.digests_of_uris_to_skip.begin(),
+                              params_.digests_of_uris_to_skip.end()) ==
+           params_.digests_of_uris_to_skip.end());
   }
 
   bool ShouldSkipResource(const WebURL& url) override {
@@ -650,7 +657,8 @@
         crypto::SHA256HashString(params_.salt + GURL(url).spec());
 
     // Skip if the |url| already covered by serialization of an *earlier* frame.
-    if (base::ContainsKey(params_.digests_of_uris_to_skip, digest))
+    if (std::binary_search(params_.digests_of_uris_to_skip.begin(),
+                           params_.digests_of_uris_to_skip.end(), digest))
       return true;
 
     // Let's record |url| as being serialized for the *current* frame.
@@ -672,8 +680,8 @@
   }
 
  private:
-  const FrameMsg_SerializeAsMHTML_Params& params_;
-  std::set<std::string>* serialized_resources_uri_digests_;
+  const mojom::SerializeAsMHTMLParams& params_;
+  std::unordered_set<std::string>* serialized_resources_uri_digests_;
 
   DISALLOW_COPY_AND_ASSIGN(MHTMLPartsGenerationDelegate);
 };
@@ -1709,6 +1717,7 @@
       frame_bindings_control_binding_(this),
       frame_navigation_control_binding_(this),
       fullscreen_binding_(this),
+      mhtml_file_writer_binding_(this),
       navigation_client_impl_(nullptr),
       has_accessed_initial_document_(false),
       media_factory_(this,
@@ -2155,7 +2164,6 @@
                         OnGetSavableResourceLinks)
     IPC_MESSAGE_HANDLER(FrameMsg_GetSerializedHtmlWithLocalLinks,
                         OnGetSerializedHtmlWithLocalLinks)
-    IPC_MESSAGE_HANDLER(FrameMsg_SerializeAsMHTML, OnSerializeAsMHTML)
     IPC_MESSAGE_HANDLER(FrameMsg_EnableViewSourceMode, OnEnableViewSourceMode)
     IPC_MESSAGE_HANDLER(FrameMsg_SuppressFurtherDialogs,
                         OnSuppressFurtherDialogs)
@@ -6508,21 +6516,24 @@
                                 &delegate);
 }
 
-void RenderFrameImpl::OnSerializeAsMHTML(
-    const FrameMsg_SerializeAsMHTML_Params& params) {
-  TRACE_EVENT0("page-serialization", "RenderFrameImpl::OnSerializeAsMHTML");
+// mojom::MhtmlFileWriter implementation
+// ----------------------------------------
+
+void RenderFrameImpl::SerializeAsMHTML(mojom::SerializeAsMHTMLParamsPtr params,
+                                       SerializeAsMHTMLCallback callback) {
+  TRACE_EVENT0("page-serialization", "RenderFrameImpl::SerializeAsMHTML");
   base::TimeTicks start_time = base::TimeTicks::Now();
-  // Unpack IPC payload.
-  base::File file = IPC::PlatformFileForTransitToFile(params.destination_file);
+
+  // Unpack payload.
   const WebString mhtml_boundary =
-      WebString::FromUTF8(params.mhtml_boundary_marker);
+      WebString::FromUTF8(params->mhtml_boundary_marker);
   DCHECK(!mhtml_boundary.IsEmpty());
 
   // Holds WebThreadSafeData instances for some or all of header, contents and
   // footer.
   std::vector<WebThreadSafeData> mhtml_contents;
-  std::set<std::string> serialized_resources_uri_digests;
-  MHTMLPartsGenerationDelegate delegate(params,
+  std::unordered_set<std::string> serialized_resources_uri_digests;
+  MHTMLPartsGenerationDelegate delegate(*params,
                                         &serialized_resources_uri_digests);
 
   MhtmlSaveStatus save_status = MhtmlSaveStatus::SUCCESS;
@@ -6531,7 +6542,7 @@
   // Generate MHTML header if needed.
   if (IsMainFrame()) {
     TRACE_EVENT0("page-serialization",
-                 "RenderFrameImpl::OnSerializeAsMHTML header");
+                 "RenderFrameImpl::SerializeAsMHTML header");
     // The returned data can be empty if the main frame should be skipped. If
     // the main frame is skipped, then the whole archive is bad.
     mhtml_contents.emplace_back(WebFrameSerializer::GenerateMHTMLHeader(
@@ -6544,7 +6555,7 @@
   // results in an omitted resource in the final file.
   if (save_status == MhtmlSaveStatus::SUCCESS) {
     TRACE_EVENT0("page-serialization",
-                 "RenderFrameImpl::OnSerializeAsMHTML parts serialization");
+                 "RenderFrameImpl::SerializeAsMHTML parts serialization");
     // The returned data can be empty if the frame should be skipped, but this
     // is OK.
     mhtml_contents.emplace_back(WebFrameSerializer::GenerateMHTMLParts(
@@ -6566,34 +6577,41 @@
   if (save_status == MhtmlSaveStatus::SUCCESS && has_some_data) {
     base::PostTaskWithTraitsAndReplyWithResult(
         FROM_HERE, {base::MayBlock()},
-        base::Bind(&WriteMHTMLToDisk, base::Passed(&mhtml_contents),
-                   base::Passed(&file)),
-        base::Bind(&RenderFrameImpl::OnWriteMHTMLToDiskComplete,
-                   weak_factory_.GetWeakPtr(), params.job_id,
-                   base::Passed(&serialized_resources_uri_digests),
-                   main_thread_use_time));
+        base::BindOnce(&WriteMHTMLToDisk, std::move(mhtml_contents),
+                       std::move(params->destination_file)),
+        base::BindOnce(&RenderFrameImpl::OnWriteMHTMLToDiskComplete,
+                       weak_factory_.GetWeakPtr(), std::move(callback),
+                       std::move(serialized_resources_uri_digests),
+                       main_thread_use_time));
   } else {
-    file.Close();
-    OnWriteMHTMLToDiskComplete(params.job_id, serialized_resources_uri_digests,
+    params->destination_file.Close();
+    OnWriteMHTMLToDiskComplete(std::move(callback),
+                               std::move(serialized_resources_uri_digests),
                                main_thread_use_time, save_status);
   }
 }
 
 void RenderFrameImpl::OnWriteMHTMLToDiskComplete(
-    int job_id,
-    std::set<std::string> serialized_resources_uri_digests,
+    SerializeAsMHTMLCallback callback,
+    std::unordered_set<std::string> serialized_resources_uri_digests,
     base::TimeDelta main_thread_use_time,
     MhtmlSaveStatus save_status) {
   TRACE_EVENT1("page-serialization",
                "RenderFrameImpl::OnWriteMHTMLToDiskComplete",
                "frame save status", GetMhtmlSaveStatusLabel(save_status));
   DCHECK(RenderThread::Get()) << "Must run in the main renderer thread";
-  // Notify the browser process about completion.
+
+  // Convert the set into a vector for transport.
+  std::vector<std::string> digests_of_new_parts(
+      std::make_move_iterator(serialized_resources_uri_digests.begin()),
+      std::make_move_iterator(serialized_resources_uri_digests.end()));
+
+  // Notify the browser process about completion using the callback.
   // Note: we assume this method is fast enough to not need to be accounted for
   // in PageSerialization.MhtmlGeneration.RendererMainThreadTime.SingleFrame.
-  Send(new FrameHostMsg_SerializeAsMHTMLResponse(
-      routing_id_, job_id, save_status, serialized_resources_uri_digests,
-      main_thread_use_time));
+  std::move(callback).Run(
+      static_cast<content::mojom::MhtmlSaveStatus>(save_status),
+      std::move(digests_of_new_parts), main_thread_use_time);
 }
 
 #ifndef STATIC_ASSERT_ENUM
@@ -7247,6 +7265,9 @@
   registry_.AddInterface(
       base::Bind(&RenderFrameImpl::BindWidget, weak_factory_.GetWeakPtr()));
 
+  GetAssociatedInterfaceRegistry()->AddInterface(base::BindRepeating(
+      &RenderFrameImpl::BindMhtmlFileWriter, base::Unretained(this)));
+
   if (!frame_->Parent()) {
     // Only main frame have ImageDownloader service.
     registry_.AddInterface(base::Bind(&ImageDownloaderImpl::CreateMojoService,
@@ -7270,6 +7291,12 @@
                           GetTaskRunner(blink::TaskType::kInternalIPC));
 }
 
+void RenderFrameImpl::BindMhtmlFileWriter(
+    mojom::MhtmlFileWriterAssociatedRequest request) {
+  mhtml_file_writer_binding_.Bind(
+      std::move(request), GetTaskRunner(blink::TaskType::kInternalDefault));
+}
+
 void RenderFrameImpl::CheckIfAudioSinkExistsAndIsAuthorized(
     const blink::WebString& sink_id,
     std::unique_ptr<blink::WebSetSinkIdCallbacks> callbacks) {
diff --git a/content/renderer/render_frame_impl.h b/content/renderer/render_frame_impl.h
index 634e9d0..8f17a70 100644
--- a/content/renderer/render_frame_impl.h
+++ b/content/renderer/render_frame_impl.h
@@ -12,6 +12,7 @@
 #include <memory>
 #include <set>
 #include <string>
+#include <unordered_set>
 #include <utility>
 #include <vector>
 
@@ -35,6 +36,7 @@
 #include "content/common/frame_message_enums.h"
 #include "content/common/host_zoom.mojom.h"
 #include "content/common/media/renderer_audio_input_stream_factory.mojom.h"
+#include "content/common/mhtml_file_writer.mojom.h"
 #include "content/common/possibly_associated_interface_ptr.h"
 #include "content/common/renderer.mojom.h"
 #include "content/common/unique_name_helper.h"
@@ -110,7 +112,6 @@
 
 struct FrameMsg_MixedContentFound_Params;
 struct FrameMsg_PostMessage_Params;
-struct FrameMsg_SerializeAsMHTML_Params;
 struct FrameMsg_TextTrackSettings_Params;
 
 namespace blink {
@@ -190,6 +191,7 @@
       mojom::FullscreenVideoElementHandler,
       mojom::HostZoom,
       mojom::FrameBindingsControl,
+      mojom::MhtmlFileWriter,
       public blink::WebLocalFrameClient,
       public blink::WebFrameSerializerClient,
       service_manager::mojom::InterfaceProvider {
@@ -638,6 +640,10 @@
   // mojom::HostZoom implementation:
   void SetHostZoomLevel(const GURL& url, double zoom_level) override;
 
+  // mojom::MhtmlFileWriter implementation:
+  void SerializeAsMHTML(const mojom::SerializeAsMHTMLParamsPtr params,
+                        SerializeAsMHTMLCallback callback) override;
+
   // blink::WebLocalFrameClient implementation:
   blink::WebPlugin* CreatePlugin(const blink::WebPluginParams& params) override;
   blink::WebMediaPlayer* CreateMediaPlayer(
@@ -857,6 +863,9 @@
   void BindFullscreen(
       mojom::FullscreenVideoElementHandlerAssociatedRequest request);
 
+  // Binds to the MHTML file generation service in the browser.
+  void BindMhtmlFileWriter(mojom::MhtmlFileWriterAssociatedRequest request);
+
   // Binds to the autoplay configuration service in the browser.
   void BindAutoplayConfiguration(
       blink::mojom::AutoplayConfigurationClientAssociatedRequest request);
@@ -1119,7 +1128,6 @@
   void OnGetSerializedHtmlWithLocalLinks(
       const std::map<GURL, base::FilePath>& url_to_local_path,
       const std::map<int, base::FilePath>& frame_routing_id_to_local_path);
-  void OnSerializeAsMHTML(const FrameMsg_SerializeAsMHTML_Params& params);
   void OnEnableViewSourceMode();
   void OnSuppressFurtherDialogs();
   void OnClearFocusedElement();
@@ -1142,11 +1150,11 @@
 #endif
 #endif
 
-  // Callback scheduled from OnSerializeAsMHTML for when writing serialized
+  // Callback scheduled from SerializeAsMHTML for when writing serialized
   // MHTML to file has been completed in the file thread.
   void OnWriteMHTMLToDiskComplete(
-      int job_id,
-      std::set<std::string> serialized_resources_uri_digests,
+      SerializeAsMHTMLCallback callback,
+      std::unordered_set<std::string> serialized_resources_uri_digests,
       base::TimeDelta main_thread_use_time,
       MhtmlSaveStatus save_status);
 
@@ -1634,6 +1642,7 @@
       frame_navigation_control_binding_;
   mojo::AssociatedBinding<mojom::FullscreenVideoElementHandler>
       fullscreen_binding_;
+  mojo::AssociatedBinding<mojom::MhtmlFileWriter> mhtml_file_writer_binding_;
 
   // Only used when PerNavigationMojoInterface is enabled.
   std::unique_ptr<NavigationClient> navigation_client_impl_;
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 2f5a2cb..f5a0b8d8 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -4054,7 +4054,7 @@
       label="OBSOLETE_WC_CONTENT_WITH_CERT_ERRORS_BAD_SECURITY_INFO"/>
   <int value="102" label="RFMF_RENDERER_FAKED_ITS_OWN_DEATH"/>
   <int value="103" label="DWNLD_INVALID_SAVABLE_RESOURCE_LINKS_RESPONSE"/>
-  <int value="104" label="DWNLD_INVALID_SERIALIZE_AS_MHTML_RESPONSE"/>
+  <int value="104" label="OBSOLETE_DWNLD_INVALID_SERIALIZE_AS_MHTML_RESPONSE"/>
   <int value="105" label="BDH_DEVICE_NOT_ALLOWED_FOR_ORIGIN"/>
   <int value="106" label="OBSOLETE_ACI_WRONG_STORAGE_PARTITION"/>
   <int value="107" label="OBSOLETE_RDHI_WRONG_STORAGE_PARTITION"/>