Skip to content

Commit 6d21920

Browse files
cjhopmanmeta-codesync[bot]
authored andcommitted
Make OccupiedGraphNode value page-able via PagableNodeValue
Summary: Introduces a `PagableNodeValue` enum (`Hydrated(DiceValidValue)` / `PagedOut(DataKey)`) and stores it as `OccupiedGraphNode.res`. Adds two new `VersionedGraphResult` variants — `MatchPagedOut(PagedOutMatch)` and `CheckDepsPagedOut(PagedOutMismatch)` — that the graph emits when the node's value is paged out, carrying the `DataKey` the worker needs to hydrate. `OccupiedGraphNode::val()` now returns `&PagableNodeValue`; callers needing the hydrated value call `.expect_hydrated(msg)` with a message explaining why they know the value is hydrated (analogous to `Option::expect`). No code path actually constructs `PagableNodeValue::PagedOut` yet, so the new lookup variants are unreachable in practice. The follow-up commit wires up `Dice::page_out` (which transitions nodes to `PagedOut`) and the worker hydration step (which consumes the new variants). Tests pass unchanged. Reviewed By: jtbraun Differential Revision: D101759758 fbshipit-source-id: 2efd54a402bc01aecc3f3c62d8de7d622c70fc67
1 parent 10758fa commit 6d21920

4 files changed

Lines changed: 174 additions & 17 deletions

File tree

dice/dice/src/impls/core/graph/nodes.rs

Lines changed: 116 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ use allocative::Allocative;
2525
use dupe::Dupe;
2626
use gazebo::variants::UnpackVariants;
2727
use itertools::Itertools;
28+
use pagable::DataKey;
2829
use sorted_vector_map::SortedVectorMap;
2930

31+
use super::types::PagedOutMatch;
32+
use super::types::PagedOutMismatch;
3033
use super::types::VersionedGraphResult;
3134
use crate::HashSet;
3235
use crate::api::key::InvalidationSourcePriority;
@@ -162,7 +165,10 @@ impl VersionedGraphNode {
162165
VersionedGraphNode::Occupied(entry) if reusable.is_reusable(&value, &deps, entry) => {
163166
debug!("marking graph entry as unchanged");
164167
entry.mark_unchanged(key.v, valid_deps_versions, invalidation_paths);
165-
let ret = entry.computed_val(key.v);
168+
let ret = entry.computed_val(
169+
key.v,
170+
"is_reusable returned true, which only happens for hydrated entries",
171+
);
166172
return (ret, false);
167173
}
168174
VersionedGraphNode::Occupied(entry) => {
@@ -214,7 +220,10 @@ impl VersionedGraphNode {
214220
dirtied_history.clone(),
215221
invalidation_paths,
216222
);
217-
let ret = new.computed_val(key.v);
223+
let ret = new.computed_val(
224+
key.v,
225+
"newly-constructed OccupiedGraphNode is always hydrated",
226+
);
218227
*self = VersionedGraphNode::Occupied(new);
219228

220229
(ret, true)
@@ -298,11 +307,52 @@ pub(crate) enum InvalidateResult<'a> {
298307
Changed(Option<std::vec::Drain<'a, DiceKey>>),
299308
}
300309

310+
/// The stored value for an `OccupiedGraphNode`. At least one of `value` (the
311+
/// in-memory hydrated form) and `data_key` (the on-disk content-addressable
312+
/// reference) must be set. When both are set, the value is resident in memory
313+
/// AND known to be persisted on disk, so the next page-out can skip
314+
/// re-serialization.
315+
#[derive(Allocative, Debug)]
316+
pub(crate) struct PagableNodeValue {
317+
value: Option<DiceValidValue>,
318+
data_key: Option<DataKey>,
319+
}
320+
321+
impl PagableNodeValue {
322+
/// Constructs a hydrated-only value (no on-disk copy yet).
323+
pub(crate) fn hydrated(value: DiceValidValue) -> Self {
324+
Self {
325+
value: Some(value),
326+
data_key: None,
327+
}
328+
}
329+
330+
/// Returns the hydrated value, panicking with `msg` if the value is not currently
331+
/// resident in memory. `msg` should explain why the caller knows the value is
332+
/// hydrated (analogous to `Option::expect`).
333+
pub(crate) fn expect_hydrated(&self, msg: &str) -> &DiceValidValue {
334+
self.value.as_ref().unwrap_or_else(|| {
335+
panic!(
336+
"PagableNodeValue::expect_hydrated called on a paged-out value: {}",
337+
msg
338+
)
339+
})
340+
}
341+
342+
#[expect(
343+
dead_code,
344+
reason = "used by the page-out flow; D101759759 will remove this suppression"
345+
)]
346+
pub(crate) fn as_hydrated(&self) -> Option<&DiceValidValue> {
347+
self.value.as_ref()
348+
}
349+
}
350+
301351
/// The stored entry of the cache
302352
#[derive(Allocative, Debug)]
303353
pub(crate) struct OccupiedGraphNode {
304354
key: DiceKey,
305-
res: DiceValidValue,
355+
res: PagableNodeValue,
306356
metadata: NodeMetadata,
307357
invalidation_paths: TrackedInvalidationPaths,
308358
}
@@ -436,7 +486,7 @@ impl OccupiedGraphNode {
436486
) -> Self {
437487
Self {
438488
key,
439-
res,
489+
res: PagableNodeValue::hydrated(res),
440490
metadata: NodeMetadata {
441491
deps,
442492
rdeps: LazyDepsSet::new(),
@@ -467,13 +517,33 @@ impl OccupiedGraphNode {
467517
self.invalidation_paths.update(new_invalidation_paths)
468518
}
469519

470-
pub(crate) fn val(&self) -> &DiceValidValue {
520+
/// Returns this node's stored value (which may be hydrated or paged out).
521+
/// Callers that need a hydrated `DiceValidValue` should call `.expect_hydrated(msg)`
522+
/// with a message explaining why the caller knows the value is hydrated.
523+
pub(crate) fn val(&self) -> &PagableNodeValue {
471524
&self.res
472525
}
473526

474-
pub(crate) fn computed_val(&self, for_version: VersionNumber) -> DiceComputedValue {
527+
/// Restores the in-memory hydrated value (typically after deserializing from
528+
/// disk). Keeps any existing `data_key` so the next page-out skips
529+
/// re-serialization.
530+
#[expect(
531+
dead_code,
532+
reason = "used by the rehydrate request; D101759759 will remove this suppression"
533+
)]
534+
pub(crate) fn rehydrate(&mut self, value: DiceValidValue) {
535+
self.res.value = Some(value);
536+
}
537+
538+
/// `expect_hydrated_msg` is forwarded to `expect_hydrated` and should explain why the
539+
/// caller knows the entry is hydrated.
540+
pub(crate) fn computed_val(
541+
&self,
542+
for_version: VersionNumber,
543+
expect_hydrated_msg: &str,
544+
) -> DiceComputedValue {
475545
DiceComputedValue::new(
476-
MaybeValidDiceValue::valid(self.res.dupe()),
546+
MaybeValidDiceValue::valid(self.val().expect_hydrated(expect_hydrated_msg).dupe()),
477547
self.metadata.verified_ranges.dupe(),
478548
self.invalidation_paths.at_version(for_version),
479549
)
@@ -487,13 +557,20 @@ impl OccupiedGraphNode {
487557
) -> InvalidateResult<'_> {
488558
// TODO(cjhopman): accepting injections only for InjectedKey would make the VersionedGraph simpler. Currently, this is used
489559
// for "mocking" dice keys in tests via DiceBuilder::mock_and_return().
490-
if self.val().equality(&value) {
560+
if self
561+
.val()
562+
.expect_hydrated(
563+
"on_injected does not yet handle paged-out entries; this is a known \
564+
footgun fixed in a follow-up commit",
565+
)
566+
.equality(&value)
567+
{
491568
// TODO(cjhopman): This is wrong. The node could currently be in a dirtied state and we
492569
// aren't recording that the value is verified at this version.
493570
return InvalidateResult::NoChange;
494571
}
495572

496-
self.res = value;
573+
self.res = PagableNodeValue::hydrated(value);
497574
self.metadata.deps = Arc::new(SeriesParallelDeps::None);
498575
self.metadata.verified_ranges = Arc::new(VersionRange::begins_with(version).into_ranges());
499576
self.invalidation_paths
@@ -508,19 +585,43 @@ impl OccupiedGraphNode {
508585

509586
fn at_version(&self, v: VersionNumber) -> VersionedGraphResult {
510587
match self.metadata.verified_ranges.find_value_upper_bound(v) {
511-
Some(found) if found == v => VersionedGraphResult::Match(self.computed_val(v)),
588+
Some(found) if found == v => {
589+
if self.res.value.is_some() {
590+
VersionedGraphResult::Match(
591+
self.computed_val(v, "the Hydrated branch checked value.is_some()"),
592+
)
593+
} else {
594+
VersionedGraphResult::MatchPagedOut(PagedOutMatch {
595+
data_key: self.res.data_key.expect(
596+
"PagableNodeValue invariant: at least one of value or data_key is set",
597+
),
598+
valid: self.metadata.verified_ranges.dupe(),
599+
invalidation_paths: self.invalidation_paths.at_version(v),
600+
})
601+
}
602+
}
512603
Some(prev_verified_version) => {
513604
if self
514605
.metadata
515606
.dirtied_history
516607
.restricted_range(v)
517608
.contains(&prev_verified_version)
518609
{
519-
VersionedGraphResult::CheckDeps(VersionedGraphResultMismatch {
520-
entry: self.val().dupe(),
521-
prev_verified_version,
522-
deps_to_validate: self.metadata.deps.dupe(),
523-
})
610+
if let Some(value) = &self.res.value {
611+
VersionedGraphResult::CheckDeps(VersionedGraphResultMismatch {
612+
entry: value.dupe(),
613+
prev_verified_version,
614+
deps_to_validate: self.metadata.deps.dupe(),
615+
})
616+
} else {
617+
VersionedGraphResult::CheckDepsPagedOut(PagedOutMismatch {
618+
data_key: self.res.data_key.expect(
619+
"PagableNodeValue invariant: at least one of value or data_key is set",
620+
),
621+
prev_verified_version,
622+
deps_to_validate: self.metadata.deps.dupe(),
623+
})
624+
}
524625
} else {
525626
VersionedGraphResult::Compute
526627
}

dice/dice/src/impls/core/graph/storage.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ impl VersionedGraph {
365365
invalidation_paths,
366366
);
367367

368-
let res = entry.computed_val(v);
368+
let res = entry.computed_val(v, "newly-constructed OccupiedGraphNode is always hydrated");
369369
self.nodes.insert(key, VersionedGraphNode::Occupied(entry));
370370
res
371371
}
@@ -407,7 +407,11 @@ impl ValueReusable {
407407
if new_deps != &***value.deps() {
408408
return false;
409409
}
410-
new_value.equality(value.val())
410+
new_value.equality(value.val().expect_hydrated(
411+
"is_reusable: no node is paged out at this point in the stack \
412+
(page_out is wired up in a follow-up commit); the paged-out case \
413+
is fixed there to return false instead",
414+
))
411415
}
412416
// For version-based, the deps are guaranteed to match if `version` is in the node's verified versions.
413417
ValueReusable::VersionBased(version) => value.is_verified_at(*version),

dice/dice/src/impls/core/graph/types.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@
1313
use dupe::Dupe;
1414
use gazebo::variants::UnpackVariants;
1515
use gazebo::variants::VariantName;
16+
use pagable::DataKey;
1617

1718
use crate::arc::Arc;
1819
use crate::impls::deps::graph::SeriesParallelDeps;
1920
use crate::impls::key::DiceKey;
2021
use crate::impls::value::DiceComputedValue;
2122
use crate::impls::value::DiceValidValue;
23+
use crate::impls::value::TrackedInvalidationPaths;
2224
use crate::versions::VersionNumber;
25+
use crate::versions::VersionRanges;
2326

2427
/// The Key for a Versioned, incremental computation
2528
#[derive(Copy, Clone, Dupe, Debug)]
@@ -43,13 +46,54 @@ pub(crate) struct VersionedGraphResultMismatch {
4346
pub(crate) deps_to_validate: Arc<SeriesParallelDeps>,
4447
}
4548

49+
/// Equivalent of [`DiceComputedValue`] for the case where the matched entry's value is
50+
/// paged out. The worker hydrates `data_key` via `DiceStorage` to materialize the value
51+
/// before constructing a `DiceComputedValue`.
52+
#[derive(Debug)]
53+
#[expect(
54+
dead_code,
55+
reason = "fields consumed by the worker; D101759759 will remove this suppression"
56+
)]
57+
pub(crate) struct PagedOutMatch {
58+
pub(crate) data_key: DataKey,
59+
pub(crate) valid: Arc<VersionRanges>,
60+
pub(crate) invalidation_paths: TrackedInvalidationPaths,
61+
}
62+
63+
/// Equivalent of [`VersionedGraphResultMismatch`] for the case where the previous entry's
64+
/// value is paged out. The worker hydrates `data_key` via `DiceStorage` to materialize the
65+
/// previous value before deciding whether deps still hold.
66+
#[derive(Debug)]
67+
#[expect(
68+
dead_code,
69+
reason = "fields consumed by the worker; D101759759 will remove this suppression"
70+
)]
71+
pub(crate) struct PagedOutMismatch {
72+
pub(crate) data_key: DataKey,
73+
pub(crate) prev_verified_version: VersionNumber,
74+
pub(crate) deps_to_validate: Arc<SeriesParallelDeps>,
75+
}
76+
4677
#[derive(Debug, VariantName, UnpackVariants)]
4778
pub(crate) enum VersionedGraphResult {
4879
/// the entry is present and valid at the requested version
4980
Match(DiceComputedValue),
81+
/// the entry is present and valid at the requested version, but its value is paged
82+
/// out and must be hydrated before use
83+
#[expect(
84+
dead_code,
85+
reason = "constructed when paging actually happens; D101759759 will remove this suppression"
86+
)]
87+
MatchPagedOut(PagedOutMatch),
5088
/// the entry at the requested version has been invalidated and
5189
/// we have a previous value with deps to possibly resurrect
5290
CheckDeps(VersionedGraphResultMismatch),
91+
/// like `CheckDeps`, but the previous entry's value is paged out
92+
#[expect(
93+
dead_code,
94+
reason = "constructed when paging actually happens; D101759759 will remove this suppression"
95+
)]
96+
CheckDepsPagedOut(PagedOutMismatch),
5397
/// the entry is missing or there's no previously valid value to check
5498
Compute,
5599
/// the storage has rejected the request

dice/dice/src/impls/worker.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,15 @@ impl DiceTaskWorker {
161161
VersionedGraphResult::Match(entry) => {
162162
return task_state.lookup_matches(handle, entry);
163163
}
164+
VersionedGraphResult::MatchPagedOut(_) => {
165+
// Page-in is wired up in a follow-up commit. Until then, no node ever
166+
// ends up paged out (see DiceStorage), so this branch is unreachable.
167+
unreachable!("paged-out lookup result not yet supported by worker")
168+
}
164169
VersionedGraphResult::CheckDeps(mismatch2) => Some(mismatch2),
170+
VersionedGraphResult::CheckDepsPagedOut(_) => {
171+
unreachable!("paged-out lookup result not yet supported by worker")
172+
}
165173
VersionedGraphResult::Compute => None,
166174
VersionedGraphResult::Rejected(..) => {
167175
return Err(CancellationReason::Rejected);

0 commit comments

Comments
 (0)