1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "components/paint_preview/renderer/paint_preview_recorder_impl.h"
6 
7 #include <utility>
8 
9 #include "base/auto_reset.h"
10 #include "base/metrics/histogram_functions.h"
11 #include "base/task_runner.h"
12 #include "base/time/time.h"
13 #include "cc/paint/paint_record.h"
14 #include "cc/paint/paint_recorder.h"
15 #include "components/paint_preview/renderer/paint_preview_recorder_utils.h"
16 #include "content/public/renderer/render_frame.h"
17 #include "mojo/public/cpp/base/shared_memory_utils.h"
18 #include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h"
19 #include "third_party/blink/public/web/web_local_frame.h"
20 
21 namespace paint_preview {
22 
23 namespace {
24 
FinishRecording(sk_sp<const cc::PaintRecord> recording,const gfx::Rect & bounds,PaintPreviewTracker * tracker,base::File skp_file,mojom::PaintPreviewCaptureResponse * response)25 mojom::PaintPreviewStatus FinishRecording(
26     sk_sp<const cc::PaintRecord> recording,
27     const gfx::Rect& bounds,
28     PaintPreviewTracker* tracker,
29     base::File skp_file,
30     mojom::PaintPreviewCaptureResponse* response) {
31   ParseGlyphs(recording.get(), tracker);
32   if (!SerializeAsSkPicture(recording, tracker, bounds, std::move(skp_file)))
33     return mojom::PaintPreviewStatus::kCaptureFailed;
34 
35   BuildResponse(tracker, response);
36   return mojom::PaintPreviewStatus::kOk;
37 }
38 
39 }  // namespace
40 
PaintPreviewRecorderImpl(content::RenderFrame * render_frame)41 PaintPreviewRecorderImpl::PaintPreviewRecorderImpl(
42     content::RenderFrame* render_frame)
43     : content::RenderFrameObserver(render_frame),
44       is_painting_preview_(false),
45       is_main_frame_(render_frame->IsMainFrame()) {
46   render_frame->GetAssociatedInterfaceRegistry()->AddInterface(
47       base::BindRepeating(&PaintPreviewRecorderImpl::BindPaintPreviewRecorder,
48                           weak_ptr_factory_.GetWeakPtr()));
49 }
50 
51 PaintPreviewRecorderImpl::~PaintPreviewRecorderImpl() = default;
52 
CapturePaintPreview(mojom::PaintPreviewCaptureParamsPtr params,CapturePaintPreviewCallback callback)53 void PaintPreviewRecorderImpl::CapturePaintPreview(
54     mojom::PaintPreviewCaptureParamsPtr params,
55     CapturePaintPreviewCallback callback) {
56   mojom::PaintPreviewStatus status = mojom::PaintPreviewStatus::kOk;
57   base::ReadOnlySharedMemoryRegion region;
58   // This should not be called recursively or multiple times while unfinished
59   // (Blink can only run one capture per RenderFrame at a time).
60   DCHECK(!is_painting_preview_);
61   // DCHECK, but fallback safely as it is difficult to reason about whether this
62   // might happen due to it being tied to a RenderFrame rather than
63   // RenderWidget and we don't want to crash the renderer as this is
64   // recoverable.
65   auto response = mojom::PaintPreviewCaptureResponse::New();
66   if (is_painting_preview_) {
67     status = mojom::PaintPreviewStatus::kAlreadyCapturing;
68     std::move(callback).Run(status, std::move(response));
69     return;
70   }
71   base::AutoReset<bool>(&is_painting_preview_, true);
72 
73   CapturePaintPreviewInternal(params, response.get(), &status);
74   std::move(callback).Run(status, std::move(response));
75 }
76 
OnDestruct()77 void PaintPreviewRecorderImpl::OnDestruct() {
78   paint_preview_recorder_receiver_.reset();
79   base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, this);
80 }
81 
BindPaintPreviewRecorder(mojo::PendingAssociatedReceiver<mojom::PaintPreviewRecorder> receiver)82 void PaintPreviewRecorderImpl::BindPaintPreviewRecorder(
83     mojo::PendingAssociatedReceiver<mojom::PaintPreviewRecorder> receiver) {
84   paint_preview_recorder_receiver_.Bind(std::move(receiver));
85 }
86 
CapturePaintPreviewInternal(const mojom::PaintPreviewCaptureParamsPtr & params,mojom::PaintPreviewCaptureResponse * response,mojom::PaintPreviewStatus * status)87 void PaintPreviewRecorderImpl::CapturePaintPreviewInternal(
88     const mojom::PaintPreviewCaptureParamsPtr& params,
89     mojom::PaintPreviewCaptureResponse* response,
90     mojom::PaintPreviewStatus* status) {
91   blink::WebLocalFrame* frame = render_frame()->GetWebFrame();
92   // Ensure the a frame actually exists to avoid a possible crash.
93   if (!frame) {
94     DVLOG(1) << "Error: renderer has no frame yet!";
95     return;
96   }
97 
98   // Warm up paint for an out-of-lifecycle paint phase.
99   frame->DispatchBeforePrintEvent();
100 
101   DCHECK_EQ(is_main_frame_, params->is_main_frame);
102   gfx::Rect bounds;
103   if (is_main_frame_ || params->clip_rect == gfx::Rect(0, 0, 0, 0)) {
104     auto size = frame->DocumentSize();
105 
106     // |size| may be 0 if a tab is captured prior to layout finishing. This
107     // shouldn't occur often, if at all, in normal usage. However, this may
108     // occur during tests. Capturing prior to layout is non-sensical as the
109     // canvas size cannot be deremined so just abort.
110     if (size.height == 0 || size.width == 0) {
111       *status = mojom::PaintPreviewStatus::kCaptureFailed;
112       return;
113     }
114     bounds = gfx::Rect(0, 0, size.width, size.height);
115   } else {
116     bounds = gfx::Rect(params->clip_rect.size());
117   }
118 
119   cc::PaintRecorder recorder;
120   PaintPreviewTracker tracker(params->guid, frame->GetEmbeddingToken(),
121                               is_main_frame_);
122   cc::PaintCanvas* canvas =
123       recorder.beginRecording(bounds.width(), bounds.height());
124   canvas->SetPaintPreviewTracker(&tracker);
125 
126   // Use time ticks manually rather than a histogram macro so as to;
127   // 1. Account for main frames and subframes separately.
128   // 2. Mitigate binary size as this won't be used that often.
129   // 3. Record only on successes as failures are likely to be outliers (fast or
130   //    slow).
131   base::TimeTicks start_time = base::TimeTicks::Now();
132   bool success = frame->CapturePaintPreview(bounds, canvas);
133   base::TimeDelta capture_time = base::TimeTicks::Now() - start_time;
134   response->blink_recording_time = capture_time;
135 
136   if (is_main_frame_) {
137     base::UmaHistogramBoolean("Renderer.PaintPreview.Capture.MainFrameSuccess",
138                               success);
139     if (success) {
140       // Main frame should generally be the largest cost and will always run so
141       // it is tracked separately.
142       base::UmaHistogramTimes(
143           "Renderer.PaintPreview.Capture.MainFrameBlinkCaptureDuration",
144           capture_time);
145     }
146   } else {
147     base::UmaHistogramBoolean("Renderer.PaintPreview.Capture.SubframeSuccess",
148                               success);
149     if (success) {
150       base::UmaHistogramTimes(
151           "Renderer.PaintPreview.Capture.SubframeBlinkCaptureDuration",
152           capture_time);
153     }
154   }
155 
156   // Restore to before out-of-lifecycle paint phase.
157   frame->DispatchAfterPrintEvent();
158   if (!success) {
159     *status = mojom::PaintPreviewStatus::kCaptureFailed;
160     return;
161   }
162 
163   // TODO(crbug/1011896): Determine if making this async would be beneficial.
164   *status = FinishRecording(recorder.finishRecordingAsPicture(), bounds,
165                             &tracker, std::move(params->file), response);
166 }
167 
168 }  // namespace paint_preview
169