From 7ed28478d97abde16ef62c5c35c3e01fffc93370 Mon Sep 17 00:00:00 2001 From: liyuqian Date: Tue, 12 Mar 2019 14:10:11 -0700 Subject: [PATCH] Reland PerformanceOverlayLayer golden test (flutter/engine#8140) This reverts commit c416fe99ee0ddb464ca85c0b336d679ea626a03c. Now we shouldn't break the engine build as https://chromium-review.googlesource.com/c/chromium/tools/build/+/1480746 is landed. The golden test is disabled by default and we'll enable it later in our recipe and test it in presubmit tests. --- .../ci/licenses_golden/licenses_flutter | 5 + engine/src/flutter/flow/BUILD.gn | 7 +- .../flutter/flow/flow_run_all_unittests.cc | 32 ++++++ engine/src/flutter/flow/flow_test_utils.cc | 40 ++++++++ engine/src/flutter/flow/flow_test_utils.h | 29 ++++++ .../flow/layers/performance_overlay_layer.cc | 31 ++++-- .../flow/layers/performance_overlay_layer.h | 6 +- .../performance_overlay_layer_unittests.cc | 96 ++++++++++++++++++ .../resources/performance_overlay_gold.png | Bin 0 -> 16572 bytes 9 files changed, 234 insertions(+), 12 deletions(-) create mode 100644 engine/src/flutter/flow/flow_run_all_unittests.cc create mode 100644 engine/src/flutter/flow/flow_test_utils.cc create mode 100644 engine/src/flutter/flow/flow_test_utils.h create mode 100644 engine/src/flutter/flow/layers/performance_overlay_layer_unittests.cc create mode 100644 engine/src/flutter/testing/resources/performance_overlay_gold.png diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index a73eae28ef..7f1d66782c 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -88,6 +88,7 @@ FILE: ../../../flutter/flow/layers/opacity_layer.cc FILE: ../../../flutter/flow/layers/opacity_layer.h FILE: ../../../flutter/flow/layers/performance_overlay_layer.cc FILE: ../../../flutter/flow/layers/performance_overlay_layer.h +FILE: ../../../flutter/flow/layers/performance_overlay_layer_unittests.cc FILE: ../../../flutter/flow/layers/physical_shape_layer.cc FILE: ../../../flutter/flow/layers/physical_shape_layer.h FILE: ../../../flutter/flow/layers/picture_layer.cc @@ -733,9 +734,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== ==================================================================================================== +LIBRARY: engine LIBRARY: txt ORIGIN: ../../../flutter/third_party/txt/LICENSE TYPE: LicenseType.apache +FILE: ../../../flutter/flow/flow_run_all_unittests.cc +FILE: ../../../flutter/flow/flow_test_utils.cc +FILE: ../../../flutter/flow/flow_test_utils.h FILE: ../../../flutter/third_party/txt/benchmarks/paint_record_benchmarks.cc FILE: ../../../flutter/third_party/txt/benchmarks/paragraph_benchmarks.cc FILE: ../../../flutter/third_party/txt/benchmarks/paragraph_builder_benchmarks.cc diff --git a/engine/src/flutter/flow/BUILD.gn b/engine/src/flutter/flow/BUILD.gn index 6b345aa87a..fbbf0e49db 100644 --- a/engine/src/flutter/flow/BUILD.gn +++ b/engine/src/flutter/flow/BUILD.gn @@ -93,13 +93,18 @@ executable("flow_unittests") { testonly = true sources = [ + "flow_run_all_unittests.cc", + "flow_test_utils.h", + "flow_test_utils.cc", "matrix_decomposition_unittests.cc", "raster_cache_unittests.cc", + "layers/performance_overlay_layer_unittests.cc", ] deps = [ ":flow", - "$flutter_root/testing", + "$flutter_root/fml", + "//third_party/googletest:gtest", "//third_party/dart/runtime:libdart_jit", # for tracing "//third_party/skia", ] diff --git a/engine/src/flutter/flow/flow_run_all_unittests.cc b/engine/src/flutter/flow/flow_run_all_unittests.cc new file mode 100644 index 0000000000..c1755e639b --- /dev/null +++ b/engine/src/flutter/flow/flow_run_all_unittests.cc @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "flutter/fml/command_line.h" +#include "flutter/fml/logging.h" +#include "gtest/gtest.h" + +#include "flow_test_utils.h" + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + fml::CommandLine cmd = fml::CommandLineFromArgcArgv(argc, argv); + flow::SetGoldenDir( + cmd.GetOptionValueWithDefault("golden-dir", "flutter/testing/resources")); + flow::SetFontFile(cmd.GetOptionValueWithDefault( + "font-file", + "flutter/third_party/txt/third_party/fonts/Roboto-Regular.ttf")); + return RUN_ALL_TESTS(); +} diff --git a/engine/src/flutter/flow/flow_test_utils.cc b/engine/src/flutter/flow/flow_test_utils.cc new file mode 100644 index 0000000000..0bf6d4c31d --- /dev/null +++ b/engine/src/flutter/flow/flow_test_utils.cc @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +namespace flow { + +static std::string gGoldenDir; +static std::string gFontFile; + +const std::string& GetGoldenDir() { + return gGoldenDir; +} + +void SetGoldenDir(const std::string& dir) { + gGoldenDir = dir; +} + +const std::string& GetFontFile() { + return gFontFile; +} + +void SetFontFile(const std::string& file) { + gFontFile = file; +} + +} // namespace flow diff --git a/engine/src/flutter/flow/flow_test_utils.h b/engine/src/flutter/flow/flow_test_utils.h new file mode 100644 index 0000000000..58da75b3f4 --- /dev/null +++ b/engine/src/flutter/flow/flow_test_utils.h @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +namespace flow { + +const std::string& GetGoldenDir(); + +void SetGoldenDir(const std::string& dir); + +const std::string& GetFontFile(); + +void SetFontFile(const std::string& dir); + +} // namespace flow diff --git a/engine/src/flutter/flow/layers/performance_overlay_layer.cc b/engine/src/flutter/flow/layers/performance_overlay_layer.cc index c80786e866..e7f40058c3 100644 --- a/engine/src/flutter/flow/layers/performance_overlay_layer.cc +++ b/engine/src/flutter/flow/layers/performance_overlay_layer.cc @@ -15,8 +15,12 @@ namespace { void DrawStatisticsText(SkCanvas& canvas, const std::string& string, int x, - int y) { + int y, + const std::string& font_path) { SkFont font; + if (font_path != "") { + font = SkFont(SkTypeface::MakeFromFile(font_path.c_str())); + } font.setSize(15); font.setLinearMetrics(false); SkPaint paint; @@ -33,7 +37,8 @@ void VisualizeStopWatch(SkCanvas& canvas, SkScalar height, bool show_graph, bool show_labels, - const std::string& label_prefix) { + const std::string& label_prefix, + const std::string& font_path) { const int label_x = 8; // distance from x const int label_y = -10; // distance from y+height @@ -51,14 +56,20 @@ void VisualizeStopWatch(SkCanvas& canvas, stream << label_prefix << " " << "max " << max_ms_per_frame << " ms/frame, " << "avg " << average_ms_per_frame << " ms/frame"; - DrawStatisticsText(canvas, stream.str(), x + label_x, y + height + label_y); + DrawStatisticsText(canvas, stream.str(), x + label_x, y + height + label_y, + font_path); } } } // namespace -PerformanceOverlayLayer::PerformanceOverlayLayer(uint64_t options) - : options_(options) {} +PerformanceOverlayLayer::PerformanceOverlayLayer(uint64_t options, + const char* font_path) + : options_(options) { + if (font_path != nullptr) { + font_path_ = font_path; + } +} void PerformanceOverlayLayer::Paint(PaintContext& context) const { const int padding = 8; @@ -73,15 +84,15 @@ void PerformanceOverlayLayer::Paint(PaintContext& context) const { SkScalar height = paint_bounds().height() / 2; SkAutoCanvasRestore save(context.leaf_nodes_canvas, true); - VisualizeStopWatch(*context.leaf_nodes_canvas, context.frame_time, x, y, - width, height - padding, - options_ & kVisualizeRasterizerStatistics, - options_ & kDisplayRasterizerStatistics, "GPU"); + VisualizeStopWatch( + *context.leaf_nodes_canvas, context.frame_time, x, y, width, + height - padding, options_ & kVisualizeRasterizerStatistics, + options_ & kDisplayRasterizerStatistics, "GPU", font_path_); VisualizeStopWatch(*context.leaf_nodes_canvas, context.engine_time, x, y + height, width, height - padding, options_ & kVisualizeEngineStatistics, - options_ & kDisplayEngineStatistics, "UI"); + options_ & kDisplayEngineStatistics, "UI", font_path_); } } // namespace flow diff --git a/engine/src/flutter/flow/layers/performance_overlay_layer.h b/engine/src/flutter/flow/layers/performance_overlay_layer.h index b5f20ecbd7..a47b836c49 100644 --- a/engine/src/flutter/flow/layers/performance_overlay_layer.h +++ b/engine/src/flutter/flow/layers/performance_overlay_layer.h @@ -5,6 +5,8 @@ #ifndef FLUTTER_FLOW_LAYERS_PERFORMANCE_OVERLAY_LAYER_H_ #define FLUTTER_FLOW_LAYERS_PERFORMANCE_OVERLAY_LAYER_H_ +#include + #include "flutter/flow/layers/layer.h" #include "flutter/fml/macros.h" @@ -17,12 +19,14 @@ const int kVisualizeEngineStatistics = 1 << 3; class PerformanceOverlayLayer : public Layer { public: - explicit PerformanceOverlayLayer(uint64_t options); + explicit PerformanceOverlayLayer(uint64_t options, + const char* font_path = nullptr); void Paint(PaintContext& context) const override; private: int options_; + std::string font_path_; FML_DISALLOW_COPY_AND_ASSIGN(PerformanceOverlayLayer); }; diff --git a/engine/src/flutter/flow/layers/performance_overlay_layer_unittests.cc b/engine/src/flutter/flow/layers/performance_overlay_layer_unittests.cc new file mode 100644 index 0000000000..cee659f925 --- /dev/null +++ b/engine/src/flutter/flow/layers/performance_overlay_layer_unittests.cc @@ -0,0 +1,96 @@ +// Copyright 2013 The Flutter 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 "flutter/flow/flow_test_utils.h" +#include "flutter/flow/layers/performance_overlay_layer.h" +#include "flutter/flow/raster_cache.h" + +#include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/utils/SkBase64.h" + +#include "gtest/gtest.h" + +// To get the size of kMockedTimes in compile time. +template +constexpr int size(const T (&array)[N]) noexcept { + return N; +} + +constexpr int kMockedTimes[] = {17, 1, 4, 24, 4, 25, 30, 4, 13, 34, + 14, 0, 18, 9, 32, 36, 26, 23, 5, 8, + 32, 18, 29, 16, 29, 18, 0, 36, 33, 10}; + +// Relative to the flutter/src/engine/flutter directory +const char* kGoldenFileName = "performance_overlay_gold.png"; + +// Relative to the flutter/src/engine/flutter directory +const char* kNewGoldenFileName = "performance_overlay_gold_new.png"; + +TEST(PerformanceOverlayLayer, Gold) { + const std::string& golden_dir = flow::GetGoldenDir(); + // This unit test should only be run on Linux (not even on Mac since it's a + // golden test). Hence we don't have to worry about the "/" vs. "\". + std::string golden_file_path = golden_dir + "/" + kGoldenFileName; + std::string new_golden_file_path = golden_dir + "/" + kNewGoldenFileName; + + flow::Stopwatch mock_stopwatch; + for (int i = 0; i < size(kMockedTimes); ++i) { + mock_stopwatch.SetLapTime( + fml::TimeDelta::FromMilliseconds(kMockedTimes[i])); + } + + const SkImageInfo image_info = SkImageInfo::MakeN32Premul(1000, 1000); + sk_sp surface = SkSurface::MakeRaster(image_info); + + ASSERT_TRUE(surface != nullptr); + + flow::TextureRegistry unused_texture_registry; + + flow::Layer::PaintContext paintContext = { + nullptr, surface->getCanvas(), nullptr, mock_stopwatch, + mock_stopwatch, unused_texture_registry, nullptr, false}; + + // Specify font file to ensure the same font across different operation + // systems. + flow::PerformanceOverlayLayer layer(flow::kDisplayRasterizerStatistics | + flow::kVisualizeRasterizerStatistics | + flow::kDisplayEngineStatistics | + flow::kVisualizeEngineStatistics, + flow::GetFontFile().c_str()); + layer.set_paint_bounds(SkRect::MakeWH(1000, 400)); + surface->getCanvas()->clear(SK_ColorTRANSPARENT); + layer.Paint(paintContext); + + sk_sp snapshot = surface->makeImageSnapshot(); + sk_sp snapshot_data = snapshot->encodeToData(); + + sk_sp golden_data = + SkData::MakeFromFileName(golden_file_path.c_str()); + EXPECT_TRUE(golden_data != nullptr) + << "Golden file not found: " << golden_file_path << ".\n" + << "Please either set --golden-dir, or make sure that the unit test is " + << "run from the right directory (e.g., flutter/engine/src)."; + + const bool golden_data_matches = golden_data->equals(snapshot_data.get()); + if (!golden_data_matches) { + SkFILEWStream wstream(new_golden_file_path.c_str()); + wstream.write(snapshot_data->data(), snapshot_data->size()); + wstream.flush(); + + size_t b64_size = + SkBase64::Encode(snapshot_data->data(), snapshot_data->size(), nullptr); + sk_sp b64_data = SkData::MakeUninitialized(b64_size + 1); + char* b64_char = static_cast(b64_data->writable_data()); + SkBase64::Encode(snapshot_data->data(), snapshot_data->size(), b64_char); + b64_char[b64_size] = 0; // make it null terminated for printing + + EXPECT_TRUE(golden_data_matches) + << "Golden file mismatch. Please check " + << "the difference between " << kGoldenFileName << " and " + << kNewGoldenFileName << ", and replace the former " + << "with the latter if the difference looks good.\n\n" + << "See also the base64 encoded " << kNewGoldenFileName << ":\n" + << b64_char; + } +} diff --git a/engine/src/flutter/testing/resources/performance_overlay_gold.png b/engine/src/flutter/testing/resources/performance_overlay_gold.png new file mode 100644 index 0000000000000000000000000000000000000000..119551f705793600890950be96b6b0d60688cc54 GIT binary patch literal 16572 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