mirror of
https://github.com/qdrant/qdrant.git
synced 2026-08-06 01:50:57 -05:00
* refactor: move deferred-point ownership into the ID tracker Re-implements the idea from #8512 against current `dev`. Deferred-point state (`deferred_internal_id` + `deferred_deleted_count`) moves out of `Segment.deferred_point_status` and the cached `SparseVectorIndex.deferred_internal_id` field into `PointMappings`, exposed through `IdTrackerRead`. The threshold is set once at `MutableIdTracker::open` time; `PointMappings::drop` now maintains the deleted counter inline (with double-delete protection), removing the manual increment in `delete_point_internal` and the `calculate_deleted_deferred_point_count` rescan. Read paths consume the threshold through the id tracker: - The segment read view drops the `deferred_point_status` field and `with_view` no longer threads it in; `read_view/{deferred,info}.rs` call `self.id_tracker.deferred_*()` directly. - `SparseVectorIndex` no longer stores its own copy and its `update_vector` / search debug-assert read from `self.id_tracker.borrow().deferred_internal_id()`. - `VectorQueryContext.deferred_internal_id` and the `SegmentQueryContext::get_vector_context` parameter are gone; the three downstream readers (`plain_vector_index`, sparse search, sparse `update_vector`) consult their own id tracker. `PointMappingsRefEnum` centralises the dispatch: - `iter_internal_with_behavior(DeferredBehavior)` replaces ad-hoc branches in `iter_filtered_points` impls. - `external_iter_cutoff(DeferredBehavior)` covers iterators sourced outside the mapping (field-index outputs in `struct_payload_index::iter_filtered_points`). - The internal `deferred_internal_id()` accessor is private; the raw threshold no longer leaks to consumers. - `iter_from_visible` / `iter_random_visible` read the mapping's own threshold; callers that previously passed `DeferredBehavior::apply(...)` now branch on `deferred_behavior.include_all_points()` (scroll / order_by) or simply drop the argument (sampling / facet). `PayloadIndexRead::query_points` drops the now-redundant `deferred_internal_id` parameter; `iter_filtered_points` takes `DeferredBehavior` directly so HNSW build/search can request `IncludeAll` while normal reads request `Exclude`. RocksDB-related parts of the original PR are skipped — that tracker is already gone from `dev`. Tests adapted: sites that mutated `segment.deferred_point_status` directly now construct a parallel non-deferred segment via `create_deferred_segment(..., 0)` for comparison; `test_deleted_deferred_point_count` reads counters through the id tracker. See `docs/plans/deferred-points-owned-by-id-tracker.md` for the design write-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(benches): drop stale deferred_internal_id arg from query_points calls The boolean / range / conditional bench files weren't built by `cargo test -p segment`, so they slipped through. `cargo clippy --workspace --all-targets` catches them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: drop id_tracker / point_mappings args from iter_filtered_points Both impls already hold an id tracker on `self`: - `StructPayloadIndexReadView` carries `id_tracker: &'a I`, so `self.id_tracker.point_mappings()` borrows from `'a` and the lazy iterator chain keeps working unchanged. - `PlainPayloadIndex` carries `id_tracker: Arc<AtomicRefCell<...>>`, where the mapping borrow is local; collect into a `Vec` and return `into_iter()`. PlainPayloadIndex::iter_filtered_points has no direct callers — only `query_points` was using it — so eager collection is a non-issue. While here, take `self` by value on `iter_internal_visible`, `iter_from_visible`, `iter_random_visible`, `iter_internal_with_behavior`, and `external_iter_cutoff`. `PointMappingsRefEnum` is `Copy`; this matches the existing `iter_internal` / `iter_from` / `iter_random` shape and lets the iterator outlive a local `let point_mappings = ...;` binding. The HNSW `condition_points` helper drops its now-unused `id_tracker` parameter. All callers (sampling, scroll, order_by, facet ×2, hnsw build/search) just drop the two arguments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: ignore /docs/plans/ and untrack the previously-committed plan `docs/plans/` is a scratch directory for per-feature planning notes — not something we want under source control. Add it to `.gitignore` and drop the deferred-points plan that slipped into history; the design is captured in the PR description. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: replace external_iter_cutoff with filter_deferred iterator wrapper Instead of exposing a raw `Option<PointOffsetType>` cutoff that every caller has to apply with their own `.filter(...)`, give `PointMappingsRefEnum` an iterator wrapper: fn filter_deferred<I: Iterator<Item = PointOffsetType>>( self, iter: I, deferred_behavior: DeferredBehavior, ) -> impl Iterator<Item = PointOffsetType> It returns the iterator unchanged for `IncludeAll` (or when the mapping has no threshold) and otherwise wraps it in a cutoff `.filter`, dispatched via `itertools::Either` so the no-cutoff path stays allocation-free. The struct payload index's `iter_filtered_points` swaps its open-coded filter for a single `point_mappings.filter_deferred(...)` call. The deferred threshold no longer leaks out of `PointMappingsRefEnum`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: move deferred wrapping out of peek_top_all, gate it as test-only `BatchFilteredSearcher::peek_top_all` baked the deferred cutoff into its iterator construction, which was the last place outside `PointMappingsRefEnum` that knew about the threshold. Split the deleted-iteration concern out into a new accessor: fn iter_not_deleted(&self) -> impl Iterator<Item = PointOffsetType> + 'a It borrows `&'a BitSlice` directly (not via `&self`), so callers can chain `filter_deferred` and then move `self` into `peek_top_iter` without lifetime conflicts. Sparse + plain vector index call sites now do: let iter = id_tracker .point_mappings() .filter_deferred(searcher.iter_not_deleted(), DeferredBehavior::Exclude); searcher.peek_top_iter(iter, &is_stopped) leaving `BatchFilteredSearcher` completely ignorant of deferred state. With deferred handling lifted out, `peek_top_all` itself is now used only by tests (3 inline `#[cfg(test)] mod tests`, 1 integration test, 1 bench) — gate it under `#[cfg(feature = "testing")]` to match `new_for_test`. Production code goes through the `iter_not_deleted` + `filter_deferred` + `peek_top_iter` composition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: optimize Segment::retrieve and thread user data through read_vectors Two interlocking changes that together collapse the per-point lookups and intermediate allocations in `Segment::retrieve` down to one external-to-internal pass. ## `IdTrackerRead::resolve_external_ids` (new default trait method) Single-pass translation of a `&[PointIdType]` slice into two parallel vectors `(Vec<PointIdType>, Vec<PointOffsetType>)`. Folds deferred filtering (compare offset against the threshold inline — no separate `point_is_deferred` lookup) and missing-id errors (eager `PointIdError`) into resolution. Lives on the trait so the deferred threshold never leaks out of the id tracker; the parallel-vector shape lets a future batched payload / vector fetcher consume `&offsets` straight without unzipping. The `appendable_flag` guard previously in `point_is_deferred` is gone: non-appendable trackers always carry `deferred_internal_id() == None` (set only via `MutableIdTracker::open`, guarded by the segment constructor), so the check was load-bearing nowhere. ## User-data threading through `read_vectors` `VectorStorageRead::read_vectors` now takes `IntoIterator<Item = (U, PointOffsetType)>` and yields `(U, PointOffsetType, CowVector)`. The user-data tag rides alongside each offset all the way through, so callers can map results back into a parallel array without keeping a separate `offset → ...` lookup table. - Default trait impl: one-line per-key loop. - Dense impl: `unzip()` into parallel `(Vec<U>, Vec<PointOffsetType>)` in a single pass — same allocation count as before, just U riding alongside. - Enum delegations (`VectorStorageEnum`, `VectorStorageReadEnum`) forward unchanged. - `for_each_in_batch` and below stay untouched. `SegmentReadView::vectors_by_offsets<U: Copy>` becomes a lazy filter chain — no parallel `Vec<(orig_idx, offset)>` allocation. The dead `SegmentReadView::read_vectors` helper is removed. ## `Segment::retrieve` end-to-end Per N points / V vectors / payload: | Operation | Before | After | |----------------------------|---------------------|-------| | `id_tracker.internal_id` | N × (1 + V + 1) | N | | `id_tracker.external_id` | N × V | 0 | | `point_is_deferred` | N (when applicable) | 0 | | `offset_to_id` HashMap | N entries | none | | `Vec` in `vectors_by_offsets` | 1 | 0 | The vectors stage passes the external id as `read_vectors`'s user data — the callback gets `id` directly without any index lookup. The payload stage uses `payload_by_offset` against the already-resolved offsets. The shape is also batch-friendly: swapping in a future `IdTrackerRead::batch_internal_id` or `payload_index.batch_get_payload` needs no changes outside the two call sites. ## Behavioural notes - Missing-id now errors eagerly inside resolution, instead of in the vectors stage (`WithVector::Bool(true)` / `Selector`) or payload stage (`with_payload.enable`). The previous `WithVector::Bool(false)` + no-payload path silently inserted an empty record; that is now also an error. None of the existing callers (search post-processing, external retrieve API, the deferred-points test on tests/mod.rs:1179) pass non-existent ids. - Added a per-payload `check_stopped`; the vectors stage already had `stop_if` on its iterator chain. - `vector_by_offset` (the single-element helper) passes `()` as the no-op user data. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style: apply rustfmt to optimised retrieve / read_vectors paths Pre-push hook failure on the previous commit was rustfmt. Same content, formatted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * do not error out on missing points in retrieve --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
329 lines
12 KiB
Rust
329 lines
12 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::atomic::AtomicBool;
|
|
|
|
use ahash::AHashSet;
|
|
use common::counter::hardware_counter::HardwareCounterCell;
|
|
use common::types::TelemetryDetail;
|
|
use itertools::Itertools;
|
|
use rand::prelude::StdRng;
|
|
use rand::{Rng, RngExt, SeedableRng};
|
|
use segment::data_types::named_vectors::NamedVectors;
|
|
use segment::data_types::query_context::{QueryContext, VectorQueryContext};
|
|
use segment::data_types::vectors::{QueryVector, VectorElementType, VectorInternal};
|
|
use segment::entry::entry_point::SegmentEntry;
|
|
use segment::fixtures::payload_fixtures::random_vector;
|
|
use segment::index::VectorIndexRead;
|
|
use segment::index::sparse_index::sparse_index_config::{SparseIndexConfig, SparseIndexType};
|
|
use segment::index::sparse_index::sparse_vector_index::SparseVectorIndexOpenArgs;
|
|
use segment::segment_constructor::{build_segment, create_sparse_vector_index_test};
|
|
use segment::types::{
|
|
Condition, DEFAULT_SPARSE_FULL_SCAN_THRESHOLD, Distance, ExtendedPointId, Filter,
|
|
HasIdCondition, Indexes, PointIdType, SegmentConfig, SeqNumberType, SparseVectorDataConfig,
|
|
SparseVectorStorageType, VectorDataConfig, VectorStorageDatatype, VectorStorageType,
|
|
};
|
|
use segment::vector_storage::query::{ContextPair, DiscoverQuery};
|
|
use sparse::common::sparse_vector::SparseVector;
|
|
use tempfile::Builder;
|
|
|
|
use crate::fixtures::segment::SPARSE_VECTOR_NAME;
|
|
|
|
const MAX_EXAMPLE_PAIRS: usize = 3;
|
|
|
|
fn convert_to_sparse_vector(vector: &[VectorElementType]) -> SparseVector {
|
|
let mut sparse_vector = SparseVector::default();
|
|
for (idx, value) in vector.iter().enumerate() {
|
|
sparse_vector.indices.push(idx as u32);
|
|
sparse_vector.values.push(*value);
|
|
}
|
|
sparse_vector
|
|
}
|
|
|
|
fn random_named_vector<R: Rng + ?Sized>(
|
|
rnd: &mut R,
|
|
dim: usize,
|
|
) -> (NamedVectors<'_>, NamedVectors<'_>) {
|
|
let dense_vector = random_vector(rnd, dim);
|
|
let sparse_vector = convert_to_sparse_vector(&dense_vector);
|
|
|
|
let mut sparse_result = NamedVectors::default();
|
|
sparse_result.insert(SPARSE_VECTOR_NAME.to_owned(), sparse_vector.into());
|
|
|
|
let mut dense_result = NamedVectors::default();
|
|
dense_result.insert(SPARSE_VECTOR_NAME.to_owned(), dense_vector.into());
|
|
|
|
(sparse_result, dense_result)
|
|
}
|
|
|
|
fn random_discover_query<R: Rng + ?Sized>(rnd: &mut R, dim: usize) -> (QueryVector, QueryVector) {
|
|
let num_pairs: usize = rnd.random_range(1..MAX_EXAMPLE_PAIRS);
|
|
let dense_target = random_vector(rnd, dim);
|
|
let sparse_target = convert_to_sparse_vector(&dense_target);
|
|
|
|
let dense_pairs = (0..num_pairs)
|
|
.map(|_| {
|
|
let positive = random_vector(rnd, dim);
|
|
let negative = random_vector(rnd, dim);
|
|
(positive, negative)
|
|
})
|
|
.collect_vec();
|
|
let sparse_pairs = (0..num_pairs)
|
|
.map(|i| {
|
|
let positive = convert_to_sparse_vector(&dense_pairs[i].0);
|
|
let negative = convert_to_sparse_vector(&dense_pairs[i].1);
|
|
(positive, negative)
|
|
})
|
|
.collect_vec();
|
|
|
|
let dense_query = DiscoverQuery::new(
|
|
dense_target.into(),
|
|
dense_pairs
|
|
.into_iter()
|
|
.map(|(positive, negative)| ContextPair {
|
|
positive: positive.into(),
|
|
negative: negative.into(),
|
|
})
|
|
.collect(),
|
|
)
|
|
.into();
|
|
let sparse_query = DiscoverQuery::new(
|
|
sparse_target.into(),
|
|
sparse_pairs
|
|
.into_iter()
|
|
.map(|(positive, negative)| ContextPair {
|
|
positive: positive.into(),
|
|
negative: negative.into(),
|
|
})
|
|
.collect(),
|
|
)
|
|
.into();
|
|
|
|
(sparse_query, dense_query)
|
|
}
|
|
|
|
fn random_nearest_query<R: Rng + ?Sized>(rnd: &mut R, dim: usize) -> (QueryVector, QueryVector) {
|
|
let dense_target = random_vector(rnd, dim);
|
|
let sparse_target = convert_to_sparse_vector(&dense_target);
|
|
(sparse_target.into(), dense_target.into())
|
|
}
|
|
|
|
#[test]
|
|
fn sparse_index_discover_test() {
|
|
let stopped = AtomicBool::new(false);
|
|
|
|
let dim = 8;
|
|
let num_vectors: u64 = 5_000;
|
|
let distance = Distance::Dot;
|
|
|
|
let mut rnd = StdRng::seed_from_u64(42);
|
|
|
|
let dir = Builder::new().prefix("segment_dir").tempdir().unwrap();
|
|
let index_dir = Builder::new().prefix("hnsw_dir").tempdir().unwrap();
|
|
|
|
let sparse_config = SegmentConfig {
|
|
vector_data: Default::default(),
|
|
sparse_vector_data: HashMap::from([(
|
|
SPARSE_VECTOR_NAME.to_owned(),
|
|
SparseVectorDataConfig {
|
|
index: SparseIndexConfig {
|
|
full_scan_threshold: Some(DEFAULT_SPARSE_FULL_SCAN_THRESHOLD),
|
|
index_type: SparseIndexType::MutableRam,
|
|
datatype: Some(VectorStorageDatatype::Float32),
|
|
},
|
|
storage_type: SparseVectorStorageType::default(),
|
|
modifier: None,
|
|
},
|
|
)]),
|
|
payload_storage_type: Default::default(),
|
|
};
|
|
let dense_config = SegmentConfig {
|
|
vector_data: HashMap::from([(
|
|
SPARSE_VECTOR_NAME.to_owned(),
|
|
VectorDataConfig {
|
|
size: dim,
|
|
distance,
|
|
storage_type: VectorStorageType::default(),
|
|
index: Indexes::Plain {},
|
|
quantization_config: None,
|
|
multivector_config: None,
|
|
datatype: None,
|
|
},
|
|
)]),
|
|
payload_storage_type: Default::default(),
|
|
sparse_vector_data: Default::default(),
|
|
};
|
|
|
|
let mut sparse_segment = build_segment(dir.path(), &sparse_config, None, true).unwrap();
|
|
let mut dense_segment = build_segment(dir.path(), &dense_config, None, true).unwrap();
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
for n in 0..num_vectors {
|
|
let (sparse_vector, dense_vector) = random_named_vector(&mut rnd, dim);
|
|
|
|
let idx = n.into();
|
|
sparse_segment
|
|
.upsert_point(n as SeqNumberType, idx, sparse_vector, &hw_counter)
|
|
.unwrap();
|
|
dense_segment
|
|
.upsert_point(n as SeqNumberType, idx, dense_vector, &hw_counter)
|
|
.unwrap();
|
|
}
|
|
|
|
let payload_index_ptr = sparse_segment.payload_index.clone();
|
|
|
|
let vector_storage = &sparse_segment.vector_data[SPARSE_VECTOR_NAME].vector_storage;
|
|
let sparse_index = create_sparse_vector_index_test(SparseVectorIndexOpenArgs {
|
|
config: SparseIndexConfig {
|
|
full_scan_threshold: Some(DEFAULT_SPARSE_FULL_SCAN_THRESHOLD),
|
|
index_type: SparseIndexType::ImmutableRam,
|
|
datatype: Some(VectorStorageDatatype::Float32),
|
|
},
|
|
id_tracker: sparse_segment.id_tracker.clone(),
|
|
vector_storage: vector_storage.clone(),
|
|
payload_index: payload_index_ptr,
|
|
path: index_dir.path(),
|
|
stopped: &stopped,
|
|
tick_progress: || (),
|
|
})
|
|
.unwrap();
|
|
|
|
let top = 3;
|
|
let attempts = 100;
|
|
for i in 0..attempts {
|
|
// do discover search
|
|
let (sparse_query, dense_query) = random_discover_query(&mut rnd, dim);
|
|
|
|
let vec_context = VectorQueryContext::default();
|
|
let sparse_discover_result = sparse_index
|
|
.search(&[&sparse_query], None, top, None, &vec_context)
|
|
.unwrap();
|
|
|
|
let dense_discover_result = dense_segment.vector_data[SPARSE_VECTOR_NAME]
|
|
.vector_index
|
|
.borrow()
|
|
.search(&[&dense_query], None, top, None, &vec_context)
|
|
.unwrap();
|
|
|
|
// check id only because scores can be epsilon-size different
|
|
assert_eq!(
|
|
sparse_discover_result[0]
|
|
.iter()
|
|
.map(|r| r.idx)
|
|
.collect_vec(),
|
|
dense_discover_result[0].iter().map(|r| r.idx).collect_vec(),
|
|
);
|
|
|
|
// do regular nearest search
|
|
let (sparse_query, dense_query) = random_nearest_query(&mut rnd, dim);
|
|
|
|
let query_context = QueryContext::default();
|
|
let segment_query_context = query_context.get_segment_query_context();
|
|
let vector_context = segment_query_context.get_vector_context(SPARSE_VECTOR_NAME);
|
|
|
|
let sparse_search_result = sparse_index
|
|
.search(&[&sparse_query], None, top, None, &vector_context)
|
|
.unwrap();
|
|
|
|
let cpu_usage = query_context.hardware_usage_accumulator().get_cpu();
|
|
assert!(cpu_usage > 0);
|
|
|
|
let dense_search_result = dense_segment.vector_data[SPARSE_VECTOR_NAME]
|
|
.vector_index
|
|
.borrow()
|
|
.search(&[&dense_query], None, top, None, &vector_context)
|
|
.unwrap();
|
|
|
|
// check that nearest search uses sparse index
|
|
let telemetry = sparse_index.get_telemetry_data(TelemetryDetail::default());
|
|
assert_eq!(telemetry.unfiltered_sparse.count, i + 1);
|
|
|
|
// check id only because scores can be epsilon-size different
|
|
assert_eq!(
|
|
sparse_search_result[0].iter().map(|r| r.idx).collect_vec(),
|
|
dense_search_result[0].iter().map(|r| r.idx).collect_vec(),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn sparse_index_hardware_measurement_test() {
|
|
let stopped = AtomicBool::new(false);
|
|
|
|
let dim = 8;
|
|
let num_vectors: u64 = 5_000;
|
|
|
|
let mut rnd = StdRng::seed_from_u64(42);
|
|
|
|
let dir = Builder::new().prefix("segment_dir").tempdir().unwrap();
|
|
let index_dir = Builder::new().prefix("hnsw_dir").tempdir().unwrap();
|
|
|
|
let sparse_config = SegmentConfig {
|
|
vector_data: Default::default(),
|
|
sparse_vector_data: HashMap::from([(
|
|
SPARSE_VECTOR_NAME.to_owned(),
|
|
SparseVectorDataConfig {
|
|
index: SparseIndexConfig {
|
|
full_scan_threshold: Some(DEFAULT_SPARSE_FULL_SCAN_THRESHOLD),
|
|
index_type: SparseIndexType::MutableRam,
|
|
datatype: Some(VectorStorageDatatype::Float32),
|
|
},
|
|
storage_type: SparseVectorStorageType::default(),
|
|
modifier: None,
|
|
},
|
|
)]),
|
|
payload_storage_type: Default::default(),
|
|
};
|
|
|
|
let mut sparse_segment = build_segment(dir.path(), &sparse_config, None, true).unwrap();
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
for n in 0..num_vectors {
|
|
let (sparse_vector, _) = random_named_vector(&mut rnd, dim);
|
|
|
|
let idx = n.into();
|
|
sparse_segment
|
|
.upsert_point(n as SeqNumberType, idx, sparse_vector, &hw_counter)
|
|
.unwrap();
|
|
}
|
|
let payload_index_ptr = sparse_segment.payload_index.clone();
|
|
|
|
let vector_storage = &sparse_segment.vector_data[SPARSE_VECTOR_NAME].vector_storage;
|
|
let sparse_index = create_sparse_vector_index_test(SparseVectorIndexOpenArgs {
|
|
config: SparseIndexConfig {
|
|
full_scan_threshold: Some(DEFAULT_SPARSE_FULL_SCAN_THRESHOLD),
|
|
index_type: SparseIndexType::ImmutableRam,
|
|
datatype: Some(VectorStorageDatatype::Float32),
|
|
},
|
|
id_tracker: sparse_segment.id_tracker.clone(),
|
|
vector_storage: vector_storage.clone(),
|
|
payload_index: payload_index_ptr,
|
|
path: index_dir.path(),
|
|
stopped: &stopped,
|
|
tick_progress: || (),
|
|
})
|
|
.unwrap();
|
|
|
|
let query_vec = QueryVector::Nearest(VectorInternal::Sparse(
|
|
SparseVector::new(vec![0, 1, 2], vec![42.0, 42.42, 42.4242]).unwrap(),
|
|
));
|
|
|
|
let query_context = QueryContext::default();
|
|
let segment_query_context = query_context.get_segment_query_context();
|
|
let vector_context = segment_query_context.get_vector_context(SPARSE_VECTOR_NAME);
|
|
|
|
let cpu_usage = query_context.hardware_usage_accumulator().get_cpu();
|
|
assert_eq!(cpu_usage, 0);
|
|
|
|
// Some filter so we do plain sparse search
|
|
let ids: AHashSet<PointIdType> = (0..3).map(ExtendedPointId::NumId).collect();
|
|
let filter = Filter::new_must(Condition::HasId(HasIdCondition::from(ids)));
|
|
|
|
sparse_index
|
|
.search(&[&query_vec], Some(&filter), 1, None, &vector_context)
|
|
.unwrap();
|
|
|
|
let cpu_usage = query_context.hardware_usage_accumulator().get_cpu();
|
|
assert!(cpu_usage > 0);
|
|
}
|