scene/storage.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | /// @file storage.cpp | ||
| 3 | /// @brief Buckets, rows, versions and change sets of the columnar prim store. | ||
| 4 | #include "ernie/scene/storage.h" | ||
| 5 | |||
| 6 | #include "ernie/util/random.h" | ||
| 7 | |||
| 8 | #include <algorithm> | ||
| 9 | |||
| 10 | namespace ernie { | ||
| 11 | |||
| 12 | namespace { | ||
| 13 | |||
| 14 | constexpr std::uint32_t kNoSlot = std::numeric_limits<std::uint32_t>::max(); | ||
| 15 | /// Row changes kept per bucket; a reader further behind resyncs the bucket. | ||
| 16 | constexpr std::size_t kLogLimit = std::size_t{1} << 16; | ||
| 17 | |||
| 18 | } // namespace | ||
| 19 | |||
| 20 | 4018 | std::size_t PrimStore::KeyHash::operator()(const BucketKey& key) const { | |
| 21 | 4018 | std::uint64_t hash = Mix64(key.type); | |
| 22 | 4270 | for (const Token schema : key.apiSchemas) { | |
| 23 | 252 | hash = HashCombine(hash, schema); | |
| 24 | } | ||
| 25 | 4018 | return static_cast<std::size_t>(hash); | |
| 26 | } | ||
| 27 | |||
| 28 | 3767 | BucketKey BucketKey::Normalized() const { | |
| 29 | 3767 | BucketKey sorted = *this; | |
| 30 | 3767 | std::ranges::sort(sorted.apiSchemas); | |
| 31 | 3767 | const auto [last, end] = std::ranges::unique(sorted.apiSchemas); | |
| 32 | 3767 | sorted.apiSchemas.erase(last, end); | |
| 33 | 3767 | return sorted; | |
| 34 | } | ||
| 35 | |||
| 36 | 3039 | std::optional<BucketId> PrimStore::FindBucket(const BucketKey& key) const { | |
| 37 | 6078 | const auto found = bucketIndex_.find(key.Normalized()); | |
| 38 | 5363 | return found != bucketIndex_.end() ? std::optional(found->second) : std::nullopt; | |
| 39 | } | ||
| 40 | |||
| 41 | 728 | BucketId PrimStore::AddBucket(const BucketKey& key, std::span<const ColumnLayout> layout) { | |
| 42 | 728 | BucketKey sorted = key.Normalized(); | |
| 43 | 728 | if (const auto found = bucketIndex_.find(sorted); found != bucketIndex_.end()) { | |
| 44 | 2 | return found->second; | |
| 45 | } | ||
| 46 | 726 | auto bucket = std::make_unique<Bucket>(); | |
| 47 | 726 | bucket->key = sorted; | |
| 48 | 9176 | for (const ColumnLayout& column : layout) { | |
| 49 | 8450 | AddColumn(*bucket, column.attribute, column.prototype->Clone()); | |
| 50 | } | ||
| 51 | 726 | const auto id = static_cast<BucketId>(buckets_.size()); | |
| 52 | 726 | buckets_.push_back(std::move(bucket)); | |
| 53 | 726 | bucketIndex_.emplace(std::move(sorted), id); | |
| 54 | 726 | return id; | |
| 55 | 1454 | } | |
| 56 | |||
| 57 | 8642 | Column& PrimStore::AddColumn(Bucket& bucket, AttributeId attribute, | |
| 58 | std::unique_ptr<ColumnValues> values) { | ||
| 59 | 8642 | values->Grow(bucket.Rows()); | |
| 60 | 8642 | Column column; | |
| 61 | 8642 | column.attribute = attribute; | |
| 62 | 8642 | column.values = std::move(values); | |
| 63 | 8642 | column.Resize(bucket.Rows()); | |
| 64 | 8642 | bucket.columnIndex.emplace(attribute, static_cast<std::uint32_t>(bucket.columns.size())); | |
| 65 | 17284 | return bucket.columns.emplace_back(std::move(column)); | |
| 66 | 8642 | } | |
| 67 | |||
| 68 | 352602 | const PrimStore::SlotData& PrimStore::Slot(PrimId prim) const { | |
| 69 | 352602 | if (!Alive(prim)) { | |
| 70 | 1 | throw std::invalid_argument("The prim has been removed"); | |
| 71 | } | ||
| 72 | 352601 | return slots_[prim.slot]; | |
| 73 | } | ||
| 74 | |||
| 75 | 352657 | bool PrimStore::Alive(PrimId prim) const { | |
| 76 | 352657 | return prim.slot < slots_.size() && slots_[prim.slot].alive && | |
| 77 | 352653 | slots_[prim.slot].generation == prim.generation; | |
| 78 | } | ||
| 79 | |||
| 80 | 186335 | std::optional<PrimId> PrimStore::Find(PathId path) const { | |
| 81 | 186335 | if (path >= pathSlots_.size() || pathSlots_[path] == kNoSlot) { | |
| 82 | 151554 | return std::nullopt; | |
| 83 | } | ||
| 84 | 34781 | const std::uint32_t slot = pathSlots_[path]; | |
| 85 | 34781 | return PrimId{slot, slots_[slot].generation}; | |
| 86 | } | ||
| 87 | |||
| 88 | 73191 | PrimId PrimStore::AddSlot(PathId path, BucketId bucket, std::uint32_t row) { | |
| 89 | 73191 | std::uint32_t slot; | |
| 90 | 73191 | if (free_.empty()) { | |
| 91 | 2951 | slot = static_cast<std::uint32_t>(slots_.size()); | |
| 92 | 2951 | slots_.emplace_back(); | |
| 93 | } else { | ||
| 94 | 70240 | slot = free_.back(); | |
| 95 | 70240 | free_.pop_back(); | |
| 96 | } | ||
| 97 | 73191 | SlotData& data = slots_[slot]; | |
| 98 | 73191 | data.bucket = bucket; | |
| 99 | 73191 | data.row = row; | |
| 100 | 73191 | data.path = path; | |
| 101 | 73191 | data.alive = true; | |
| 102 | 73191 | if (pathSlots_.size() <= path) { | |
| 103 | 2823 | pathSlots_.resize(std::max<std::size_t>(paths_.Size(), path + 1), kNoSlot); | |
| 104 | } | ||
| 105 | 73191 | pathSlots_[path] = slot; | |
| 106 | 73191 | return {slot, data.generation}; | |
| 107 | } | ||
| 108 | |||
| 109 | 72993 | PrimId PrimStore::Define(PathId path, BucketId bucket) { | |
| 110 | 72993 | if (const std::optional<PrimId> existing = Find(path)) { | |
| 111 | 2 | if (PrimBucket(*existing) != bucket) { | |
| 112 | 2 | throw std::invalid_argument("Prim " + paths_.ToPath(path).String() + | |
| 113 | 3 | " already exists with another type"); | |
| 114 | } | ||
| 115 | 1 | return *existing; | |
| 116 | } | ||
| 117 | 72991 | return DefineMany(std::span(&path, 1), bucket).front(); | |
| 118 | } | ||
| 119 | |||
| 120 | 72992 | std::vector<PrimId> PrimStore::DefineMany(std::span<const PathId> paths, BucketId bucket) { | |
| 121 | 146183 | for (const PathId path : paths) { | |
| 122 | 73191 | if (Find(path)) { | |
| 123 | ✗ | throw std::invalid_argument("Prim " + paths_.ToPath(path).String() + " already exists"); | |
| 124 | } | ||
| 125 | } | ||
| 126 | 72992 | const auto count = static_cast<std::uint32_t>(paths.size()); | |
| 127 | 72992 | const std::uint32_t first = AddRows(bucket, count); | |
| 128 | 72992 | Bucket& b = *buckets_[bucket]; | |
| 129 | 72992 | std::vector<PrimId> prims; | |
| 130 | 72992 | prims.reserve(count); | |
| 131 | 146183 | for (std::uint32_t i = 0; i < count; ++i) { | |
| 132 | 73191 | prims.push_back(AddSlot(paths[i], bucket, first + i)); | |
| 133 | 73191 | b.prims[first + i] = prims.back(); | |
| 134 | } | ||
| 135 | 72992 | return prims; | |
| 136 | ✗ | } | |
| 137 | |||
| 138 | 73070 | std::uint32_t PrimStore::AddRows(BucketId bucket, std::uint32_t count) { | |
| 139 | 73070 | Bucket& b = *buckets_[bucket]; | |
| 140 | 73070 | const std::uint32_t first = b.Rows(); | |
| 141 | 253481 | for (Column& column : b.columns) { | |
| 142 | 180411 | column.values->Grow(count); | |
| 143 | 180411 | column.Resize(first + count); | |
| 144 | } | ||
| 145 | 73070 | b.prims.resize(first + count); | |
| 146 | 73070 | Log(b, {Next(), StructuralChange::Kind::Added, first, count, 0}); | |
| 147 | 73070 | return first; | |
| 148 | } | ||
| 149 | |||
| 150 | 70265 | void PrimStore::Remove(PrimId prim) { | |
| 151 | 70265 | SlotData& slot = Slot(prim); | |
| 152 | 70265 | DropRow(slot); | |
| 153 | 70265 | pathSlots_[slot.path] = kNoSlot; | |
| 154 | 70265 | slot.alive = false; | |
| 155 | 70265 | ++slot.generation; | |
| 156 | 70265 | free_.push_back(prim.slot); | |
| 157 | 70265 | } | |
| 158 | |||
| 159 | 78 | void PrimStore::Move(PrimId prim, BucketId bucket) { | |
| 160 | 78 | SlotData& slot = Slot(prim); | |
| 161 | 78 | if (slot.bucket == bucket) { | |
| 162 | return; | ||
| 163 | } | ||
| 164 | 78 | const std::uint32_t row = AddRows(bucket, 1); | |
| 165 | 78 | Bucket& to = *buckets_[bucket]; | |
| 166 | 78 | const Bucket& from = *buckets_[slot.bucket]; | |
| 167 | 78 | to.prims[row] = prim; | |
| 168 | 78 | const std::uint64_t version = Next(); | |
| 169 | 1306 | for (const Column& source : from.columns) { | |
| 170 | 1228 | Column* column = to.Find(source.attribute); | |
| 171 | 1239 | if (column != nullptr && source.authored.Test(slot.row) && | |
| 172 | 11 | column->values->Type() == source.values->Type()) { | |
| 173 | 11 | column->values->CopyRow(row, *source.values, slot.row); | |
| 174 | 11 | Stamp(bucket, *column, row, row + 1, version); | |
| 175 | } | ||
| 176 | } | ||
| 177 | 78 | DropRow(slot); | |
| 178 | 78 | slot.bucket = bucket; | |
| 179 | 78 | slot.row = row; | |
| 180 | } | ||
| 181 | |||
| 182 | 70343 | void PrimStore::DropRow(const SlotData& slot) { | |
| 183 | 70343 | Bucket& b = *buckets_[slot.bucket]; | |
| 184 | 70343 | const std::uint32_t row = slot.row; | |
| 185 | 70343 | const std::uint32_t last = b.Rows() - 1; | |
| 186 | 70343 | const std::uint64_t version = Next(); | |
| 187 | 70343 | Log(b, {version, StructuralChange::Kind::Removed, row, 1, 0}); | |
| 188 | 70343 | if (row != last) { | |
| 189 | 252 | b.prims[row] = b.prims[last]; | |
| 190 | 252 | slots_[b.prims[row].slot].row = row; | |
| 191 | 252 | Log(b, {version, StructuralChange::Kind::Moved, row, 1, last}); | |
| 192 | } | ||
| 193 | 70343 | b.prims.pop_back(); | |
| 194 | 215197 | for (Column& column : b.columns) { | |
| 195 | 144854 | column.values->SwapRemove(row); | |
| 196 | 144854 | column.authored.Set(row, column.authored.Test(last)); | |
| 197 | 144854 | column.Resize(last); | |
| 198 | 144854 | if (row != last) { | |
| 199 | // The moved row's values change at its new place, where readers look. | ||
| 200 | 3431 | Touch(slot.bucket, column, row, row + 1, version); | |
| 201 | } | ||
| 202 | } | ||
| 203 | 70343 | } | |
| 204 | |||
| 205 | 10 | void PrimStore::Clear(PrimId prim, AttributeId attribute) { | |
| 206 | 10 | const SlotData& slot = Slot(prim); | |
| 207 | 10 | Column* column = buckets_[slot.bucket]->Find(attribute); | |
| 208 | 10 | if (column == nullptr || !column->authored.Test(slot.row)) { | |
| 209 | return; | ||
| 210 | } | ||
| 211 | 10 | column->values->Reset(slot.row); | |
| 212 | 10 | column->authored.Set(slot.row, false); | |
| 213 | 10 | Touch(slot.bucket, *column, slot.row, slot.row + 1, Next()); | |
| 214 | } | ||
| 215 | |||
| 216 | 143665 | void PrimStore::Log(Bucket& bucket, StructuralChange change) { | |
| 217 | 143665 | bucket.version = change.version; | |
| 218 | 143665 | bucket.log.push_back(change); | |
| 219 | 143665 | if (bucket.log.size() > kLogLimit) { | |
| 220 | 74465 | bucket.logStart = bucket.log.front().version; | |
| 221 | 74465 | bucket.log.pop_front(); | |
| 222 | } | ||
| 223 | 143665 | } | |
| 224 | |||
| 225 | 4994 | void PrimStore::Stamp(BucketId bucket, Column& column, std::uint32_t begin, std::uint32_t end, | |
| 226 | std::uint64_t version) { | ||
| 227 | 4994 | column.authored.SetRange(begin, end); | |
| 228 | 4994 | Touch(bucket, column, begin, end, version); | |
| 229 | 4994 | } | |
| 230 | |||
| 231 | 8562 | void PrimStore::Touch(BucketId bucket, Column& column, std::uint32_t begin, std::uint32_t end, | |
| 232 | std::uint64_t version) { | ||
| 233 | 8562 | if (begin == end) { | |
| 234 | return; | ||
| 235 | } | ||
| 236 | 17124 | std::fill(column.chunkVersions.begin() + begin / kChunkRows, | |
| 237 | 8562 | column.chunkVersions.begin() + (end - 1) / kChunkRows + 1, version); | |
| 238 | 8562 | column.version = version; | |
| 239 | 8562 | buckets_[bucket]->version = version; | |
| 240 | } | ||
| 241 | |||
| 242 | 291 | ChangeSet PrimStore::ChangesSince(std::uint64_t cursor) const { | |
| 243 | 291 | ChangeSet changes; | |
| 244 | 291 | changes.version = version_; | |
| 245 | 1039 | for (BucketId id = 0; id < buckets_.size(); ++id) { | |
| 246 | 748 | const Bucket& bucket = *buckets_[id]; | |
| 247 | 748 | if (bucket.version <= cursor) { | |
| 248 | 317 | continue; | |
| 249 | } | ||
| 250 | // A reader too far behind to replay the log gets the bucket afresh: every row new and | ||
| 251 | // every value changed. | ||
| 252 | 431 | const bool reset = cursor < bucket.logStart; | |
| 253 | 431 | if (reset) { | |
| 254 | 1 | changes.rows.push_back( | |
| 255 | 1 | {id, {bucket.version, StructuralChange::Kind::Reset, 0, bucket.Rows(), 0}}); | |
| 256 | } else { | ||
| 257 | 430 | const auto newer = | |
| 258 | 430 | std::upper_bound(bucket.log.begin(), bucket.log.end(), cursor, | |
| 259 | 1713 | [](std::uint64_t at, const StructuralChange& change) { | |
| 260 | 1713 | return at < change.version; | |
| 261 | 430 | }); | |
| 262 | 6125 | for (auto change = newer; change != bucket.log.end(); ++change) { | |
| 263 | 5695 | changes.rows.push_back({id, *change}); | |
| 264 | } | ||
| 265 | } | ||
| 266 | 5893 | for (const Column& column : bucket.columns) { | |
| 267 | 5462 | if (column.version <= cursor && !reset) { | |
| 268 | 3261 | continue; | |
| 269 | } | ||
| 270 | 2201 | ChangeSet::Values values{id, column.attribute, {}}; | |
| 271 | 12607 | for (std::uint32_t chunk = 0; chunk < column.chunkVersions.size(); ++chunk) { | |
| 272 | 10406 | if (column.chunkVersions[chunk] <= cursor && !reset) { | |
| 273 | 3738 | continue; | |
| 274 | } | ||
| 275 | 6668 | const std::uint32_t begin = chunk * kChunkRows; | |
| 276 | 6668 | const std::uint32_t end = std::min(begin + kChunkRows, bucket.Rows()); | |
| 277 | 6668 | if (!values.rows.empty() && values.rows.back().end == begin) { | |
| 278 | 3658 | values.rows.back().end = end; | |
| 279 | } else { | ||
| 280 | 3010 | values.rows.push_back({begin, end}); | |
| 281 | } | ||
| 282 | } | ||
| 283 | 2201 | if (!values.rows.empty()) { | |
| 284 | 2201 | changes.values.push_back(std::move(values)); | |
| 285 | } | ||
| 286 | 2201 | } | |
| 287 | } | ||
| 288 | 291 | return changes; | |
| 289 | ✗ | } | |
| 290 | |||
| 291 | } // namespace ernie | ||
| 292 |