perf(python): accelerate Dataset Index metadata lookups (#959)
* perf(python): accelerate Dataset Index metadata lookups
* build(python): require Dataset Index Cython extension
* fix(python): bound Dataset Index lookups against corrupt ranges
The Cython fast paths indexed the mmap with child ranges read from the index
records without validating them, so a corrupt or hand-crafted .tsidx could
read past the mapping and crash the interpreter (SIGSEGV/SIGBUS) instead of
raising, which the previous pure-Python path did via record(). Validate the
device/column/series child ranges the same way describe_series and
find_series_span already do, and take ids as signed values so negative ids
keep raising IndexError instead of OverflowError.
Also drop the now-unused pure-Python _find_child, retarget the hash-collision
test at the production Cython lookup (the old test only exercised that dead
helper), and skip the dataclass-slots assertion on Python 3.9 where
dataclasses do not support slots.
* refactor(python): drop Dataset Index descriptor cache env knobs
The descriptor cache size and its hit/miss counters were exposed through
TSFILE_DATAFRAME_DESCRIPTOR_CACHE_SIZE / _STATS, adding process-wide
configuration surface plus a stats branch on every lookup for a bounded
process-local cache that already has a sane budget. Keep the historical
4096-entry default and restore the unconditional cache reads.
diff --git a/python/setup.py b/python/setup.py
index c7b4cce..fe19fc9 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -273,6 +273,11 @@
exts = [
Extension("tsfile.dataset._merge", ["tsfile/dataset/_merge.pyx"], **merge_common),
+ Extension(
+ "tsfile.dataset._index",
+ ["tsfile/dataset/_index.pyx"],
+ **merge_common,
+ ),
Extension("tsfile.tsfile_py_cpp", ["tsfile/tsfile_py_cpp.pyx"], **common),
Extension(
"tsfile.tsfile_reader",
diff --git a/python/tests/test_dataset_index.py b/python/tests/test_dataset_index.py
index 91761a1..4ac9f2e 100644
--- a/python/tests/test_dataset_index.py
+++ b/python/tests/test_dataset_index.py
@@ -17,6 +17,8 @@
from types import SimpleNamespace
import os
+import struct
+import sys
import threading
import numpy as np
@@ -37,6 +39,8 @@
from tsfile.dataset.index import (
COLUMN_SCHEMA,
DEVICE_FILE_SPAN,
+ DEVICE_NAME_INDEX,
+ DEVICE_RECORD,
DIRECTORY,
HEADER,
LOGICAL_SERIES,
@@ -44,6 +48,7 @@
RECORDS,
SERIES_FILE_SPAN,
SERIES_LOCATOR,
+ TABLE_RECORD,
TSFILE_RECORD,
build_sections_from_dataframe,
crc32c,
@@ -90,6 +95,22 @@
)
+def _index_section_entry(blob, section_type):
+ return DIRECTORY.unpack_from(
+ blob, HEADER.size + (section_type - 1) * DIRECTORY.size
+ )
+
+
+def _patch_index_field(path, section_type, field_offset, value, fmt="<I"):
+ """Rewrite one raw index field, invalidating that section's checksum."""
+ with open(path, "r+b") as stream:
+ blob = bytearray(stream.read())
+ entry = _index_section_entry(blob, section_type)
+ struct.pack_into(fmt, blob, entry[2] + field_offset, value)
+ stream.seek(0)
+ stream.write(blob)
+
+
def test_binary_layout_matches_cpp_v1():
assert HEADER.size == 64
assert DIRECTORY.size == 32
@@ -124,27 +145,288 @@
assert index.string(file_record[0]) == str(source)
-def test_child_lookup_checks_full_bytes_for_hash_collisions(monkeypatch):
- rows = [
- (0, 10, 42, 0, 0),
- (0, 11, 42, 1, 0),
- (0, 12, 42, 2, 0),
+def test_index_lookup_is_required_and_does_not_unpack_python_record_tuples(
+ tmp_path, monkeypatch
+):
+ assert index_module.IndexLookup is not None
+ source = tmp_path / "source.tsfile"
+ source.write_bytes(b"T" * 4096)
+ output = tmp_path / "dataset.tsidx"
+ dataframe = _synthetic_dataframe(str(source))
+ write_index_atomic(str(output), build_sections_from_dataframe(dataframe))
+
+ with MappedDatasetIndex(str(output), verify_sections=True) as index:
+ assert isinstance(index._lookup, index_module.IndexLookup)
+ assert not hasattr(index, "_fast_lookup")
+
+ def fail_record(*_args, **_kwargs):
+ raise AssertionError("index lookup should not unpack Python record tuples")
+
+ monkeypatch.setattr(index, "record", fail_record)
+ assert index.find_device_id(0, "root.") == 0
+ assert index.find_column_id(0, "s1") == 0
+ assert index.find_series_id(0, 0) == 0
+
+
+def test_series_description_does_not_unpack_python_record_tuples(tmp_path, monkeypatch):
+ source = tmp_path / "source.tsfile"
+ source.write_bytes(b"T" * 4096)
+ output = tmp_path / "dataset.tsidx"
+ dataframe = _synthetic_dataframe(str(source))
+ write_index_atomic(str(output), build_sections_from_dataframe(dataframe))
+
+ with MappedDatasetIndex(str(output), verify_sections=True) as index:
+ series = index.record(LOGICAL_SERIES, 0)
+ span = index.record(SERIES_FILE_SPAN, series[2])
+ locator = index.record(SERIES_LOCATOR, span[2])
+ device_span = index.record(DEVICE_FILE_SPAN, locator[0])
+ expected = (series[0], series[1], series[4], series[5])
+ expected_count = device_span[6] if device_span[4] == 1 else span[6]
+
+ def fail_record(*_args, **_kwargs):
+ raise AssertionError("series description should not unpack records")
+
+ monkeypatch.setattr(index, "record", fail_record)
+ device_id, column_id, min_time, max_time, count, shards = index.describe_series(
+ 0
+ )
+
+ assert (device_id, column_id, min_time, max_time) == expected
+ assert count == expected_count
+ assert shards == [(span[1], span[2], expected_count, span[4], span[5])]
+
+
+def test_span_and_locator_metadata_do_not_unpack_python_records(tmp_path, monkeypatch):
+ source = tmp_path / "source.tsfile"
+ source.write_bytes(b"T" * 4096)
+ output = tmp_path / "dataset.tsidx"
+ dataframe = _synthetic_dataframe(str(source))
+ write_index_atomic(str(output), build_sections_from_dataframe(dataframe))
+
+ with MappedDatasetIndex(str(output), verify_sections=True) as index:
+ series = index.record(LOGICAL_SERIES, 0)
+ span = index.record(SERIES_FILE_SPAN, series[2])
+ locator = index.record(SERIES_LOCATOR, span[2])
+ device_span = index.record(DEVICE_FILE_SPAN, locator[0])
+
+ def fail_record(*_args, **_kwargs):
+ raise AssertionError("span metadata should not unpack records")
+
+ monkeypatch.setattr(index, "record", fail_record)
+ assert index.find_series_span(0, span[1]) == (
+ span[2],
+ span[4],
+ span[5],
+ span[6],
+ )
+ assert index.locator_metadata(span[2]) == (
+ locator[0],
+ locator[1],
+ device_span[1],
+ device_span[4],
+ device_span[6],
+ )
+
+
+def test_runtime_reader_span_lookup_uses_index_metadata(tmp_path, monkeypatch):
+ source = tmp_path / "part.tsfile"
+ _write_runtime_file(source, 0)
+
+ with TsFileDataFrame(str(source), show_progress=False, use_index=True) as dataframe:
+ reader = dataframe._runtime.catalog.reader_for(0)
+ series = dataframe._runtime.index.record(LOGICAL_SERIES, 0)
+
+ def fail_record(*_args, **_kwargs):
+ raise AssertionError("runtime span lookup should not unpack records")
+
+ monkeypatch.setattr(dataframe._runtime.index, "record", fail_record)
+ locator_id, min_time, max_time, length = reader._span(series[0], series[1])
+
+ assert locator_id >= 0
+ assert min_time == 0
+ assert max_time == 1
+ assert length >= 0
+
+
+def test_prepared_locator_metadata_does_not_unpack_python_records(
+ tmp_path, monkeypatch
+):
+ source = tmp_path / "source.tsfile"
+ source.write_bytes(b"T" * 4096)
+ output = tmp_path / "dataset.tsidx"
+ dataframe = _synthetic_dataframe(str(source))
+ write_index_atomic(str(output), build_sections_from_dataframe(dataframe))
+
+ with MappedDatasetIndex(str(output), verify_sections=True) as index:
+ span = index.record(SERIES_FILE_SPAN, 0)
+ locator = index.record(SERIES_LOCATOR, span[2])
+ device_span = index.record(DEVICE_FILE_SPAN, locator[0])
+ file_record = index.record(TSFILE_RECORD, span[1])
+
+ def fail_record(*_args, **_kwargs):
+ raise AssertionError("prepared locator should not unpack records")
+
+ monkeypatch.setattr(index, "record", fail_record)
+ assert index.prepared_locator_metadata(span[1], span[2]) == (
+ file_record[2],
+ file_record[3],
+ locator[1],
+ locator[2],
+ locator[3],
+ locator[4],
+ device_span[2],
+ device_span[3],
+ )
+
+
+def test_identity_and_device_bounds_do_not_unpack_python_records(tmp_path, monkeypatch):
+ source = tmp_path / "source.tsfile"
+ source.write_bytes(b"T" * 4096)
+ output = tmp_path / "dataset.tsidx"
+ dataframe = _synthetic_dataframe(str(source))
+ write_index_atomic(str(output), build_sections_from_dataframe(dataframe))
+
+ with MappedDatasetIndex(str(output), verify_sections=True) as index:
+ device = index.record(index_module.DEVICE_RECORD, 0)
+ table = index.record(index_module.TABLE_RECORD, device[0])
+ column = index.record(COLUMN_SCHEMA, 0)
+ expected_device = (device[0], device[1])
+ expected_table_name_id = table[0]
+ expected_column_name_id = column[1]
+ expected_bounds = (device[8], device[9])
+ expected_name = index.string(expected_column_name_id)
+
+ def fail_record(*_args, **_kwargs):
+ raise AssertionError("identity metadata should not unpack records")
+
+ def fail_string_bytes(*_args, **_kwargs):
+ raise AssertionError("index string lookup should not copy bytes")
+
+ monkeypatch.setattr(index, "record", fail_record)
+ monkeypatch.setattr(index, "string_bytes", fail_string_bytes)
+ assert index.device_route(0) == expected_device
+ assert index.table_name_id(device[0]) == expected_table_name_id
+ assert index.column_name_id(0) == expected_column_name_id
+ assert index.device_time_bounds(0) == expected_bounds
+ assert index.string(expected_column_name_id) == expected_name
+
+
+def test_series_identity_does_not_unpack_python_records(tmp_path, monkeypatch):
+ source = tmp_path / "source.tsfile"
+ source.write_bytes(b"T" * 4096)
+ output = tmp_path / "dataset.tsidx"
+ dataframe = _synthetic_dataframe(str(source))
+ write_index_atomic(str(output), build_sections_from_dataframe(dataframe))
+
+ with MappedDatasetIndex(str(output), verify_sections=True) as index:
+ series = index.record(LOGICAL_SERIES, 0)
+
+ def fail_record(*_args, **_kwargs):
+ raise AssertionError("series identity should not unpack records")
+
+ monkeypatch.setattr(index, "record", fail_record)
+ assert index.series_identity(0) == (series[0], series[1])
+
+
+def test_child_lookup_checks_full_bytes_for_hash_collisions(tmp_path):
+ """A stored hash match with different bytes must not resolve to that row."""
+ source = tmp_path / "devices.tsfile"
+ _write_runtime_devices_file(source)
+ with TsFileDataFrame(str(source), show_progress=False, use_index=True) as dataframe:
+ index_path = dataframe._runtime.index.path
+
+ with MappedDatasetIndex(index_path) as index:
+ rows = list(index.records(DEVICE_NAME_INDEX))
+ names = [index.string(row[3]) for row in rows]
+ assert len(rows) >= 3
+
+ # Same length as the row it must not match, so only the bytes can reject it.
+ tail = names[-1][:-1]
+ probe = None
+ for replacement in "xyz019":
+ value = tail[:-1] + replacement + "."
+ if (
+ len(value) == len(names[-1])
+ and value not in names
+ and index_module.name_hash(value.encode("utf-8")) >= rows[-1][2]
+ ):
+ probe = value
+ break
+ assert probe is not None
+
+ # Give the last row the probe's hash: the lookup finds that row by hash and
+ # can only reject it by comparing the stored bytes.
+ with open(index_path, "r+b") as stream:
+ blob = bytearray(stream.read())
+ entry = _index_section_entry(blob, DEVICE_NAME_INDEX)
+ struct.pack_into(
+ "<Q",
+ blob,
+ entry[2] + (entry[4] - 1) * entry[1] + 8,
+ index_module.name_hash(probe.encode("utf-8")),
+ )
+ stream.seek(0)
+ stream.write(blob)
+
+ with MappedDatasetIndex(index_path) as index:
+ untouched = list(index.records(DEVICE_NAME_INDEX))[:-1]
+ for table_id, device_id, _, sid, _ in untouched:
+ assert index.find_device_id(table_id, index.string(sid)) == device_id
+ with pytest.raises(KeyError):
+ index.find_device_id(untouched[0][0], probe)
+
+
+@pytest.mark.parametrize(
+ ("section_type", "field_offset", "lookup"),
+ [
+ (TABLE_RECORD, 12, lambda index: index.find_device_id(0, "root.")),
+ (TABLE_RECORD, 20, lambda index: index.find_column_id(0, "s1")),
+ (DEVICE_RECORD, 20, lambda index: index.find_series_id(0, 0)),
+ ],
+)
+def test_out_of_range_child_ranges_raise_instead_of_reading_out_of_bounds(
+ tmp_path, section_type, field_offset, lookup
+):
+ """Corrupt child ranges must raise, not walk past the mmap (SIGSEGV/SIGBUS)."""
+ source = tmp_path / "source.tsfile"
+ source.write_bytes(b"T" * 4096)
+ output = tmp_path / "dataset.tsidx"
+ dataframe = _synthetic_dataframe(str(source))
+ write_index_atomic(str(output), build_sections_from_dataframe(dataframe))
+ _patch_index_field(output, section_type, field_offset, 0xFFFFFF00)
+
+ # section checksums stay unverified here, matching DatasetRuntime's load path
+ with MappedDatasetIndex(str(output)) as index:
+ with pytest.raises(IndexError):
+ lookup(index)
+
+
+def test_lookup_rejects_negative_ids_with_index_error(tmp_path):
+ source = tmp_path / "source.tsfile"
+ source.write_bytes(b"T" * 4096)
+ output = tmp_path / "dataset.tsidx"
+ dataframe = _synthetic_dataframe(str(source))
+ write_index_atomic(str(output), build_sections_from_dataframe(dataframe))
+
+ calls = [
+ lambda index: index.find_device_id(-1, "root."),
+ lambda index: index.find_column_id(-1, "s1"),
+ lambda index: index.find_series_id(-1, 0),
+ lambda index: index.describe_series(-1),
+ lambda index: index.series_identity(-1),
+ lambda index: index.find_series_span(-1, 0),
+ lambda index: index.locator_metadata(-1),
+ lambda index: index.prepared_locator_metadata(-1, 0),
+ lambda index: index.device_route(-1),
+ lambda index: index.table_name_id(-1),
+ lambda index: index.column_name_id(-1),
+ lambda index: index.device_time_bounds(-1),
+ lambda index: index.string(-1),
]
- names = [b"alpha", b"beta", b"gamma"]
- monkeypatch.setattr(index_module, "name_hash", lambda _value: 42)
-
- class _Index:
- @staticmethod
- def record(_section_type, record_id):
- return rows[record_id]
-
- @staticmethod
- def string_bytes(sid):
- return names[sid]
-
- assert MappedDatasetIndex._find_child(_Index(), 0, 0, "beta", 0, 3) == 11
- with pytest.raises(KeyError):
- MappedDatasetIndex._find_child(_Index(), 0, 0, "missing", 0, 3)
+ with MappedDatasetIndex(str(output)) as index:
+ for call in calls:
+ with pytest.raises(IndexError):
+ call(index)
def test_rejects_damaged_header_checksum(tmp_path):
@@ -448,6 +730,17 @@
assert find_device_calls == 4
+@pytest.mark.skipif(
+ sys.version_info < (3, 10), reason="dataclasses only support slots on 3.10+"
+)
+def test_runtime_descriptor_objects_use_slots():
+ shard = runtime_module.RuntimeSeriesShard(None, 1, 2, 3, 4, 5, 6)
+ descriptor = runtime_module.RuntimeSeriesDescriptor((1, 2), 3, 4, (shard,), 5, 6, 7)
+
+ assert not hasattr(shard, "__dict__")
+ assert not hasattr(descriptor, "__dict__")
+
+
def test_reader_pool_enforces_open_file_cap(tmp_path, monkeypatch):
first = tmp_path / "part1.tsfile"
second = tmp_path / "part2.tsfile"
diff --git a/python/tsfile/dataset/_index.pyx b/python/tsfile/dataset/_index.pyx
new file mode 100644
index 0000000..fb74537
--- /dev/null
+++ b/python/tsfile/dataset/_index.pyx
@@ -0,0 +1,537 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+# cython: boundscheck=False, wraparound=False, cdivision=True, language_level=3
+
+"""Cython accessors for the existing read-only Dataset Index mmap.
+
+This module deliberately owns no index-sized data structures. It receives the
+already-mapped byte view and section directory from ``MappedDatasetIndex`` and
+only removes Python tuple/bytes allocation from the binary-search loops.
+"""
+
+from libc.stdint cimport int64_t, uint8_t, uint16_t, uint32_t, uint64_t
+from cpython.unicode cimport PyUnicode_AsUTF8AndSize, PyUnicode_DecodeUTF8
+
+
+cdef inline uint32_t _read_u32(
+ const uint8_t[:] view, uint64_t offset) noexcept nogil:
+ return (
+ <uint32_t>view[offset]
+ | (<uint32_t>view[offset + 1] << 8)
+ | (<uint32_t>view[offset + 2] << 16)
+ | (<uint32_t>view[offset + 3] << 24)
+ )
+
+
+cdef inline uint16_t _read_u16(
+ const uint8_t[:] view, uint64_t offset) noexcept nogil:
+ return (
+ <uint16_t>view[offset]
+ | (<uint16_t>view[offset + 1] << 8)
+ )
+
+
+cdef inline uint64_t _read_u64(
+ const uint8_t[:] view, uint64_t offset) noexcept nogil:
+ return (
+ <uint64_t>view[offset]
+ | (<uint64_t>view[offset + 1] << 8)
+ | (<uint64_t>view[offset + 2] << 16)
+ | (<uint64_t>view[offset + 3] << 24)
+ | (<uint64_t>view[offset + 4] << 32)
+ | (<uint64_t>view[offset + 5] << 40)
+ | (<uint64_t>view[offset + 6] << 48)
+ | (<uint64_t>view[offset + 7] << 56)
+ )
+
+
+cdef inline uint64_t _fnv1a(
+ const char* data, Py_ssize_t length) noexcept nogil:
+ cdef uint64_t result = 1469598103934665603
+ cdef uint64_t prime = 1099511628211
+ cdef Py_ssize_t index
+ for index in range(length):
+ result ^= <uint8_t>data[index]
+ result = result * prime
+ return result
+
+
+cdef inline bint _string_equals(
+ const uint8_t[:] view,
+ uint64_t offsets_base,
+ uint64_t strings_base,
+ uint32_t string_count,
+ uint32_t string_id,
+ const char* data,
+ Py_ssize_t length,
+) noexcept nogil:
+ cdef uint32_t start
+ cdef uint32_t end
+ cdef Py_ssize_t index
+ if string_id + 1 >= string_count:
+ return False
+ start = _read_u32(view, offsets_base + <uint64_t>string_id * 4)
+ end = _read_u32(view, offsets_base + <uint64_t>(string_id + 1) * 4)
+ if end - start != length:
+ return False
+ for index in range(length):
+ if view[strings_base + start + index] != <uint8_t>data[index]:
+ return False
+ return True
+
+
+cdef inline int64_t _find_child(
+ const uint8_t[:] view,
+ uint64_t section_base,
+ uint32_t record_size,
+ uint32_t first,
+ uint32_t count,
+ uint32_t table_id,
+ uint64_t target_hash,
+ const char* data,
+ Py_ssize_t length,
+ uint64_t string_offsets_base,
+ uint64_t string_bytes_base,
+ uint32_t string_count,
+) noexcept nogil:
+ cdef uint32_t low = first
+ cdef uint32_t high = first + count
+ cdef uint32_t middle
+ cdef uint64_t row_base
+ cdef uint64_t row_hash
+ cdef uint32_t row_table_id
+ cdef uint32_t row_id
+ cdef uint32_t row_string_id
+
+ while low < high:
+ middle = low + (high - low) // 2
+ row_base = section_base + <uint64_t>middle * record_size
+ row_hash = _read_u64(view, row_base + 8)
+ if row_hash < target_hash:
+ low = middle + 1
+ else:
+ high = middle
+
+ high = first + count
+ while low < high:
+ row_base = section_base + <uint64_t>low * record_size
+ if _read_u64(view, row_base + 8) != target_hash:
+ break
+ row_table_id = _read_u32(view, row_base)
+ row_string_id = _read_u32(view, row_base + 16)
+ if row_table_id == table_id and _string_equals(
+ view,
+ string_offsets_base,
+ string_bytes_base,
+ string_count,
+ row_string_id,
+ data,
+ length,
+ ):
+ row_id = _read_u32(view, row_base + 4)
+ return row_id
+ low += 1
+ return -1
+
+
+cdef const char* _utf8_name(object name, Py_ssize_t* length):
+ cdef const char* data
+ if not isinstance(name, str):
+ raise TypeError("Dataset Index names must be str")
+ data = PyUnicode_AsUTF8AndSize(name, length)
+ if data == NULL:
+ raise UnicodeEncodeError("utf-8", name, 0, len(name), "invalid UTF-8")
+ return data
+
+
+cdef class IndexLookup:
+ """Typed lookup kernel over a ``MappedDatasetIndex`` memoryview."""
+
+ cdef const uint8_t[:] _view
+ cdef uint64_t _section_offsets[14]
+ cdef uint32_t _section_sizes[14]
+ cdef uint32_t _section_counts[14]
+
+ def __cinit__(self, object view, object entries):
+ cdef int section
+ self._view = view
+ for section in range(1, 14):
+ self._section_offsets[section] = entries[section][2]
+ self._section_sizes[section] = entries[section][1]
+ self._section_counts[section] = entries[section][4]
+
+ def find_device_id(self, Py_ssize_t table_id, object name):
+ cdef const char* data
+ cdef Py_ssize_t length
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t table_base
+ cdef uint32_t first
+ cdef uint32_t count
+ cdef uint64_t target_hash
+ cdef int64_t result
+ data = _utf8_name(name, &length)
+ if table_id < 0 or table_id >= self._section_counts[4]:
+ raise IndexError(table_id)
+ table_base = self._section_offsets[4] + <uint64_t>table_id * self._section_sizes[4]
+ first = _read_u32(view, table_base + 8)
+ count = _read_u32(view, table_base + 12)
+ if <uint64_t>first + count > self._section_counts[5]:
+ raise IndexError(table_id)
+ target_hash = _fnv1a(data, length)
+ with nogil:
+ result = _find_child(
+ view,
+ self._section_offsets[5],
+ self._section_sizes[5],
+ first,
+ count,
+ table_id,
+ target_hash,
+ data,
+ length,
+ self._section_offsets[1],
+ self._section_offsets[2],
+ self._section_counts[1],
+ )
+ if result < 0:
+ raise KeyError(name)
+ return result
+
+ def find_column_id(self, Py_ssize_t table_id, object name):
+ cdef const char* data
+ cdef Py_ssize_t length
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t table_base
+ cdef uint32_t first
+ cdef uint32_t count
+ cdef uint64_t target_hash
+ cdef int64_t result
+ data = _utf8_name(name, &length)
+ if table_id < 0 or table_id >= self._section_counts[4]:
+ raise IndexError(table_id)
+ table_base = self._section_offsets[4] + <uint64_t>table_id * self._section_sizes[4]
+ first = _read_u32(view, table_base + 16)
+ count = _read_u32(view, table_base + 20)
+ if <uint64_t>first + count > self._section_counts[7]:
+ raise IndexError(table_id)
+ target_hash = _fnv1a(data, length)
+ with nogil:
+ result = _find_child(
+ view,
+ self._section_offsets[7],
+ self._section_sizes[7],
+ first,
+ count,
+ table_id,
+ target_hash,
+ data,
+ length,
+ self._section_offsets[1],
+ self._section_offsets[2],
+ self._section_counts[1],
+ )
+ if result < 0:
+ raise KeyError(name)
+ return result
+
+ def find_series_id(self, Py_ssize_t device_id, Py_ssize_t column_id):
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t device_base
+ cdef uint32_t first
+ cdef uint32_t count
+ cdef uint32_t low
+ cdef uint32_t high
+ cdef uint32_t middle
+ cdef uint64_t row_base
+ cdef uint32_t row_column_id
+ cdef uint32_t row_device_id
+ cdef int64_t result = -1
+ if device_id < 0 or device_id >= self._section_counts[6]:
+ raise IndexError(device_id)
+ device_base = self._section_offsets[6] + <uint64_t>device_id * self._section_sizes[6]
+ first = _read_u32(view, device_base + 16)
+ count = _read_u32(view, device_base + 20)
+ if <uint64_t>first + count > self._section_counts[9]:
+ raise IndexError(device_id)
+ low = first
+ high = first + count
+ with nogil:
+ while low < high:
+ middle = low + (high - low) // 2
+ row_base = self._section_offsets[9] + <uint64_t>middle * self._section_sizes[9]
+ row_column_id = _read_u32(view, row_base + 4)
+ if row_column_id < column_id:
+ low = middle + 1
+ else:
+ high = middle
+ if low < first + count:
+ row_base = self._section_offsets[9] + <uint64_t>low * self._section_sizes[9]
+ row_device_id = _read_u32(view, row_base)
+ row_column_id = _read_u32(view, row_base + 4)
+ if row_device_id == device_id and row_column_id == column_id:
+ result = low
+ if result < 0:
+ raise KeyError(column_id)
+ return result
+
+ def describe_series(self, Py_ssize_t series_id):
+ """Expand one logical series without creating intermediate records."""
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t series_base
+ cdef uint32_t device_id
+ cdef uint32_t column_id
+ cdef uint32_t first_span
+ cdef uint32_t span_count
+ cdef uint32_t index
+ cdef uint32_t span_id
+ cdef uint64_t span_base
+ cdef uint32_t file_id
+ cdef uint32_t locator_id
+ cdef uint32_t device_span_id
+ cdef uint64_t locator_base
+ cdef uint64_t device_span_base
+ cdef uint64_t span_length
+ cdef uint64_t timeline_length
+ cdef int64_t min_time
+ cdef int64_t max_time
+ cdef int64_t span_min_time
+ cdef int64_t span_max_time
+ cdef uint64_t count = 0
+ cdef list shards = []
+
+ if series_id < 0 or series_id >= self._section_counts[9]:
+ raise IndexError(series_id)
+
+ series_base = (
+ self._section_offsets[9]
+ + <uint64_t>series_id * self._section_sizes[9]
+ )
+ device_id = _read_u32(view, series_base)
+ column_id = _read_u32(view, series_base + 4)
+ first_span = _read_u32(view, series_base + 8)
+ span_count = _read_u32(view, series_base + 12)
+ min_time = <int64_t>_read_u64(view, series_base + 16)
+ max_time = <int64_t>_read_u64(view, series_base + 24)
+
+ if <uint64_t>first_span + span_count > self._section_counts[12]:
+ raise IndexError(series_id)
+
+ for index in range(span_count):
+ span_id = first_span + index
+ span_base = (
+ self._section_offsets[12]
+ + <uint64_t>span_id * self._section_sizes[12]
+ )
+ file_id = _read_u32(view, span_base + 4)
+ locator_id = _read_u32(view, span_base + 8)
+ span_min_time = <int64_t>_read_u64(view, span_base + 16)
+ span_max_time = <int64_t>_read_u64(view, span_base + 24)
+ span_length = _read_u64(view, span_base + 32)
+
+ if locator_id >= self._section_counts[13]:
+ raise IndexError(locator_id)
+ locator_base = (
+ self._section_offsets[13]
+ + <uint64_t>locator_id * self._section_sizes[13]
+ )
+ device_span_id = _read_u32(view, locator_base)
+ if device_span_id >= self._section_counts[11]:
+ raise IndexError(device_span_id)
+ device_span_base = (
+ self._section_offsets[11]
+ + <uint64_t>device_span_id * self._section_sizes[11]
+ )
+ if _read_u16(view, device_span_base + 20) == 1:
+ timeline_length = _read_u64(view, device_span_base + 24)
+ else:
+ timeline_length = span_length
+ count += timeline_length
+ shards.append(
+ (file_id, locator_id, timeline_length, span_min_time, span_max_time)
+ )
+
+ return device_id, column_id, min_time, max_time, count, shards
+
+ def series_identity(self, Py_ssize_t series_id):
+ """Return device and column ids for one logical series."""
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t base
+ if series_id < 0 or series_id >= self._section_counts[9]:
+ raise IndexError(series_id)
+ base = self._section_offsets[9] + <uint64_t>series_id * self._section_sizes[9]
+ return _read_u32(view, base), _read_u32(view, base + 4)
+
+ def find_series_span(self, Py_ssize_t series_id, Py_ssize_t file_id):
+ """Find one series span and return locator/time/length scalars."""
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t series_base
+ cdef uint32_t first_span
+ cdef uint32_t span_count
+ cdef uint32_t index
+ cdef uint32_t span_id
+ cdef uint64_t span_base
+
+ if series_id < 0 or series_id >= self._section_counts[9]:
+ raise IndexError(series_id)
+ series_base = (
+ self._section_offsets[9]
+ + <uint64_t>series_id * self._section_sizes[9]
+ )
+ first_span = _read_u32(view, series_base + 8)
+ span_count = _read_u32(view, series_base + 12)
+ if <uint64_t>first_span + span_count > self._section_counts[12]:
+ raise IndexError(series_id)
+ for index in range(span_count):
+ span_id = first_span + index
+ span_base = (
+ self._section_offsets[12]
+ + <uint64_t>span_id * self._section_sizes[12]
+ )
+ if _read_u32(view, span_base + 4) == file_id:
+ return (
+ _read_u32(view, span_base + 8),
+ <int64_t>_read_u64(view, span_base + 16),
+ <int64_t>_read_u64(view, span_base + 24),
+ _read_u64(view, span_base + 32),
+ )
+ raise KeyError((series_id, file_id))
+
+ def locator_metadata(self, Py_ssize_t locator_id):
+ """Return locator and owning device-span scalars."""
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t locator_base
+ cdef uint32_t device_span_id
+ cdef uint64_t device_span_base
+
+ if locator_id < 0 or locator_id >= self._section_counts[13]:
+ raise IndexError(locator_id)
+ locator_base = (
+ self._section_offsets[13]
+ + <uint64_t>locator_id * self._section_sizes[13]
+ )
+ device_span_id = _read_u32(view, locator_base)
+ if device_span_id >= self._section_counts[11]:
+ raise IndexError(device_span_id)
+ device_span_base = (
+ self._section_offsets[11]
+ + <uint64_t>device_span_id * self._section_sizes[11]
+ )
+ return (
+ device_span_id,
+ _read_u16(view, locator_base + 4),
+ _read_u32(view, device_span_base + 4),
+ _read_u16(view, device_span_base + 20),
+ _read_u64(view, device_span_base + 24),
+ )
+
+ def prepared_locator_metadata(self, Py_ssize_t file_id, Py_ssize_t locator_id):
+ """Return the generation and locator fields used by native prepare."""
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t locator_base
+ cdef uint64_t device_span_base
+ cdef uint64_t file_base
+ cdef uint32_t device_span_id
+ cdef uint32_t device_file_id
+
+ if locator_id < 0 or locator_id >= self._section_counts[13]:
+ raise IndexError(locator_id)
+ if file_id < 0 or file_id >= self._section_counts[10]:
+ raise IndexError(file_id)
+ locator_base = (
+ self._section_offsets[13]
+ + <uint64_t>locator_id * self._section_sizes[13]
+ )
+ device_span_id = _read_u32(view, locator_base)
+ if device_span_id >= self._section_counts[11]:
+ raise IndexError(device_span_id)
+ device_span_base = (
+ self._section_offsets[11]
+ + <uint64_t>device_span_id * self._section_sizes[11]
+ )
+ device_file_id = _read_u32(view, device_span_base + 4)
+ if device_file_id != file_id:
+ raise ValueError("series locator points at another TsFile")
+ file_base = (
+ self._section_offsets[10]
+ + <uint64_t>file_id * self._section_sizes[10]
+ )
+ return (
+ _read_u64(view, file_base + 8),
+ _read_u64(view, file_base + 16),
+ _read_u16(view, locator_base + 4),
+ _read_u16(view, locator_base + 6),
+ _read_u64(view, locator_base + 8),
+ _read_u32(view, locator_base + 16),
+ _read_u64(view, device_span_base + 8),
+ _read_u32(view, device_span_base + 16),
+ )
+
+ def device_route(self, Py_ssize_t device_id):
+ """Return table id and logical-path string id for one device."""
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t base
+ if device_id < 0 or device_id >= self._section_counts[6]:
+ raise IndexError(device_id)
+ base = self._section_offsets[6] + <uint64_t>device_id * self._section_sizes[6]
+ return _read_u32(view, base), _read_u32(view, base + 4)
+
+ def table_name_id(self, Py_ssize_t table_id):
+ """Return the string-pool id for one table name."""
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t base
+ if table_id < 0 or table_id >= self._section_counts[4]:
+ raise IndexError(table_id)
+ base = self._section_offsets[4] + <uint64_t>table_id * self._section_sizes[4]
+ return _read_u32(view, base)
+
+ def column_name_id(self, Py_ssize_t column_id):
+ """Return the string-pool id for one column name."""
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t base
+ if column_id < 0 or column_id >= self._section_counts[8]:
+ raise IndexError(column_id)
+ base = self._section_offsets[8] + <uint64_t>column_id * self._section_sizes[8]
+ return _read_u32(view, base + 4)
+
+ def device_time_bounds(self, Py_ssize_t device_id):
+ """Return min/max timestamps for one device."""
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t base
+ if device_id < 0 or device_id >= self._section_counts[6]:
+ raise IndexError(device_id)
+ base = self._section_offsets[6] + <uint64_t>device_id * self._section_sizes[6]
+ return (
+ <int64_t>_read_u64(view, base + 32),
+ <int64_t>_read_u64(view, base + 40),
+ )
+
+ def string(self, Py_ssize_t string_id):
+ """Decode one string pool entry without a temporary bytes slice."""
+ cdef const uint8_t[:] view = self._view
+ cdef uint64_t offsets_base = self._section_offsets[1]
+ cdef uint64_t strings_base = self._section_offsets[2]
+ cdef uint32_t string_count = self._section_counts[1]
+ cdef uint32_t start
+ cdef uint32_t end
+ cdef const char* data
+ if string_id < 0 or string_id + 1 >= string_count:
+ raise IndexError(string_id)
+ start = _read_u32(view, offsets_base + <uint64_t>string_id * 4)
+ end = _read_u32(view, offsets_base + <uint64_t>(string_id + 1) * 4)
+ data = <const char*>&view[strings_base + start]
+ return PyUnicode_DecodeUTF8(data, end - start, "strict")
diff --git a/python/tsfile/dataset/index.py b/python/tsfile/dataset/index.py
index 4bd9c37..9b4ba53 100644
--- a/python/tsfile/dataset/index.py
+++ b/python/tsfile/dataset/index.py
@@ -36,6 +36,7 @@
from ..constants import ColumnCategory
from .metadata import MODEL_TREE, _join_series_path
+from ._index import IndexLookup
MAGIC = b"TSIDX\0\0\0"
VERSION_MAJOR = 1
@@ -229,6 +230,7 @@
def __init__(self, path: str, verify_sections: bool = False):
self.path = path
+ self._lookup = None
self._file = open(path, "rb")
try:
stat = os.fstat(self._file.fileno())
@@ -241,7 +243,9 @@
self._mmap = mmap.mmap(self._file.fileno(), 0, access=mmap.ACCESS_READ)
self._view = memoryview(self._mmap)
self._entries = self._validate(verify_sections)
+ self._lookup = IndexLookup(self._view, self._entries)
except Exception:
+ self._lookup = None
if getattr(self, "_view", None) is not None:
self._view.release()
self._view = None
@@ -336,6 +340,7 @@
return entries
def close(self):
+ self._lookup = None
if getattr(self, "_view", None) is not None:
self._view.release()
self._view = None
@@ -383,7 +388,7 @@
return bytes(self._view[strings[2] + start : strings[2] + end])
def string(self, sid: int) -> str:
- return self.string_bytes(sid).decode("utf-8")
+ return self._lookup.string(sid)
def _equal_hash_range(self, section_type: int, hash_index: int, value_hash: int):
low = 0
@@ -411,54 +416,50 @@
if self.string_bytes(sid) == encoded
]
- def _find_child(
- self, section_type: int, table_id: int, name: str, first: int, count: int
- ):
- encoded = name.encode("utf-8")
- target_hash = name_hash(encoded)
- low, high = first, first + count
- while low < high:
- middle = low + (high - low) // 2
- row = self.record(section_type, middle)
- key = (row[0], row[2], self.string_bytes(row[3]))
- target = (table_id, target_hash, encoded)
- if key < target:
- low = middle + 1
- else:
- high = middle
- if low < first + count:
- row = self.record(section_type, low)
- if (
- row[0] == table_id
- and row[2] == target_hash
- and self.string_bytes(row[3]) == encoded
- ):
- return row[1]
- raise KeyError(name)
-
def find_device_id(self, table_id: int, name: str) -> int:
- table = self.record(TABLE_RECORD, table_id)
- return self._find_child(DEVICE_NAME_INDEX, table_id, name, table[2], table[3])
+ return self._lookup.find_device_id(table_id, name)
def find_column_id(self, table_id: int, name: str) -> int:
- table = self.record(TABLE_RECORD, table_id)
- return self._find_child(COLUMN_NAME_INDEX, table_id, name, table[4], table[5])
+ return self._lookup.find_column_id(table_id, name)
def find_series_id(self, device_id: int, column_id: int) -> int:
- device = self.record(DEVICE_RECORD, device_id)
- low, high = device[4], device[4] + device[5]
- while low < high:
- middle = low + (high - low) // 2
- if self.record(LOGICAL_SERIES, middle)[1] < column_id:
- low = middle + 1
- else:
- high = middle
- if (
- low < device[4] + device[5]
- and self.record(LOGICAL_SERIES, low)[1] == column_id
- ):
- return low
- raise KeyError(column_id)
+ return self._lookup.find_series_id(device_id, column_id)
+
+ def describe_series(self, series_id: int):
+ """Return scalar route metadata for one logical series."""
+ return self._lookup.describe_series(series_id)
+
+ def series_identity(self, series_id: int):
+ """Return device and column ids for one logical series."""
+ return self._lookup.series_identity(series_id)
+
+ def find_series_span(self, series_id: int, file_id: int):
+ """Return one series span without exposing its full record tuple."""
+ return self._lookup.find_series_span(series_id, file_id)
+
+ def locator_metadata(self, locator_id: int):
+ """Return locator/device-span fields needed by the runtime reader."""
+ return self._lookup.locator_metadata(locator_id)
+
+ def prepared_locator_metadata(self, file_id: int, locator_id: int):
+ """Return generation and locator fields used by native prepare."""
+ return self._lookup.prepared_locator_metadata(file_id, locator_id)
+
+ def device_route(self, device_id: int):
+ """Return table id and logical-path string id for one device."""
+ return self._lookup.device_route(device_id)
+
+ def table_name_id(self, table_id: int):
+ """Return the string-pool id for one table name."""
+ return self._lookup.table_name_id(table_id)
+
+ def column_name_id(self, column_id: int):
+ """Return the string-pool id for one column name."""
+ return self._lookup.column_name_id(column_id)
+
+ def device_time_bounds(self, device_id: int):
+ """Return min/max timestamps for one device."""
+ return self._lookup.device_time_bounds(device_id)
def index_path_for(paths: Sequence[str]) -> str:
diff --git a/python/tsfile/dataset/runtime.py b/python/tsfile/dataset/runtime.py
index 3ce096b..dca1f59 100644
--- a/python/tsfile/dataset/runtime.py
+++ b/python/tsfile/dataset/runtime.py
@@ -25,6 +25,7 @@
from concurrent.futures import ThreadPoolExecutor, wait
from dataclasses import dataclass
import os
+import sys
import threading
from typing import Dict, Optional, Tuple
@@ -57,9 +58,10 @@
from .merge import build_aligned_matrix
_SERIES_DESCRIPTOR_CACHE_SIZE = 4096
+_DATACLASS_SLOTS = {"slots": True} if sys.version_info >= (3, 10) else {}
-@dataclass(frozen=True)
+@dataclass(frozen=True, **_DATACLASS_SLOTS)
class RuntimeSeriesShard:
"""One immutable physical fragment already expanded from the mmap route."""
@@ -72,7 +74,7 @@
max_time: int
-@dataclass(frozen=True)
+@dataclass(frozen=True, **_DATACLASS_SLOTS)
class RuntimeSeriesDescriptor:
"""Bounded process-local expansion of one logical series route."""
@@ -260,23 +262,28 @@
self._closed = False
def _locator_tuple(self, file_id, locator_id):
- locator = self._index.record(SERIES_LOCATOR, locator_id)
- device_span = self._index.record(DEVICE_FILE_SPAN, locator[0])
- file_record = self._index.record(TSFILE_RECORD, file_id)
- if device_span[1] != file_id:
- raise ValueError("series locator points at another TsFile")
+ (
+ file_size,
+ file_fingerprint_value,
+ locator_flags,
+ locator_layout,
+ locator_offset,
+ locator_length,
+ device_span_offset,
+ device_span_length,
+ ) = self._index.prepared_locator_metadata(file_id, locator_id)
return (
id(self._index),
file_id,
- file_record[2],
- file_record[3],
+ file_size,
+ file_fingerprint_value,
locator_id,
- locator[1],
- locator[2],
- locator[3],
- locator[4],
- device_span[2],
- device_span[3],
+ locator_flags,
+ locator_layout,
+ locator_offset,
+ locator_length,
+ device_span_offset,
+ device_span_length,
)
def get(self, file_id, locator_id, reader, time_owner=None):
@@ -594,12 +601,12 @@
def _series_id(self, ref):
device_id, field_idx = ref
- device = self._catalog.index.record(DEVICE_RECORD, device_id)
+ table_id, _path_string_id = self._catalog.index.device_route(device_id)
table_name = self._catalog.index.string(
- self._catalog.index.record(TABLE_RECORD, device[0])[0]
+ self._catalog.index.table_name_id(table_id)
)
field_name = self._catalog.table_entries[table_name].field_columns[field_idx]
- column_id = self._catalog.index.find_column_id(device[0], field_name)
+ column_id = self._catalog.index.find_column_id(table_id, field_name)
return self._catalog.index.find_series_id(device_id, column_id)
def describe(self, ref, series_id=None, column_id=None):
@@ -611,31 +618,32 @@
if series_id is None:
series_id = self._series_id(ref)
- series = self._catalog.index.record(LOGICAL_SERIES, series_id)
- if series[0] != ref[0]:
+ (
+ series_device_id,
+ described_column_id,
+ series_min_time,
+ series_max_time,
+ count,
+ shard_infos,
+ ) = self._catalog.index.describe_series(series_id)
+ if series_device_id != ref[0]:
raise KeyError(ref)
if column_id is None:
- column_id = series[1]
- elif series[1] != column_id:
+ column_id = described_column_id
+ elif described_column_id != column_id:
raise KeyError(ref)
shards = []
- count = 0
- for span_id in range(series[2], series[2] + series[3]):
- span = self._catalog.index.record(SERIES_FILE_SPAN, span_id)
- locator = self._catalog.index.record(SERIES_LOCATOR, span[2])
- device_span = self._catalog.index.record(DEVICE_FILE_SPAN, locator[0])
- timeline_length = device_span[6] if device_span[4] == 1 else span[6]
- count += timeline_length
+ for file_id, locator_id, timeline_length, min_time, max_time in shard_infos:
shards.append(
RuntimeSeriesShard(
- self._catalog.reader_for(span[1]),
- series[0],
+ self._catalog.reader_for(file_id),
+ series_device_id,
column_id,
- span[2],
+ locator_id,
timeline_length,
- span[4],
- span[5],
+ min_time,
+ max_time,
)
)
@@ -644,8 +652,8 @@
series_id,
column_id,
tuple(shards),
- series[4] if count else None,
- series[5] if count else None,
+ series_min_time if count else None,
+ series_max_time if count else None,
count,
)
if self._cache_size:
@@ -727,11 +735,11 @@
def resolve_series_descriptor_by_id(self, series_id, table_name, field_name):
if series_id < 0 or series_id >= self.index.count(LOGICAL_SERIES):
raise KeyError(series_id)
- series = self.index.record(LOGICAL_SERIES, series_id)
+ device_id, column_id = self.index.series_identity(series_id)
table = self.table_entries[table_name]
field_idx = table.get_field_index(field_name)
return self.series_shards.describe(
- (series[0], field_idx), series_id=series_id, column_id=series[1]
+ (device_id, field_idx), series_id=series_id, column_id=column_id
)
def _infer_model(self):
@@ -760,8 +768,7 @@
return len(self._catalog.devices)
def __getitem__(self, device_id):
- record = self._catalog.index.record(DEVICE_RECORD, device_id)
- return record[8], record[9]
+ return self._catalog.index.device_time_bounds(device_id)
class RuntimeSeriesReader:
@@ -776,49 +783,50 @@
def _span(self, device_id, column_id):
series_id = self._series(device_id, column_id)
- series = self.runtime.index.record(LOGICAL_SERIES, series_id)
- for span_id in range(series[2], series[2] + series[3]):
- span = self.runtime.index.record(SERIES_FILE_SPAN, span_id)
- if span[1] == self.file_id:
- return span
- raise KeyError((device_id, column_id, self.file_id))
+ return self.runtime.index.find_series_span(series_id, self.file_id)
def _identity(self, device_id, column_id):
index = self.runtime.index
- device = index.record(DEVICE_RECORD, device_id)
- table = index.record(TABLE_RECORD, device[0])
- table_name = index.string(table[0])
- components = split_logical_series_path(index.string(device[1]))
+ table_id, path_string_id = index.device_route(device_id)
+ table_name = index.string(index.table_name_id(table_id))
+ components = split_logical_series_path(index.string(path_string_id))
tags = tuple(components[1:-1])
table_entry = self.runtime.catalog.table_entries[table_name]
- column_name = index.string(index.record(COLUMN_SCHEMA, column_id)[1])
+ column_name = index.string(index.column_name_id(column_id))
return table_name, tags, table_entry, column_name
def get_device_info(self, device_id):
- record = self.runtime.index.record(DEVICE_RECORD, device_id)
table_name, tags = self.runtime.catalog.devices[device_id]
table = self.runtime.catalog.table_entries[table_name]
+ min_time, max_time = self.runtime.index.device_time_bounds(device_id)
return {
"table_name": table_name,
"tag_columns": table.tag_columns,
"tag_values": dict(zip(table.tag_columns, tags)),
- "min_time": record[8],
- "max_time": record[9],
+ "min_time": min_time,
+ "max_time": max_time,
}
def get_series_info_by_ref(self, device_id, column_id):
- span = self._span(device_id, column_id)
- locator = self.runtime.index.record(SERIES_LOCATOR, span[2])
- device_span = self.runtime.index.record(DEVICE_FILE_SPAN, locator[0])
+ locator_id, span_min_time, span_max_time, span_length = self._span(
+ device_id, column_id
+ )
+ (
+ _device_span_id,
+ _locator_flags,
+ _file_id,
+ layout,
+ device_timeline_length,
+ ) = self.runtime.index.locator_metadata(locator_id)
table_name, tags, table, column_name = self._identity(device_id, column_id)
- timeline_length = device_span[6] if device_span[4] == 1 else span[6]
+ timeline_length = device_timeline_length if layout == 1 else span_length
return {
"length": timeline_length,
- "min_time": span[4],
- "max_time": span[5],
+ "min_time": span_min_time,
+ "max_time": span_max_time,
"timeline_length": timeline_length,
- "timeline_min_time": span[4],
- "timeline_max_time": span[5],
+ "timeline_min_time": span_min_time,
+ "timeline_max_time": span_max_time,
"table_name": table_name,
"column_name": column_name,
"device_id": device_id,
@@ -904,7 +912,7 @@
):
span = self._span(device_id, column_id)
return self._query_at_locator(
- span[2],
+ span[0],
start_time=start_time,
end_time=end_time,
offset=offset,
@@ -950,19 +958,17 @@
return np.array([], dtype=np.int64), {}
spans = [self._span(device_id, column_id) for column_id in column_ids]
- locators = [
- self.runtime.index.record(SERIES_LOCATOR, span[2]) for span in spans
+ locator_metadata = [
+ self.runtime.index.locator_metadata(span[0]) for span in spans
]
- device_span_ids = {locator[0] for locator in locators}
+ device_span_ids = {metadata[0] for metadata in locator_metadata}
can_read_aligned = len(device_span_ids) == 1
if can_read_aligned:
- device_span = self.runtime.index.record(
- DEVICE_FILE_SPAN, next(iter(device_span_ids))
- )
+ device_span = locator_metadata[0]
can_read_aligned = (
- device_span[1] == self.file_id
- and device_span[4] == 1
- and all(locator[1] == 1 for locator in locators)
+ device_span[2] == self.file_id
+ and device_span[3] == 1
+ and all(metadata[1] == 1 for metadata in locator_metadata)
)
if can_read_aligned:
@@ -974,7 +980,7 @@
time_owner = None
for span in spans:
current = self.runtime.prepared.get(
- self.file_id, span[2], reader, time_owner=time_owner
+ self.file_id, span[0], reader, time_owner=time_owner
)
prepared.append(current)
if time_owner is None: