Skip to content

First last key#6183

Open
MathieuDutSik wants to merge 12 commits into
linera-io:mainfrom
MathieuDutSik:first_last_key
Open

First last key#6183
MathieuDutSik wants to merge 12 commits into
linera-io:mainfrom
MathieuDutSik:first_last_key

Conversation

@MathieuDutSik

Copy link
Copy Markdown
Contributor

Motivation

This is a continuation of the existing PR #6171
by implementing the corrections that I asked.

Proposal

Add find_first_key_by_prefix, find_last_key_by_prefix, and the value variants to ReadableKeyValueStore, with default impls falling back to find_keys_by_prefix(...).first()/.last(). Native overrides:

  • memory: BTreeMap range, O(log N)
  • RocksDB
  • DynamoDB: Query with Limit=1 and ScanIndexForward=true/false
  • IndexedDB: IDBCursor with Next/Prev direction

For the wrappers:

  • Journaling, Metering, Dual are straightforward.
  • The separate linera-storage-service is also straightforward.
  • For the LRU caching, we use the already cached find_keys_by_prefix, find_key_values_by_prefix to return result when possible.

For the ValueSplitting this is harder since we can split some keys and anyway the remaining key could be leftover from previous big values that were deleted:

  • We cannot distinguish leftover keys from legitimate existing segments.
  • So, if the find_first_key_by_prefix returns a key that has index 0, we know it is ok, and we return. Otherwise, we do a find_keys_by_prefix which is expensive but should be very rare.
  • For the find_first_key_value_by_prefix, if the obtained key has index 0 and the obtained value a count of 1, we know it is a legitimate one and we can return. Otherwise, we do an expensive but rare find_key_values_by_prefix.

Test Plan

CI

The test map_view_mutability was modified to use a Value splitting store. The way the keys are used has changed, but this is only because the generated key-set should be prefix-free.

Release Plan

Can be backported to testnet_conway.

Links

None

@github-actions

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown

Instruction Count Benchmark Results

Baseline: 576bb2ebea

Warning

5 instruction-count regression(s) detected (>1% threshold):

  • delete_500_from_1000: +1.06%
  • front_100_from_1000: +4.32%
  • push_1000: +1.06%
  • front_100_from_1000: +2.65%
  • pre_save: +4.90%
Deterministic metrics — reproducible across runs (34 benchmarks)
Benchmark Instructions Total R+W
Cold Load
load_1000 693,857 (+0.00%) 1,010,338 (+0.00%)
CollectionView
indices_100 192,381 (No change) 267,653 (No change)
load_all_100_from_storage 638,209 (No change) 901,177 (No change)
load_all_100_in_memory 340,885 (+0.08%) 477,162 (+0.07%)
pre_save_100 265,880 (+0.05%) 367,671 (+0.05%)
try_load_10_from_100 100,549 (-0.02%) 142,375 (-0.02%)
MapView
contains_key_10_from_100 52,533 (No change) 74,551 (No change)
contains_key_10_from_1000 355,072 (-0.01%) 501,826 (-0.01%)
get_10_from_100 55,031 (+0.47%) 78,197 (+0.45%)
get_10_from_1000 357,605 (-0.01%) 505,527 (-0.01%)
get_100_missing_from_1000 610,084 (No change) 851,233 (No change)
indices_100 100,452 (+0.26%) 138,331 (+0.26%)
indices_1000 948,222 (+0.00%) 1,322,202 (+0.00%)
insert_100 257,264 (No change) 355,714 (No change)
insert_1000 2,963,505 (+0.00%) 4,013,290 (+0.00%)
post_save_1000 1,027,007 (-0.00%) 1,481,018 (-0.00%)
pre_save_100 332,564 (-0.01%) 462,461 (-0.01%)
pre_save_1000 3,381,713 (No change) 4,758,733 (No change)
remove_500_from_1000 1,189,593 (-0.00%) 1,660,493 (-0.00%)
QueueView / BucketQueueView
delete_500_from_1000 22,386 (${\color{red}\textbf{+1.06\%%}}$) 34,344 (+0.96%)
front_100_from_1000 5,680 (${\color{red}\textbf{+4.32\%%}}$) 8,394 (${\color{red}\textbf{+4.07\%%}}$)
pre_save_1000 43,122 (+0.60%) 60,496 (+0.59%)
push_1000 24,367 (${\color{red}\textbf{+1.06\%%}}$) 33,322 (${\color{red}\textbf{+1.07\%%}}$)
delete_500_from_1000 10,243 (No change) 12,351 (No change)
front_100_from_1000 9,116 (${\color{red}\textbf{+2.65\%%}}$) 13,855 (${\color{red}\textbf{+2.42\%%}}$)
pre_save_1000 1,042,881 (-0.00%) 1,498,582 (-0.00%)
push_1000 24,128 (+0.37%) 33,001 (+0.40%)
ReentrantCollectionView
contains_key_10_from_100 141,897 (No change) 201,820 (No change)
indices_100 237,116 (+0.11%) 332,341 (+0.11%)
load_all_100_from_storage 803,377 (-0.00%) 1,132,431 (-0.00%)
load_all_100_in_memory 411,235 (+0.06%) 566,271 (+0.06%)
pre_save_100 350,791 (-0.01%) 488,439 (-0.01%)
RegisterView
get_set_100 81,292 (No change) 120,126 (No change)
pre_save 5,485 (${\color{red}\textbf{+4.90\%%}}$) 8,089 (${\color{red}\textbf{+4.58\%%}}$)

Regression threshold: 1%${\color{red}\textbf{red}}$ = regression, ${\color{green}\textbf{green}}$ = improvement.

Cache-dependent metrics — expect fluctuations between runs (34 benchmarks)
Benchmark L1 Hits LLC Hits RAM Hits Est. Cycles
Cold Load
load_1000 1,001,806 (+0.00%) 8,356 (+0.01%) 176 (+0.57%) 1,049,746 (+0.01%)
CollectionView
indices_100 266,403 (No change) 854 (No change) 396 (No change) 284,533 (No change)
load_all_100_from_storage 896,656 (No change) 3,858 (No change) 663 (No change) 939,151 (No change)
load_all_100_in_memory 475,030 (+0.07%) 1,390 (-0.50%) 742 (${\color{red}\textbf{+1.09\%%}}$) 507,950 (+0.12%)
pre_save_100 365,734 (+0.05%) 1,341 (No change) 596 (No change) 393,299 (+0.05%)
try_load_10_from_100 141,516 (-0.02%) 632 (No change) 227 (-0.44%) 152,621 (-0.04%)
MapView
contains_key_10_from_100 74,251 (-0.00%) 92 (${\color{red}\textbf{+3.37\%%}}$) 208 (No change) 81,991 (+0.01%)
contains_key_10_from_1000 498,643 (-0.01%) 2,976 (No change) 207 (-0.48%) 520,768 (-0.01%)
get_10_from_100 77,896 (+0.44%) 88 (${\color{red}\textbf{+2.33\%%}}$) 213 (${\color{red}\textbf{+3.90\%%}}$) 85,791 (+0.74%)
get_10_from_1000 502,331 (-0.01%) 2,984 (+0.10%) 212 (-0.47%) 524,671 (-0.01%)
get_100_missing_from_1000 848,022 (-0.00%) 2,982 (+0.10%) 229 (No change) 870,947 (+0.00%)
indices_100 137,710 (+0.25%) 225 (No change) 396 (${\color{red}\textbf{+2.06\%%}}$) 152,695 (+0.41%)
indices_1000 1,314,540 (+0.00%) 6,482 (-0.15%) 1,180 (+0.08%) 1,388,250 (+0.00%)
insert_100 354,975 (-0.00%) 88 (${\color{red}\textbf{+1.15\%%}}$) 651 (No change) 378,200 (+0.00%)
insert_1000 4,006,265 (+0.00%) 3,043 (-0.26%) 3,982 (+0.03%) 4,160,850 (+0.00%)
post_save_1000 1,469,631 (-0.00%) 11,205 (+0.02%) 182 (-0.55%) 1,532,026 (-0.00%)
pre_save_100 461,081 (-0.01%) 766 (+0.26%) 614 (-0.16%) 486,401 (-0.01%)
pre_save_1000 4,744,804 (+0.00%) 10,113 (-0.07%) 3,816 (No change) 4,928,929 (-0.00%)
remove_500_from_1000 1,656,113 (-0.00%) 4,203 (+0.02%) 177 (-0.56%) 1,683,323 (-0.00%)
QueueView / BucketQueueView
delete_500_from_1000 34,146 (+0.95%) 36 (${\color{green}\textbf{-2.70\%%}}$) 162 (${\color{red}\textbf{+4.52\%%}}$) 39,996 (${\color{red}\textbf{+1.43\%%}}$)
front_100_from_1000 8,220 (${\color{red}\textbf{+4.01\%%}}$) 38 (${\color{red}\textbf{+11.76\%%}}$) 136 (${\color{red}\textbf{+5.43\%%}}$) 13,170 (${\color{red}\textbf{+4.62\%%}}$)
pre_save_1000 60,153 (+0.58%) 63 (${\color{red}\textbf{+1.61\%%}}$) 280 (${\color{red}\textbf{+2.94\%%}}$) 70,268 (+0.90%)
push_1000 33,106 (${\color{red}\textbf{+1.04\%%}}$) 54 (${\color{red}\textbf{+8.00\%%}}$) 162 (${\color{red}\textbf{+5.19\%%}}$) 39,046 (${\color{red}\textbf{+1.67\%%}}$)
delete_500_from_1000 12,175 (+0.01%) 38 (${\color{green}\textbf{-2.56\%%}}$) 138 (No change) 17,195 (-0.02%)
front_100_from_1000 13,658 (${\color{red}\textbf{+2.39\%%}}$) 35 (${\color{red}\textbf{+6.06\%%}}$) 162 (${\color{red}\textbf{+4.52\%%}}$) 19,503 (${\color{red}\textbf{+3.03\%%}}$)
pre_save_1000 1,493,888 (-0.00%) 2,733 (-0.18%) 1,961 (-0.05%) 1,576,188 (-0.01%)
push_1000 32,792 (+0.37%) 50 (${\color{red}\textbf{+2.04\%%}}$) 159 (${\color{red}\textbf{+4.61\%%}}$) 38,607 (+0.97%)
ReentrantCollectionView
contains_key_10_from_100 200,604 (+0.00%) 1,017 (-0.10%) 199 (No change) 212,654 (-0.00%)
indices_100 330,775 (+0.11%) 1,194 (-0.50%) 372 (${\color{red}\textbf{+2.20\%%}}$) 349,765 (+0.17%)
load_all_100_from_storage 1,125,891 (-0.00%) 6,134 (-0.23%) 406 (-0.25%) 1,170,771 (-0.01%)
load_all_100_in_memory 563,882 (+0.06%) 1,842 (-0.16%) 547 (${\color{red}\textbf{+1.48\%%}}$) 592,237 (+0.10%)
pre_save_100 485,515 (-0.01%) 2,237 (+0.13%) 687 (-0.15%) 520,745 (-0.01%)
RegisterView
get_set_100 119,905 (+0.00%) 39 (${\color{green}\textbf{-4.88\%%}}$) 182 (No change) 126,470 (-0.01%)
pre_save 7,884 (${\color{red}\textbf{+4.56\%%}}$) 43 (${\color{red}\textbf{+4.88\%%}}$) 162 (${\color{red}\textbf{+5.19\%%}}$) 13,769 (${\color{red}\textbf{+4.83\%%}}$)

Cache metrics fluctuate because anything that changes the virtual memory layout
shifts which data lands on which cache lines, changing the L1/LLC/RAM distribution.
Probable causes: ASLR (even across identical binaries), executable binary size changes,
shared library size changes, and even filename length differences.

Cachegrind simulates a two-level cache (L1 + LLC) auto-detected from the host CPU.
Est. Cycles = L1 hits + 5 × LLC hits + 35 × RAM hits.

Runner cache sizes: L1d cache: 64 KiB (2 instances);L1i cache: 64 KiB (2 instances) L2 cache: 2 MiB (2 instances);L3 cache: 32 MiB (1 instance)

rpc ProcessDeleteNamespace (RequestDeleteNamespace) returns (google.protobuf.Empty) {}
rpc ProcessListAll (google.protobuf.Empty) returns (ReplyListAll) {}
rpc ProcessListRootKeys (RequestListRootKeys) returns (ReplyListRootKeys) {}
rpc ProcessDeleteAll (google.protobuf.Empty) returns (google.protobuf.Empty) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Why the Process prefix everywhere?)

let response = response.into_inner();
let ReplyFindLastKeyByPrefix { key } = response;
Ok(key)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods share a lot of code; can that be deduplicated? Maybe they'd just pass in one or two closures to the same general method: what method to call on the client, and possibly what to do with the result?

.find_last_key_value_by_prefix(key_prefix)
.await
.map_err(DualStoreError::Second),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this could also be deduplicated with a single generic method that does the matching and map_err?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants