tp: Dump the hierarchy path for SF layers. (#2169)
So we can avoid running slow recursive queries for trace search. Ensure
we handle recursive hierarchies.
Bug: 411363817
Bug: 431939202
Test: tools/diff_test_trace_processor.py
out/linux_clang_release/trace_processor_shell
--name-filter="SurfaceFlingerLayer|PerfettoTable:winscope"
diff --git a/Android.bp b/Android.bp
index b6d62fe..2154eff 100644
--- a/Android.bp
+++ b/Android.bp
@@ -14879,6 +14879,7 @@
"src/trace_processor/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc",
+ "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.cc",
],
}
diff --git a/BUILD b/BUILD
index e662182..ece3be6 100644
--- a/BUILD
+++ b/BUILD
@@ -2881,6 +2881,8 @@
"src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.h",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h",
+ "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.cc",
+ "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.h",
],
)
diff --git a/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.cc b/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.cc
index b9dfdfe..3f27b0b 100644
--- a/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.cc
+++ b/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.cc
@@ -178,21 +178,23 @@
visibility,
const std::unordered_map<int32_t, LayerDecoder>& layers_by_id,
const surfaceflinger_layers::SurfaceFlingerRects& rects) {
+ auto* string_pool =
+ context_->trace_processor_context_->storage->mutable_string_pool();
+
tables::SurfaceFlingerLayerTable::Row layer;
layer.snapshot_id = snapshot_id;
- layer.base64_proto_id =
- context_->trace_processor_context_->storage->mutable_string_pool()
- ->InternString(
- base::StringView(base::Base64Encode(blob.data, blob.size)))
- .raw_id();
+ layer.base64_proto_id = string_pool
+ ->InternString(base::StringView(
+ base::Base64Encode(blob.data, blob.size)))
+ .raw_id();
LayerDecoder layer_decoder(blob);
if (layer_decoder.has_id()) {
layer.layer_id = layer_decoder.id();
}
+
if (layer_decoder.has_name()) {
layer.layer_name =
- context_->trace_processor_context_->storage->mutable_string_pool()
- ->InternString(base::StringView(layer_decoder.name()));
+ string_pool->InternString(base::StringView(layer_decoder.name()));
}
if (layer_decoder.has_parent()) {
layer.parent = layer_decoder.parent();
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
index 24d131a..201096e 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
@@ -46,6 +46,8 @@
sources += [
"winscope_proto_to_args_with_defaults.cc",
"winscope_proto_to_args_with_defaults.h",
+ "winscope_surfaceflinger_hierarchy_paths.cc",
+ "winscope_surfaceflinger_hierarchy_paths.h",
]
}
deps = [
@@ -70,7 +72,11 @@
"../../engine",
]
if (enable_perfetto_winscope) {
- deps += [ "../../../util:winscope_proto_mapping" ]
+ deps += [
+ "../../../../../protos/perfetto/trace/android:winscope_regular_zero",
+ "../../../importers/proto/winscope:full",
+ "../../../util:winscope_proto_mapping",
+ ]
}
public_deps = [ ":interface" ]
}
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
index 4e6941c..95bf4a1 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
@@ -28,6 +28,7 @@
from src.trace_processor.tables.profiler_tables import STACK_PROFILE_FRAME_TABLE
from src.trace_processor.tables.slice_tables import SLICE_TABLE
from src.trace_processor.tables.track_tables import TRACK_TABLE
+from src.trace_processor.tables.winscope_tables import SURFACE_FLINGER_LAYERS_SNAPSHOT_TABLE
TABLE_INFO_TABLE = Table(
python_module=__file__,
@@ -182,6 +183,17 @@
],
)
+SURFACE_FLINGER_HIERARCHY_PATH_TABLE = Table(
+ python_module=__file__,
+ class_name="WinscopeSurfaceFlingerHierarchyPathTable",
+ sql_name="__intrinsic_winscope_surfaceflinger_hierarchy_path",
+ columns=[
+ C('snapshot_id', CppUint32()),
+ C('layer_id', CppUint32()),
+ C('ancestor_id', CppUint32()),
+ ],
+)
+
# Keep this list sorted.
ALL_TABLES = [
ANCESTOR_STACK_PROFILE_CALLSITE_TABLE,
@@ -192,5 +204,6 @@
EXPERIMENTAL_ANNOTATED_CALLSTACK_TABLE,
EXPERIMENTAL_SLICE_LAYOUT_TABLE,
SLICE_SUBSET_TABLE,
+ SURFACE_FLINGER_HIERARCHY_PATH_TABLE,
TABLE_INFO_TABLE,
]
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.cc
new file mode 100644
index 0000000..b46a38e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.cc
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * 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 "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.h"
+#include <sys/types.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/base64.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/field.h"
+#include "perfetto/trace_processor/basic_types.h"
+#include "protos/perfetto/trace/android/surfaceflinger_layers.pbzero.h"
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/dataframe/dataframe.h"
+#include "src/trace_processor/dataframe/specs.h"
+#include "src/trace_processor/dataframe/typed_cursor.h"
+#include "src/trace_processor/importers/proto/winscope/surfaceflinger_layers_extractor.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/tables_py.h"
+#include "src/trace_processor/tables/winscope_tables_py.h"
+
+namespace perfetto::trace_processor {
+
+namespace {
+using LayerDecoder = protos::pbzero::LayerProto::Decoder;
+
+std::vector<int32_t> GetHierarchyPath(
+ const LayerDecoder& layer_decoder,
+ const std::unordered_map<int32_t, LayerDecoder>& layers_by_id) {
+ std::vector<int32_t> hierarchy_path = {layer_decoder.id()};
+ auto pos = layers_by_id.find(layer_decoder.parent());
+ while (pos != layers_by_id.end()) {
+ const auto& layer_dec = pos->second;
+ auto id = layer_dec.id();
+ auto parent = layer_dec.parent();
+ if (id == parent) {
+ // handles recursive hierarchies
+ break;
+ }
+ hierarchy_path.push_back(id);
+ pos = layers_by_id.find(parent);
+ }
+ return hierarchy_path;
+}
+
+base::Status InsertRows(
+ const dataframe::Dataframe& snapshot_table,
+ tables::WinscopeSurfaceFlingerHierarchyPathTable* paths_table,
+ StringPool* string_pool) {
+ constexpr auto kSpec = tables::SurfaceFlingerLayersSnapshotTable::kSpec;
+
+ for (uint32_t i = 0; i < snapshot_table.row_count(); ++i) {
+ auto base64_proto_id =
+ snapshot_table
+ .GetCellUnchecked<tables::SurfaceFlingerLayersSnapshotTable::
+ ColumnIndex::base64_proto_id>(kSpec, i);
+ PERFETTO_CHECK(base64_proto_id.has_value());
+
+ const auto raw_proto =
+ string_pool->Get(StringPool::Id::Raw(*base64_proto_id));
+ const auto blob = *base::Base64Decode(raw_proto);
+ const auto cb = protozero::ConstBytes{
+ reinterpret_cast<const uint8_t*>(blob.data()), blob.size()};
+ protos::pbzero::LayersSnapshotProto::Decoder snapshot(cb);
+ protos::pbzero::LayersProto::Decoder layers(snapshot.layers());
+
+ const auto& layers_by_id =
+ winscope::surfaceflinger_layers::ExtractLayersById(layers);
+
+ for (auto it = layers.layers(); it; ++it) {
+ LayerDecoder layer(*it);
+ if (!layer.has_id()) {
+ continue;
+ }
+ auto layer_id = static_cast<uint32_t>(layer.id());
+
+ auto path = GetHierarchyPath(layer, layers_by_id);
+ for (auto path_it = path.rbegin(); path_it != path.rend(); ++path_it) {
+ tables::WinscopeSurfaceFlingerHierarchyPathTable::Row row;
+ row.snapshot_id = i;
+ row.layer_id = layer_id;
+ row.ancestor_id = static_cast<uint32_t>(*path_it);
+ paths_table->Insert(row);
+ }
+ }
+ }
+ return base::OkStatus();
+}
+} // namespace
+
+WinscopeSurfaceFlingerHierarchyPaths::Cursor::Cursor(
+ StringPool* string_pool,
+ const PerfettoSqlEngine* engine)
+ : string_pool_(string_pool), engine_(engine), table_(string_pool) {}
+
+bool WinscopeSurfaceFlingerHierarchyPaths::Cursor::Run(
+ const std::vector<SqlValue>& arguments) {
+ PERFETTO_DCHECK(arguments.size() == 0);
+ auto table_name = tables::SurfaceFlingerLayersSnapshotTable::Name();
+ const dataframe::Dataframe* static_table_from_engine =
+ engine_->GetDataframeOrNull(table_name);
+ if (!static_table_from_engine) {
+ return OnFailure(base::ErrStatus("Failed to find %s table.",
+ std::string(table_name).c_str()));
+ }
+
+ table_.Clear();
+
+ base::Status status =
+ InsertRows(*static_table_from_engine, &table_, string_pool_);
+ if (!status.ok()) {
+ return OnFailure(status);
+ }
+ return OnSuccess(&table_.dataframe());
+}
+
+WinscopeSurfaceFlingerHierarchyPaths::WinscopeSurfaceFlingerHierarchyPaths(
+ StringPool* string_pool,
+ const PerfettoSqlEngine* engine)
+ : string_pool_(string_pool), engine_(engine) {}
+
+std::unique_ptr<StaticTableFunction::Cursor>
+WinscopeSurfaceFlingerHierarchyPaths::MakeCursor() {
+ return std::make_unique<Cursor>(string_pool_, engine_);
+}
+
+dataframe::DataframeSpec WinscopeSurfaceFlingerHierarchyPaths::CreateSpec() {
+ return tables::WinscopeSurfaceFlingerHierarchyPathTable::kSpec
+ .ToUntypedDataframeSpec();
+}
+
+std::string WinscopeSurfaceFlingerHierarchyPaths::TableName() {
+ return tables::WinscopeSurfaceFlingerHierarchyPathTable::Name();
+}
+
+uint32_t WinscopeSurfaceFlingerHierarchyPaths::GetArgumentCount() const {
+ return 0;
+}
+uint32_t WinscopeSurfaceFlingerHierarchyPaths::EstimateRowCount() {
+ // 1 path per 100 elements per 100 entries
+ return 10000;
+}
+} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.h b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.h
new file mode 100644
index 0000000..b13de9b
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_WINSCOPE_SURFACEFLINGER_HIERARCHY_PATHS_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_WINSCOPE_SURFACEFLINGER_HIERARCHY_PATHS_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/dataframe/specs.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/tables_py.h"
+
+namespace perfetto::trace_processor {
+
+class TraceProcessorContext;
+
+class WinscopeSurfaceFlingerHierarchyPaths : public StaticTableFunction {
+ public:
+ class Cursor : public StaticTableFunction::Cursor {
+ public:
+ explicit Cursor(StringPool* string_pool, const PerfettoSqlEngine* engine);
+ bool Run(const std::vector<SqlValue>& arguments) override;
+
+ private:
+ StringPool* string_pool_ = nullptr;
+ const PerfettoSqlEngine* engine_ = nullptr;
+ tables::WinscopeSurfaceFlingerHierarchyPathTable table_;
+ };
+
+ explicit WinscopeSurfaceFlingerHierarchyPaths(StringPool*,
+ const PerfettoSqlEngine*);
+
+ std::unique_ptr<StaticTableFunction::Cursor> MakeCursor() override;
+ dataframe::DataframeSpec CreateSpec() override;
+ std::string TableName() override;
+ uint32_t GetArgumentCount() const override;
+ uint32_t EstimateRowCount() override;
+
+ private:
+ StringPool* string_pool_ = nullptr;
+ const PerfettoSqlEngine* engine_ = nullptr;
+};
+
+} // namespace perfetto::trace_processor
+
+#endif // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_WINSCOPE_SURFACEFLINGER_HIERARCHY_PATHS_H_
diff --git a/src/trace_processor/tables/winscope_tables.py b/src/trace_processor/tables/winscope_tables.py
index 74e222b..7448286 100644
--- a/src/trace_processor/tables/winscope_tables.py
+++ b/src/trace_processor/tables/winscope_tables.py
@@ -275,7 +275,7 @@
C('z_order_relative_of', CppOptional(CppInt64())),
C('is_missing_z_parent', CppInt64()),
C('layer_rect_id', CppOptional(CppTableId(WINSCOPE_TRACE_RECT_TABLE))),
- C('input_rect_id', CppOptional(CppTableId(WINSCOPE_TRACE_RECT_TABLE)))
+ C('input_rect_id', CppOptional(CppTableId(WINSCOPE_TRACE_RECT_TABLE))),
],
tabledoc=TableDoc(
doc='SurfaceFlinger layer',
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index ccacb02..80bc67a 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -123,7 +123,6 @@
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h"
#include "src/trace_processor/perfetto_sql/stdlib/stdlib.h"
#include "src/trace_processor/sqlite/bindings/sqlite_aggregate_function.h"
#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
@@ -165,6 +164,7 @@
#if PERFETTO_BUILDFLAG(PERFETTO_ENABLE_WINSCOPE)
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_surfaceflinger_hierarchy_paths.h"
#endif
namespace perfetto::trace_processor {
@@ -1156,6 +1156,8 @@
#if PERFETTO_BUILDFLAG(PERFETTO_ENABLE_WINSCOPE)
fns.emplace_back(std::make_unique<WinscopeProtoToArgsWithDefaults>(
storage->mutable_string_pool(), engine, context));
+ fns.emplace_back(std::make_unique<WinscopeSurfaceFlingerHierarchyPaths>(
+ storage->mutable_string_pool(), engine));
#endif
if (config.enable_dev_features) {
diff --git a/test/trace_processor/diff_tests/parser/android/surfaceflinger_layers.textproto b/test/trace_processor/diff_tests/parser/android/surfaceflinger_layers.textproto
index b8ba94d..18c1072 100644
--- a/test/trace_processor/diff_tests/parser/android/surfaceflinger_layers.textproto
+++ b/test/trace_processor/diff_tests/parser/android/surfaceflinger_layers.textproto
@@ -289,6 +289,7 @@
excludes_composition_state: true
layers {
layers { parent: -1 }
+ layers { id: -2 parent: -2 }
layers {
id: 1
name: "layer1"
diff --git a/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_layers.py b/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_layers.py
index bc3b17c..0048001 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_layers.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_layers.py
@@ -121,7 +121,7 @@
input_rect_id
FROM
surfaceflinger_layer
- LIMIT 7;
+ LIMIT 8;
""",
out=Csv("""
"id","snapshot_id","layer_id","layer_name","parent","corner_radius_tl","corner_radius_tr","corner_radius_bl","corner_radius_br","hwc_composition_type","z_order_relative_of","is_missing_z_parent","is_visible","layer_rect_id","input_rect_id"
@@ -130,8 +130,9 @@
2,1,3,"Display 0 name="Built-in Screen"#3","[NULL]",0.000000,0.000000,0.000000,0.000000,"[NULL]","[NULL]",0,0,"[NULL]","[NULL]"
3,1,4,"WindowedMagnification:0:31#4",3,0.000000,0.000000,0.000000,0.000000,"[NULL]","[NULL]",0,0,3,"[NULL]"
4,2,"[NULL]","[NULL]",-1,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",0,0,"[NULL]","[NULL]"
- 5,2,1,"layer1","[NULL]",1.000000,1.000000,1.000000,1.000000,2,"[NULL]",0,0,9,10
- 6,2,2,"layer2","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",0,1,11,12
+ 5,2,-2,"[NULL]",-2,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",0,0,"[NULL]","[NULL]"
+ 6,2,1,"layer1","[NULL]",1.000000,1.000000,1.000000,1.000000,2,"[NULL]",0,0,9,10
+ 7,2,2,"layer2","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",0,1,11,12
"""))
def test_tables_have_raw_protos(self):
@@ -147,7 +148,7 @@
out=Csv("""
"COUNT(*)"
3
- 19
+ 20
"""))
def test_layer_args(self):
@@ -158,7 +159,7 @@
args.key, args.display_value
FROM
surfaceflinger_layer AS sfl JOIN args ON sfl.arg_set_id = args.arg_set_id
- WHERE sfl.id = 2 and (key like "screen_bounds%" or key like "visibility_reason%")
+ WHERE sfl.id = 2 AND (key GLOB "screen_bounds*" OR key GLOB "visibility_reason*")
ORDER BY args.key
""",
out=Csv("""
diff --git a/test/trace_processor/diff_tests/syntax/table_tests.py b/test/trace_processor/diff_tests/syntax/table_tests.py
index 82d9b50..9606c54 100644
--- a/test/trace_processor/diff_tests/syntax/table_tests.py
+++ b/test/trace_processor/diff_tests/syntax/table_tests.py
@@ -708,3 +708,25 @@
"class_name_iid","class_name_iid",3,"[NULL]"
"view_id","view_id","[NULL]","NO_ID"
"""))
+
+ def test_winscope_surfaceflinger_hierarchy_paths(self):
+ return DiffTestBlueprint(
+ trace=Path('../parser/android/surfaceflinger_layers.textproto'),
+ query="""
+ SELECT * FROM __intrinsic_winscope_surfaceflinger_hierarchy_path() as tbl
+ ORDER BY tbl.id
+ LIMIT 10
+ """,
+ out=Csv("""
+ "id","snapshot_id","layer_id","ancestor_id"
+ 0,0,3,3
+ 1,0,4,3
+ 2,0,4,4
+ 3,1,3,3
+ 4,1,4,3
+ 5,1,4,4
+ 6,2,4294967294,4294967294
+ 7,2,1,1
+ 8,2,2,2
+ 9,2,3,3
+ """))