Commit 33132ec8 authored by Stephen Nusko's avatar Stephen Nusko Committed by Commit Bot

Revert "Create MemoryManagedPaintCanvas class"

This reverts commit 42d682b8.

Reason for revert: Breaks perf tests crbug/1035865

Original change's description:
> Create MemoryManagedPaintCanvas class
> 
> With deferral enabled a canvas rendering context can create an
> unlimited amount of GPU resources between frames. This class allows us
> to track how much memory is being pinned and to flush the recording
> after we pass a certain threshold.
> 
> Bug: 1016727, 1015729
> Change-Id: I3a64dc07cc0239447aa721ea2a7cc808541a0a36
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1960469
> Reviewed-by: Jeremy Roman <jbroman@chromium.org>
> Reviewed-by: Aaron Krajeski <aaronhk@chromium.org>
> Reviewed-by: Yi Xu <yiyix@chromium.org>
> Reviewed-by: Khushal <khushalsagar@chromium.org>
> Reviewed-by: Juanmi Huertas <juanmihd@chromium.org>
> Commit-Queue: Aaron Krajeski <aaronhk@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#726085}

TBR=jbroman@chromium.org,fserb@chromium.org,khushalsagar@chromium.org,yiyix@chromium.org,aaronhk@chromium.org,juanmihd@chromium.org

Bug: 1016727, 1015729,1035865
Change-Id: Ia0e7899b4381ae264ed67ae618d245d6a462067b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1976003Reviewed-by: default avatarStephen Nusko <nuskos@chromium.org>
Commit-Queue: Stephen Nusko <nuskos@chromium.org>
Auto-Submit: Stephen Nusko <nuskos@chromium.org>
Cr-Commit-Position: refs/heads/master@{#726437}
parent ba3ecb82
...@@ -17,7 +17,7 @@ PaintRecorder::~PaintRecorder() = default; ...@@ -17,7 +17,7 @@ PaintRecorder::~PaintRecorder() = default;
PaintCanvas* PaintRecorder::beginRecording(const SkRect& bounds) { PaintCanvas* PaintRecorder::beginRecording(const SkRect& bounds) {
display_item_list_->StartPaint(); display_item_list_->StartPaint();
canvas_ = CreateCanvas(display_item_list_.get(), bounds); canvas_.emplace(display_item_list_.get(), bounds);
return getRecordingCanvas(); return getRecordingCanvas();
} }
...@@ -41,10 +41,4 @@ sk_sp<PaintRecord> PaintRecorder::finishRecordingAsPicture() { ...@@ -41,10 +41,4 @@ sk_sp<PaintRecord> PaintRecorder::finishRecordingAsPicture() {
return display_item_list_->ReleaseAsRecord(); return display_item_list_->ReleaseAsRecord();
} }
std::unique_ptr<RecordPaintCanvas> PaintRecorder::CreateCanvas(
DisplayItemList* list,
const SkRect& bounds) {
return std::make_unique<RecordPaintCanvas>(list, bounds);
}
} // namespace cc } // namespace cc
...@@ -18,7 +18,7 @@ class CC_PAINT_EXPORT PaintRecorder { ...@@ -18,7 +18,7 @@ class CC_PAINT_EXPORT PaintRecorder {
public: public:
PaintRecorder(); PaintRecorder();
PaintRecorder(const PaintRecorder&) = delete; PaintRecorder(const PaintRecorder&) = delete;
virtual ~PaintRecorder(); ~PaintRecorder();
PaintRecorder& operator=(const PaintRecorder&) = delete; PaintRecorder& operator=(const PaintRecorder&) = delete;
...@@ -32,18 +32,14 @@ class CC_PAINT_EXPORT PaintRecorder { ...@@ -32,18 +32,14 @@ class CC_PAINT_EXPORT PaintRecorder {
// Only valid while recording. // Only valid while recording.
ALWAYS_INLINE RecordPaintCanvas* getRecordingCanvas() { ALWAYS_INLINE RecordPaintCanvas* getRecordingCanvas() {
return canvas_.get(); return canvas_.has_value() ? &canvas_.value() : nullptr;
} }
sk_sp<PaintRecord> finishRecordingAsPicture(); sk_sp<PaintRecord> finishRecordingAsPicture();
protected:
virtual std::unique_ptr<RecordPaintCanvas> CreateCanvas(DisplayItemList* list,
const SkRect& bounds);
private: private:
scoped_refptr<DisplayItemList> display_item_list_; scoped_refptr<DisplayItemList> display_item_list_;
std::unique_ptr<RecordPaintCanvas> canvas_; base::Optional<RecordPaintCanvas> canvas_;
}; };
} // namespace cc } // namespace cc
......
...@@ -21,7 +21,7 @@ namespace cc { ...@@ -21,7 +21,7 @@ namespace cc {
class DisplayItemList; class DisplayItemList;
class PaintFlags; class PaintFlags;
class CC_PAINT_EXPORT RecordPaintCanvas : public PaintCanvas { class CC_PAINT_EXPORT RecordPaintCanvas final : public PaintCanvas {
public: public:
RecordPaintCanvas(DisplayItemList* list, const SkRect& bounds); RecordPaintCanvas(DisplayItemList* list, const SkRect& bounds);
RecordPaintCanvas(const RecordPaintCanvas&) = delete; RecordPaintCanvas(const RecordPaintCanvas&) = delete;
......
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
#include "third_party/blink/renderer/platform/fonts/text_run_paint_info.h" #include "third_party/blink/renderer/platform/fonts/text_run_paint_info.h"
#include "third_party/blink/renderer/platform/graphics/canvas_resource_provider.h" #include "third_party/blink/renderer/platform/graphics/canvas_resource_provider.h"
#include "third_party/blink/renderer/platform/graphics/graphics_types.h" #include "third_party/blink/renderer/platform/graphics/graphics_types.h"
#include "third_party/blink/renderer/platform/graphics/memory_managed_paint_recorder.h"
#include "third_party/blink/renderer/platform/graphics/paint/paint_canvas.h" #include "third_party/blink/renderer/platform/graphics/paint/paint_canvas.h"
#include "third_party/blink/renderer/platform/graphics/skia/skia_utils.h" #include "third_party/blink/renderer/platform/graphics/skia/skia_utils.h"
#include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h" #include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h"
...@@ -86,11 +85,6 @@ OffscreenCanvasRenderingContext2D::OffscreenCanvasRenderingContext2D( ...@@ -86,11 +85,6 @@ OffscreenCanvasRenderingContext2D::OffscreenCanvasRenderingContext2D(
bernoulli_distribution_(kUMASampleProbability) { bernoulli_distribution_(kUMASampleProbability) {
is_valid_size_ = IsValidImageSize(Host()->Size()); is_valid_size_ = IsValidImageSize(Host()->Size());
// A raw pointer is safe here because the callback is only used by the
// recorder_
finalize_frame_callback_ =
WTF::BindRepeating(&OffscreenCanvasRenderingContext2D::FinalizeFrame,
WrapWeakPersistent(this));
StartRecording(); StartRecording();
// Clear the background transparent or opaque. Similar code at // Clear the background transparent or opaque. Similar code at
...@@ -132,12 +126,13 @@ void OffscreenCanvasRenderingContext2D::commit() { ...@@ -132,12 +126,13 @@ void OffscreenCanvasRenderingContext2D::commit() {
} }
void OffscreenCanvasRenderingContext2D::StartRecording() { void OffscreenCanvasRenderingContext2D::StartRecording() {
recorder_ = recorder_ = std::make_unique<PaintRecorder>();
std::make_unique<MemoryManagedPaintRecorder>(finalize_frame_callback_);
cc::PaintCanvas* canvas = recorder_->beginRecording(Width(), Height()); cc::PaintCanvas* canvas = recorder_->beginRecording(Width(), Height());
// Always save an initial frame, to support resetting the top level matrix // Always save an initial frame, to support resetting the top level matrix
// and clip. // and clip.
canvas->save(); canvas->save();
RestoreMatrixClipStack(canvas); RestoreMatrixClipStack(canvas);
} }
...@@ -298,10 +293,17 @@ cc::PaintCanvas* OffscreenCanvasRenderingContext2D::DrawingCanvas() const { ...@@ -298,10 +293,17 @@ cc::PaintCanvas* OffscreenCanvasRenderingContext2D::DrawingCanvas() const {
return recorder_->getRecordingCanvas(); return recorder_->getRecordingCanvas();
} }
cc::PaintCanvas* OffscreenCanvasRenderingContext2D::ExistingDrawingCanvas()
const {
if (!is_valid_size_)
return nullptr;
return recorder_->getRecordingCanvas();
}
void OffscreenCanvasRenderingContext2D::DidDraw() { void OffscreenCanvasRenderingContext2D::DidDraw() {
have_recorded_draw_commands_ = true; have_recorded_draw_commands_ = true;
dirty_rect_for_commit_.setWH(Width(), Height());
Host()->DidDraw(); Host()->DidDraw();
dirty_rect_for_commit_.setWH(Width(), Height());
} }
void OffscreenCanvasRenderingContext2D::DidDraw(const SkIRect& dirty_rect) { void OffscreenCanvasRenderingContext2D::DidDraw(const SkIRect& dirty_rect) {
......
...@@ -103,9 +103,7 @@ class MODULES_EXPORT OffscreenCanvasRenderingContext2D final ...@@ -103,9 +103,7 @@ class MODULES_EXPORT OffscreenCanvasRenderingContext2D final
bool ParseColorOrCurrentColor(Color&, const String& color_string) const final; bool ParseColorOrCurrentColor(Color&, const String& color_string) const final;
cc::PaintCanvas* DrawingCanvas() const final; cc::PaintCanvas* DrawingCanvas() const final;
cc::PaintCanvas* ExistingDrawingCanvas() const final { cc::PaintCanvas* ExistingDrawingCanvas() const final;
return DrawingCanvas();
}
void DidDraw() final; void DidDraw() final;
void DidDraw(const SkIRect& dirty_rect) final; void DidDraw(const SkIRect& dirty_rect) final;
...@@ -162,8 +160,6 @@ class MODULES_EXPORT OffscreenCanvasRenderingContext2D final ...@@ -162,8 +160,6 @@ class MODULES_EXPORT OffscreenCanvasRenderingContext2D final
std::mt19937 random_generator_; std::mt19937 random_generator_;
std::bernoulli_distribution bernoulli_distribution_; std::bernoulli_distribution bernoulli_distribution_;
base::RepeatingClosure finalize_frame_callback_;
}; };
DEFINE_TYPE_CASTS(OffscreenCanvasRenderingContext2D, DEFINE_TYPE_CASTS(OffscreenCanvasRenderingContext2D,
......
...@@ -996,10 +996,6 @@ jumbo_component("platform") { ...@@ -996,10 +996,6 @@ jumbo_component("platform") {
"graphics/mailbox_texture_holder.h", "graphics/mailbox_texture_holder.h",
"graphics/main_thread_mutator_client.cc", "graphics/main_thread_mutator_client.cc",
"graphics/main_thread_mutator_client.h", "graphics/main_thread_mutator_client.h",
"graphics/memory_managed_paint_canvas.cc",
"graphics/memory_managed_paint_canvas.h",
"graphics/memory_managed_paint_recorder.cc",
"graphics/memory_managed_paint_recorder.h",
"graphics/mutator_client.h", "graphics/mutator_client.h",
"graphics/offscreen_canvas_placeholder.cc", "graphics/offscreen_canvas_placeholder.cc",
"graphics/offscreen_canvas_placeholder.h", "graphics/offscreen_canvas_placeholder.h",
......
...@@ -43,7 +43,6 @@ ...@@ -43,7 +43,6 @@
#include "third_party/blink/renderer/platform/graphics/gpu/shared_context_rate_limiter.h" #include "third_party/blink/renderer/platform/graphics/gpu/shared_context_rate_limiter.h"
#include "third_party/blink/renderer/platform/graphics/gpu/shared_gpu_context.h" #include "third_party/blink/renderer/platform/graphics/gpu/shared_gpu_context.h"
#include "third_party/blink/renderer/platform/graphics/graphics_layer.h" #include "third_party/blink/renderer/platform/graphics/graphics_layer.h"
#include "third_party/blink/renderer/platform/graphics/memory_managed_paint_recorder.h"
#include "third_party/blink/renderer/platform/graphics/paint/paint_canvas.h" #include "third_party/blink/renderer/platform/graphics/paint/paint_canvas.h"
#include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h" #include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h"
#include "third_party/blink/renderer/platform/graphics/unaccelerated_static_bitmap_image.h" #include "third_party/blink/renderer/platform/graphics/unaccelerated_static_bitmap_image.h"
...@@ -75,11 +74,6 @@ Canvas2DLayerBridge::Canvas2DLayerBridge(const IntSize& size, ...@@ -75,11 +74,6 @@ Canvas2DLayerBridge::Canvas2DLayerBridge(const IntSize& size,
// Used by browser tests to detect the use of a Canvas2DLayerBridge. // Used by browser tests to detect the use of a Canvas2DLayerBridge.
TRACE_EVENT_INSTANT0("test_gpu", "Canvas2DLayerBridgeCreation", TRACE_EVENT_INSTANT0("test_gpu", "Canvas2DLayerBridgeCreation",
TRACE_EVENT_SCOPE_GLOBAL); TRACE_EVENT_SCOPE_GLOBAL);
// A raw pointer is safe here because the callback is only used by the
// |recorder_|.
finalize_frame_callback_ = WTF::BindRepeating(
&Canvas2DLayerBridge::FinalizeFrame, WTF::Unretained(this));
StartRecording(); StartRecording();
// Clear the background transparent or opaque. Similar code at // Clear the background transparent or opaque. Similar code at
...@@ -115,11 +109,9 @@ Canvas2DLayerBridge::~Canvas2DLayerBridge() { ...@@ -115,11 +109,9 @@ Canvas2DLayerBridge::~Canvas2DLayerBridge() {
} }
void Canvas2DLayerBridge::StartRecording() { void Canvas2DLayerBridge::StartRecording() {
recorder_ = recorder_ = std::make_unique<PaintRecorder>();
std::make_unique<MemoryManagedPaintRecorder>(finalize_frame_callback_);
cc::PaintCanvas* canvas = cc::PaintCanvas* canvas =
recorder_->beginRecording(size_.Width(), size_.Height()); recorder_->beginRecording(size_.Width(), size_.Height());
// Always save an initial frame, to support resetting the top level matrix // Always save an initial frame, to support resetting the top level matrix
// and clip. // and clip.
canvas->save(); canvas->save();
......
...@@ -255,8 +255,6 @@ class PLATFORM_EXPORT Canvas2DLayerBridge : public cc::TextureLayerClient { ...@@ -255,8 +255,6 @@ class PLATFORM_EXPORT Canvas2DLayerBridge : public cc::TextureLayerClient {
sk_sp<cc::PaintRecord> last_recording_; sk_sp<cc::PaintRecord> last_recording_;
base::RepeatingClosure finalize_frame_callback_;
base::WeakPtrFactory<Canvas2DLayerBridge> weak_ptr_factory_{this}; base::WeakPtrFactory<Canvas2DLayerBridge> weak_ptr_factory_{this};
DISALLOW_COPY_AND_ASSIGN(Canvas2DLayerBridge); DISALLOW_COPY_AND_ASSIGN(Canvas2DLayerBridge);
......
// Copyright 2019 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.
#include "third_party/blink/renderer/platform/graphics/memory_managed_paint_canvas.h"
namespace blink {
MemoryManagedPaintCanvas::MemoryManagedPaintCanvas(
cc::DisplayItemList* list,
const SkRect& bounds,
base::RepeatingClosure finalize_frame_callback)
: RecordPaintCanvas(list, bounds),
finalize_frame_callback_(std::move(finalize_frame_callback)) {}
MemoryManagedPaintCanvas::~MemoryManagedPaintCanvas() = default;
void MemoryManagedPaintCanvas::drawImage(const cc::PaintImage& image,
SkScalar left,
SkScalar top,
const cc::PaintFlags* flags) {
DCHECK(!image.IsPaintWorklet());
RecordPaintCanvas::drawImage(image, left, top, flags);
UpdateMemoryUsage(image);
}
void MemoryManagedPaintCanvas::drawImageRect(
const cc::PaintImage& image,
const SkRect& src,
const SkRect& dst,
const cc::PaintFlags* flags,
PaintCanvas::SrcRectConstraint constraint) {
RecordPaintCanvas::drawImageRect(image, src, dst, flags, constraint);
UpdateMemoryUsage(image);
}
void MemoryManagedPaintCanvas::UpdateMemoryUsage(const cc::PaintImage& image) {
if (cached_image_ids_.contains(image.content_id()))
return;
cached_image_ids_.insert(image.content_id());
total_stored_image_memory_ +=
image.GetSkImage()->imageInfo().computeMinByteSize();
if (total_stored_image_memory_ > kMaxPinnedMemory)
finalize_frame_callback_.Run();
}
} // namespace blink
// Copyright 2019 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.
#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_MEMORY_MANAGED_PAINT_CANVAS_H_
#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_MEMORY_MANAGED_PAINT_CANVAS_H_
#include <memory>
#include "cc/paint/record_paint_canvas.h"
#include "third_party/blink/public/platform/platform.h"
namespace blink {
// MemoryManagedPaintCanvas overrides the potentially memory intensive image
// drawing methods of PaintCanvas and keeps track of how much memory is
// being pinned between flushes. This allows the rendering context to flush if
// too much memory is used.
class PLATFORM_EXPORT MemoryManagedPaintCanvas final
: public cc::RecordPaintCanvas {
public:
MemoryManagedPaintCanvas(cc::DisplayItemList* list,
const SkRect& bounds,
base::RepeatingClosure finalize_frame_callback);
explicit MemoryManagedPaintCanvas(const cc::RecordPaintCanvas&) = delete;
~MemoryManagedPaintCanvas() override;
void drawImage(const cc::PaintImage& image,
SkScalar left,
SkScalar top,
const cc::PaintFlags* flags) override;
void drawImageRect(const cc::PaintImage& image,
const SkRect& src,
const SkRect& dst,
const cc::PaintFlags* flags,
SrcRectConstraint constraint) override;
private:
void UpdateMemoryUsage(const cc::PaintImage& image);
base::flat_set<int> cached_image_ids_;
uint64_t total_stored_image_memory_ = 0;
base::RepeatingClosure finalize_frame_callback_;
// The same value as is used in content::WebGraphicsConext3DProviderImpl.
static constexpr uint64_t kMaxPinnedMemory = 64 * 1024 * 1024;
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_MEMORY_MANAGED_PAINT_CANVAS_H_
/*
* Copyright (C) 2019 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/platform/graphics/memory_managed_paint_recorder.h"
namespace blink {
MemoryManagedPaintRecorder::MemoryManagedPaintRecorder(
base::RepeatingClosure finalize_frame_callback)
: finalize_frame_callback_(std::move(finalize_frame_callback)) {}
std::unique_ptr<cc::RecordPaintCanvas> MemoryManagedPaintRecorder::CreateCanvas(
cc::DisplayItemList* list,
const SkRect& bounds) {
return std::make_unique<MemoryManagedPaintCanvas>(list, bounds,
finalize_frame_callback_);
}
} // namespace blink
/*
* Copyright (C) 2019 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_MEMORY_MANAGED_PAINT_RECORDER_H_
#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_MEMORY_MANAGED_PAINT_RECORDER_H_
#include "cc/paint/paint_recorder.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/platform/graphics/memory_managed_paint_canvas.h"
namespace blink {
class PLATFORM_EXPORT MemoryManagedPaintRecorder : public cc::PaintRecorder {
public:
MemoryManagedPaintRecorder(base::RepeatingClosure finalize_frame_callback);
protected:
std::unique_ptr<cc::RecordPaintCanvas> CreateCanvas(
cc::DisplayItemList* list,
const SkRect& bounds) override;
private:
base::RepeatingClosure finalize_frame_callback_;
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_MEMORY_MANAGED_PAINT_RECORDER_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment