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
+          """))